6 Commits
Author SHA1 Message Date
blandClaude Opus 5 70b9f9c647 enok 0.4.0
test / test (push) Successful in 1m20s
Release enok / bygg (release) Successful in 1m13s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-20 19:57:06 +02:00
blandClaude Opus 5 77c5d15042 Ta vare på manuelle endringer ved ny import
«enok tillegg modell.ifc bygning.toml» skriver forskjellen mellom modellen og en håndredigert
bygningsbeskrivelse som en tilleggsfil: endrede verdier, nye oppføringer, «slett = true» for
det bare modellen har, og scenarioene. Kommandoen kontrollerer at tillegget gjenskaper filen
det er laget av, og sier fra om noe ikke lar seg uttrykke, siden fletting ikke kan fjerne et
enkeltfelt.

«enok import» nekter å skrive over en fil enok ikke har laget selv, og viser til kommandoen
over. --overskriv går forbi.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-16 17:51:19 +02:00
blandClaude Opus 5 32ecb0b132 Beregn rett fra en BIM-modell, uten bygning.toml
«enok beregn modell.ifc» leser modellen, fletter inn en eventuell tilleggsfil og skriver
bygningsbeskrivelsen den kom fram til, i utdatakatalogen, slik at beregningen kan etterprøves.

Importen sier nå hvilke bygningsdeler som mangler U-verdi, ikke bare hvor mange. Vinduer og
dører uten U-verdi i modellen får en typisk verdi for byggeåret, slik flatene alltid har fått,
i stedet for faste 2,4 og 2,0 W/(m²K).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-16 14:39:58 +02:00
blandClaude Opus 5 6d87ef496f Lag bygning.toml av IFC-modellen og en tilleggsfil
«enok import modell.ifc --tillegg prosjekt.toml» fletter inn det modellen ikke beskriver,
som varmeanlegg og tiltak, og rettelser der den mangler noe. Tilleggsfilen kan holde rom
utenfor oppvarmet BRA, og et navn med «*» treffer alle elementer det passer på, også i
scenarioer.

Importen er nå deterministisk: entitetene leses i fast rekkefølge, helningen arealvektes
når flater slås sammen, og skrå tak får alltid normalen oppover. Vegger får himmelretningen
i navnet, slik at tilleggsfiler kan vise til dem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-15 20:05:20 +02:00
blandClaude Opus 5 d02a3d9666 Legg til barnehage, kontor-, skole-, idretts- og kulturbygg
Driftstidene har nå dager per uke og uker per år, og lastene gjelder bare på driftsdager.
Normerte verdier for de nye kategoriene er hentet fra publiserte Simien-utskrifter og
DiBK-utredninger og kontrollert mot energibudsjettene i de samme kildene. Boligblokk har
fått egne verdier i stedet for en kopi av småhus.

Balanserte anlegg kan ha kjølebatteri, og varmegjenvinneren reguleres ned når den ellers
ville varmet tilluften over settpunktet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-15 20:05:20 +02:00
blandClaude Opus 5 cb64c91b19 Fjern kundeopplysninger fra eksempler, tester og dokumentasjon
test / test (push) Successful in 1m42s
enok er et offentlig lager. Eksempelfilen, testene og README inneholdt navn og
adresse til en privat kunde, og navnet på modellen fra kundeprosjektet. Eksemplet
heter nå «Eldre enebolig med kjeller» uten adresse eller oppdragsgiver, og testene
bruker oppdiktede adresser og navn.

Opplysningene finnes fortsatt i tidligere commits i historikken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-15 15:45:08 +02:00
27 changed files with 1350 additions and 132 deletions
Generated
+1 -1
View File
@@ -222,7 +222,7 @@ dependencies = [
[[package]] [[package]]
name = "enok" name = "enok"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "enok" name = "enok"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
license = "MIT" license = "MIT"
+122 -12
View File
@@ -13,7 +13,7 @@ enok beregn bygning.toml
``` ```
``` ```
Nordhusstykket 5 (Småhus, 140 m², byggeår 1948) Eldre enebolig med kjeller (Småhus, 140 m², byggeår 1948)
Klimadata: Indre Østfold Klimadata: Indre Østfold
Varmetapstall 3,00 W/(m²K) Varmetapstall 3,00 W/(m²K)
@@ -68,6 +68,17 @@ bygning med store vindusflater fått urealistisk høye innetemperaturer om våre
lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte
settpunkttemperaturen for kjøling, og luftingen koster ingen energi. settpunkttemperaturen for kjøling, og luftingen koster ingen energi.
**Ventilasjon.** Varmegjenvinneren reguleres ned når den ellers ville varmet tilluften over
settpunktet. Et anlegg med `kjolebatteri = true` holder tilluften på normert temperatur i
driftstiden, slik DiBKs modellbygg for energirammene er regnet, og energien føres som
ventilasjonskjøling.
**Bygningskategorier.** Normerte laster og driftstider er lagt inn for småhus, boligblokk,
barnehage, kontorbygg, skolebygg, idrettsbygg og kulturbygg. Driftstidene har timer per døgn,
dager per uke og uker per år; lastene gjelder bare på driftsdager, og ukene uten drift legges
i ferier. For sykehus, sykehjem, hotell, forretningsbygg, universitet og lett industri er det
ikke funnet offentlige kilder, og de krever en egen fil i `--data KATALOG/normerte/`.
**Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell. **Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell.
## Installasjon ## Installasjon
@@ -84,12 +95,15 @@ Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen
| Kommando | Hva den gjør | | Kommando | Hva den gjør |
| --- | --- | | --- | --- |
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` | | `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
| `enok beregn modell.ifc` | Beregner rett fra en modell, uten at `bygning.toml` finnes fra før |
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` | | `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
| `enok tillegg modell.ifc bygning.toml` | Skriver forskjellen mellom modellen og en håndredigert fil som tilleggsfil |
| `enok klima --liste` | Lister alle kommuner med klimadata | | `enok klima --liste` | Lister alle kommuner med klimadata |
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted | | `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon | | `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
Nyttige valg til `beregn`: `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`, Nyttige valg til `beregn`: `--tillegg FIL`, `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
`--uten-scenarier`, `--ut KATALOG`. `--uten-scenarier`, `--ut KATALOG`.
### Utdata ### Utdata
@@ -111,7 +125,7 @@ antatt og hvor det kommer fra.
```toml ```toml
[bygning] [bygning]
navn = "Nordhusstykket 5" navn = "Eldre enebolig med kjeller"
kategori = "smaahus" kategori = "smaahus"
byggeaar = 1948 byggeaar = 1948
oppvarmet_bra = 140.0 oppvarmet_bra = 140.0
@@ -154,6 +168,7 @@ over_terreng = 0.6 # høyden som står over terreng
[ventilasjon] [ventilasjon]
type = "naturlig" # naturlig | avtrekk | balansert type = "naturlig" # naturlig | avtrekk | balansert
# kjolebatteri = true # balansert anlegg med kjølebatteri
[[oppvarming.kilde]] [[oppvarming.kilde]]
type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, … type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, …
@@ -167,6 +182,7 @@ endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element. matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
Ferdige eksempler ligger i `eksempler/`. Ferdige eksempler ligger i `eksempler/`.
@@ -178,6 +194,73 @@ Ferdige eksempler ligger i `eksempler/`.
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag | | gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel | | Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
### Modellen og en tilleggsfil
Modellen er kilden til alt den kan beskrive, og en modell alene gir en ferdig rapport: enok
fyller hullene med normerte verdier og typiske verdier for byggeåret, og rapporten sier hva
som er antatt. Det modellen ikke kan beskrive, som varmeanlegg, lekkasjetall og tiltak, og
rettelser der den mangler noe, legges i en tilleggsfil som flettes inn etter de samme reglene
som scenarioene:
```sh
enok beregn modell.ifc # bare modellen
enok beregn modell.ifc --tillegg prosjekt.toml # modellen og tilleggsfilen
enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml # samme, som fil
```
`beregn` skriver bygningsbeskrivelsen modellen ga, til `bygning.toml` i utdatakatalogen, slik
at beregningen kan etterprøves.
En ny import skriver aldri over manuelle endringer, for de ligger ikke i den genererte filen.
Har du likevel en `bygning.toml` du har redigert for hånd, lager `enok tillegg` en tilleggsfil
av forskjellen mot modellen, så arbeidet følger med videre:
```sh
enok tillegg modell.ifc bygning.toml --ut prosjekt.toml
```
Tillegget inneholder alt filen sier som modellen ikke sier: endrede U-verdier, drift,
varmeanlegg og scenarioer, og `slett = true` for oppføringer modellen har og filen ikke.
Kommandoen kontrollerer at tillegget gjenskaper filen det er laget av, og sier fra om noe
ikke lar seg uttrykke. `enok import` nekter dessuten å skrive over en fil enok ikke har laget
selv, med mindre `--overskriv` er oppgitt.
```toml
# prosjekt.toml
[import]
uoppvarmede_rom = ["Kott", "Bod"] # holdes utenfor oppvarmet BRA
[[flate]]
navn = "Yttervegg*" # alle ytterveggene, uansett himmelretning
masseklasse = "lett"
[[flate]]
navn = "Roof Slab" # skråtaket mangler U-verdi i modellen
u_verdi = 0.55
[[vindu]]
navn = "V-*"
g_verdi = 0.76
[[sone]]
navn = "Kjeller"
andel_distribusjonstap = 1.0
[[sone.flate]]
navn = "Kjellermur*"
over_terreng = 0.6
[[oppvarming.kilde]]
type = "el_kjel"
andel_rom = 1.0
andel_tappevann = 1.0
```
Flatene får navn etter konstruksjonen i modellen, og vegger får himmelretningen etter navnet
(«Yttervegg, sør»), slik at tilleggsfilen kan vise til dem. Importen er deterministisk: samme
modell og tilleggsfil gir alltid samme `bygning.toml`. Et mønster som ikke treffer noe, nevnes
i merknadene, og et vanlig navn som ikke finnes i modellen, legges til som ny oppføring.
### Hva enok henter ut av en IFC-modell ### Hva enok henter ut av en IFC-modell
| Opplysning | Hvor den kommer fra | | Opplysning | Hvor den kommer fra |
@@ -223,6 +306,10 @@ egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` | | `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
| `OppvarmetBRA` | `HeatedFloorArea` | `120` | | `OppvarmetBRA` | `HeatedFloorArea` | `120` |
Mangler en bygningsdel U-verdi i modellen, sier importen hvilke det gjelder, og beregningen
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
eksportene ikke har U-verdi.
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
modellen alene. modellen alene.
@@ -235,8 +322,8 @@ Dette i selve modellen er verdt å gjøre riktig, omtrent i rekkefølge etter hv
- **Georeferer tegningen, eller sett prosjektstedet.** Da får modellen koordinater, og - **Georeferer tegningen, eller sett prosjektstedet.** Da får modellen koordinater, og
beregningen velger selv nærmeste av de 358 klimastedene. Uten det står Vectorworks' beregningen velger selv nærmeste av de 358 klimastedene. Uten det står Vectorworks'
standardverdi i Washington, og den blir oversett. standardverdi i Washington, og den blir oversett.
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av - **Merk yttervegger med `IsExternal`.** I en typisk eksport er bare en liten del av
hundre og fem vegger merket, og kjellermurene er ikke blant dem. veggene merket, og kjellermurene er sjelden blant dem.
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke. - **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den - **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
uoppvarmede kjelleren og regner oppvarmet areal riktig. uoppvarmede kjelleren og regner oppvarmet areal riktig.
@@ -249,7 +336,7 @@ Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i
```csv ```csv
type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
bygning,Nordhusstykket 5,140,,,,smaahus,1948, bygning,Eldre enebolig,140,,,,smaahus,1948,
vegg,Yttervegg sør,26,1.0,180,90,ute,, vegg,Yttervegg sør,26,1.0,180,90,ute,,
gulv,Mot kjeller,98,0.8,,180,uoppvarmet,, gulv,Mot kjeller,98,0.8,,180,uoppvarmet,,
vindu,Stue sør,3.0,2.8,180,90,,0.76,2 vindu,Stue sør,3.0,2.8,180,90,,0.76,2
@@ -267,8 +354,9 @@ vertskjøreren `fish` på samme måte som de andre prosjektene:
### Energirapport fra et byggeprosjekt ### Energirapport fra et byggeprosjekt
Et prosjektlager med sin egen IFC-modell og `bygning.toml` trenger ikke Rust. Det henter Et prosjektlager med sin egen IFC-modell og tilleggsfil trenger ikke Rust. Det henter
binærfilen fra enoks siste utgivelse: binærfilen fra enoks siste utgivelse. Én jobb lager `bygning.toml` av modellen, og en annen
beregner rapporten av den:
```yaml ```yaml
name: Energirapport name: Energirapport
@@ -276,14 +364,14 @@ on:
release: release:
types: [published] types: [published]
jobs: jobs:
rapport: bygningsmodell:
runs-on: fish runs-on: fish
defaults: defaults:
run: run:
shell: fish {0} shell: fish {0}
steps: steps:
- uses: https://gitea.com/actions/checkout@v6 - uses: https://gitea.com/actions/checkout@v6
- name: Hent enok og lag rapporten - name: Hent enok og lag bygning.toml
env: env:
TOKEN: ${{ github.token }} TOKEN: ${{ github.token }}
run: | run: |
@@ -293,6 +381,27 @@ jobs:
curl -sSL --fail -o enok --header "Authorization: token $TOKEN" \ curl -sSL --fail -o enok --header "Authorization: token $TOKEN" \
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok" "$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
chmod +x enok chmod +x enok
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
- uses: https://gitea.com/actions/upload-artifact@v4
with:
name: bygningsmodell
path: |
bygning.toml
enok
rapport:
needs: bygningsmodell
runs-on: fish
defaults:
run:
shell: fish {0}
steps:
- uses: https://gitea.com/actions/checkout@v6
- uses: https://gitea.com/actions/download-artifact@v4
with:
name: bygningsmodell
- name: Lag rapporten
run: |
chmod +x enok
./enok beregn bygning.toml --ut dist ./enok beregn bygning.toml --ut dist
``` ```
@@ -310,12 +419,13 @@ Motoren er kontrollert mot uavhengige referanser:
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært | | Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) | | Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
| Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) | | Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) |
| Faste energiposter for yrkesbyggene | belysning, utstyr og varmtvann treffer energibudsjettene i kildene |
### Kjent avvik ### Kjent avvik
Standard Norges lanseringseksempel for NS 3031:2025 oppgir 46,2 kWh/(m²·år) romoppvarming og Standard Norges lanseringseksempel for NS 3031:2025 oppgir 46,2 kWh/(m²·år) romoppvarming og
106,4 kWh/(m²·år) netto energibehov for TEK17-referansebygget beregnet med Simien 8. enok 106,4 kWh/(m²·år) netto energibehov for TEK17-referansebygget beregnet med Simien 8. enok
beregner 27,2 og 86,7 for den samme bygningen. beregner 27,1 og 86,6 for den samme bygningen.
Varmetapsbudsjettet, de normerte energipostene, solinnstrålingen og energibalansen stemmer Varmetapsbudsjettet, de normerte energipostene, solinnstrålingen og energibalansen stemmer
hver for seg, så avviket ligger i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025 hver for seg, så avviket ligger i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025
@@ -336,7 +446,7 @@ Alle normerte verdier ligger i `data/` og kan overstyres med `--data KATALOG`.
| --- | --- | | --- | --- |
| `skala.toml` | Energikarakterskalaen fra 1.1.2026, Enova SF via Multiconsult-notatet «Grunnlag for ny energikarakterskala», 19.12.2025 | | `skala.toml` | Energikarakterskalaen fra 1.1.2026, Enova SF via Multiconsult-notatet «Grunnlag for ny energikarakterskala», 19.12.2025 |
| `vekting.toml` | Vektingsfaktorer etter energimerkeforskriften § 10a med NVEs veileder | | `vekting.toml` | Vektingsfaktorer etter energimerkeforskriften § 10a med NVEs veileder |
| `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann er oppdatert til NS 3031:2025-verdien for småhus. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene | | `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann for boliger er oppdatert til NS 3031:2025-verdien for småhus. Tillegg A er ikke offentlig, så yrkesbyggverdiene er hentet fra publiserte Simien-utskrifter og DiBK-utredninger. Hver fil oppgir kildene og hvilke verdier som er antatt. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene |
| `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 | | `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 |
| `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode | | `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode |
| `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 | | `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 |
+64
View File
@@ -0,0 +1,64 @@
# Normerte inndata for barnehage.
#
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra offentlige
# energiberegninger som oppgir tabellverdiene for kategorien, og er kontrollert mot
# energibudsjettene i de samme rapportene:
# - Rambøll, «Kolstad barnehage energikonsept» (Mercell, 2019): driftstid 10/5/52,
# utstyr 2 W/m², varmtvann 1,6 W/m², personer 6 W/m², «iht. NS 3031 tillegg A,
# tab. A.1A.3, for bygningskategori barnehage». Budsjettet (utstyr 5,2 og varmtvann
# 10,0 kWh/m²) stemmer med 261 driftsdager.
# - Multiconsult for DiBK, «Grunnlag og forutsetninger for beregnede energirammer»
# (13.11.2015): personbelastning 6,0 W/m² i NS 3031.
# Belysning: rapporten over brukte passivhusverdien 5 W/m². NS 3031-verdien er ikke
# funnet i en offentlig kilde; 8 W/m² er antatt, som for kontor-, kultur- og idrettsbygg.
#
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på
# driftsdager, slik Simien oppgir den («effektbehov varmtvann på driftsdager»).
kategori = "barnehage"
kilde = "NS 3031:2014 Tillegg A slik gjengitt i offentlige energiberegninger (Rambøll 2019, Multiconsult/DiBK 2015); belysning antatt"
[settpunkt]
oppvarming = 21.0
oppvarming_natt = 19.0 # Rambøll oppgir årsmiddel 19,8 °C, som svarer til nattsenking
kjoling = 22.0
ventilasjon_tilluft = 19.0
[driftstid]
oppvarming = { start = 7, slutt = 17 } # 10 h
belysning = { start = 7, slutt = 17 }
utstyr = { start = 7, slutt = 17 }
varmtvann = { start = 0, slutt = 24 }
personer = { start = 7, slutt = 17 }
ventilasjon = { start = 7, slutt = 17 }
dager_per_uke = 5
uker_per_aar = 52 # 261 driftsdager
[belysning]
effekt = 8.0 # antatt → 20,9 kWh/(m²·år)
varmetilskudd = 8.0
[utstyr]
effekt = 2.0 # → 5,2 kWh/(m²·år)
varmetilskudd = 2.0
[varmtvann]
effekt = 1.6 # → 10,0 kWh/(m²·år)
varmetilskudd = 0.0
[personer]
varmetilskudd = 6.0
[ventilasjon]
luftmengde_driftstid = 10.0 # m³/(h·m²), prosjektverdi i Rambøll-rapporten
luftmengde_utenfor_driftstid = 2.0
sfp = 1.5
temperaturvirkningsgrad = 0.80
frostsikring_virkningsgrad = 0.80
[termisk]
varmekapasitet_normalisert = 72.6
kuldebroer_normalisert = 0.07
[pumper]
effekt_vannbaaren = 0.5
+9 -7
View File
@@ -1,11 +1,13 @@
# Normerte inndata for boligblokk. # Normerte inndata for boligblokk.
# Standardverdier fra NS 3031:2014 Tillegg A (Tabell A.3/A.5) slik de også fremgår av Enovas # NS 3031:2014 Tillegg A gir de samme verdiene for boligblokk som for småhus. Det bekreftes
# XML-eksempel for energimerkesystemet. NS 3031:2025 Tillegg A gir timesprofiler over døgnet; # av Simien-utskriften for en boligblokk (Multiconsult 2013, «NS3031, tabell A.1A.3»):
# eieren av standarden kan legge inn 24-verdiers profiler i feltene `profil_*` nedenfor, som da # belysning 1,95 W/m², utstyr 3,00 W/m² med varmetilskudd 1,80, varmtvann 3,40 W/m² over
# overstyrer driftstidene. Alle effekter i W per m² oppvarmet BRA. # døgnet (29,8 kWh/m²), personer 1,5 W/m², driftstid 16 h og personer/ventilasjon 24 h.
# Varmtvann følger småhusverdien fra NS 3031:2025 (25,1 kWh/m²); verdien for boligblokk i
# 2025-utgaven er ikke funnet i en offentlig kilde. Alle effekter i W per m² oppvarmet BRA.
kategori = "boligblokk" kategori = "boligblokk"
kilde = "NS 3031:2014 Tillegg A (standardverdier); NS 3031:2025-profiler kan legges inn" kilde = "NS 3031:2014 Tillegg A slik gjengitt i Simien-utskrift (Multiconsult 2013); luftmengde fra Multiconsult/DiBK 2014"
[settpunkt] [settpunkt]
oppvarming = 21.0 # °C i driftstiden oppvarming = 21.0 # °C i driftstiden
@@ -42,8 +44,8 @@ varmetilskudd = 0.0
varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden
[ventilasjon] [ventilasjon]
luftmengde_driftstid = 1.2 # m³/(h·m²) luftmengde_driftstid = 1.5 # m³/(h·m²); modellbygget for boligblokk (Multiconsult/DiBK 2014,
luftmengde_utenfor_driftstid = 1.2 # m³/(h·) luftmengde_utenfor_driftstid = 1.5 # avsnitt 8.2), svarer til leiligheter på om lag 65
sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon
temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon
frostsikring_virkningsgrad = 0.80 frostsikring_virkningsgrad = 0.80
+61
View File
@@ -0,0 +1,61 @@
# Normerte inndata for idrettsbygg.
#
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra to uavhengige,
# offentlige Simien-utskrifter for idrettshaller:
# - «Energiberegning og evaluering mot energikrav» (Mercell, 31.10.2018), sone
# Idrettshall: driftstid 12 h, belysning 8 W/m², utstyr 1 W/m², varmtvann 9,5 W/m²
# på driftsdager, personer 10 W/m², luftmengde 8 og 2 m³/(h·m²).
# - «Evaluering Energiregler 2016», Gimlehallen (BLOM Arkitekter, 2024): de samme
# verdiene for utstyr, varmtvann, personer og luftmengder (belysningen er prosjektverdi).
# Begge budsjettene (varmtvann 50,2 og utstyr 2,6 kWh/m²) svarer til 220 driftsdager,
# altså 5 dager i 44 uker. Begge utskriftene oppgir settpunkt 18,0 °C.
#
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
kategori = "idrettsbygg"
kilde = "NS 3031:2014 Tillegg A slik gjengitt i to offentlige Simien-utskrifter (2018, 2024)"
[settpunkt]
oppvarming = 18.0
oppvarming_natt = 18.0 # utskriftene oppgir 18,0 °C som årsmiddel
kjoling = 22.0
ventilasjon_tilluft = 18.0
[driftstid]
oppvarming = { start = 8, slutt = 20 } # 12 h
belysning = { start = 8, slutt = 20 }
utstyr = { start = 8, slutt = 20 }
varmtvann = { start = 0, slutt = 24 }
personer = { start = 8, slutt = 20 }
ventilasjon = { start = 8, slutt = 20 }
dager_per_uke = 5
uker_per_aar = 44 # 220 driftsdager
[belysning]
effekt = 8.0 # → 21,1 kWh/(m²·år)
varmetilskudd = 8.0
[utstyr]
effekt = 1.0 # → 2,6 kWh/(m²·år)
varmetilskudd = 1.0
[varmtvann]
effekt = 9.5 # → 50,2 kWh/(m²·år)
varmetilskudd = 0.0
[personer]
varmetilskudd = 10.0
[ventilasjon]
luftmengde_driftstid = 8.0 # m³/(h·m²)
luftmengde_utenfor_driftstid = 2.0
sfp = 1.5
temperaturvirkningsgrad = 0.80
frostsikring_virkningsgrad = 0.80
[termisk]
varmekapasitet_normalisert = 72.6
kuldebroer_normalisert = 0.07
[pumper]
effekt_vannbaaren = 0.5
+63
View File
@@ -0,0 +1,63 @@
# Normerte inndata for kontorbygg.
#
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra offentlige kilder:
# - Multiconsult for DiBK, «Konsekvensvurdering av Energiregler 2015» (11.12.2014),
# avsnitt 7.1 og 8.2: belysning 8 W/m² og veiledende luftmengde 10 m³/(h·m²) etter
# NS 3031 tabell A1 og B1.
# - Simien-utskrift «Passivhusevaluering», kontorbygg (Mercell): driftstid 12 h,
# varmtvann 0,80 W/m² på driftsdager, personer 4 W/m². Personbelastningen og
# driftstiden er de samme som i NS 3031; belysning og utstyr er passivhusverdier.
# - Utstyr 11 W/m²: Simien-evaluering for et kontorbygg (Asplan Viak 2019, FutureBuilt).
# Bare søkeutdraget er lest; fulltekstdokumentet var ikke tilgjengelig.
# - NVE, «Praktisk veileder for energimerking» (2013): kontorbygg har driftstid 5 dager
# i 52 uker.
#
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
kategori = "kontorbygg"
kilde = "NS 3031:2014 Tillegg A slik gjengitt i offentlige kilder (Multiconsult/DiBK 2014, Simien-utskrifter); utstyr ikke verifisert i fulltekst"
[settpunkt]
oppvarming = 21.0
oppvarming_natt = 19.0
kjoling = 22.0
ventilasjon_tilluft = 19.0
[driftstid]
oppvarming = { start = 7, slutt = 19 } # 12 h
belysning = { start = 7, slutt = 19 }
utstyr = { start = 7, slutt = 19 }
varmtvann = { start = 0, slutt = 24 }
personer = { start = 7, slutt = 19 }
ventilasjon = { start = 7, slutt = 19 }
dager_per_uke = 5
uker_per_aar = 52 # 261 driftsdager
[belysning]
effekt = 8.0 # → 25,1 kWh/(m²·år)
varmetilskudd = 8.0
[utstyr]
effekt = 11.0 # → 34,5 kWh/(m²·år)
varmetilskudd = 11.0
[varmtvann]
effekt = 0.8 # → 5,0 kWh/(m²·år)
varmetilskudd = 0.0
[personer]
varmetilskudd = 4.0
[ventilasjon]
luftmengde_driftstid = 10.0 # m³/(h·m²), NS 3031 tabell B1 (veiledende)
luftmengde_utenfor_driftstid = 2.0 # som i Simien-utskriftene for yrkesbygg
sfp = 1.5
temperaturvirkningsgrad = 0.80
frostsikring_virkningsgrad = 0.80
[termisk]
varmekapasitet_normalisert = 72.6
kuldebroer_normalisert = 0.07
[pumper]
effekt_vannbaaren = 0.5
+59
View File
@@ -0,0 +1,59 @@
# Normerte inndata for kulturbygg.
#
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra COWI,
# «Energikonsept Kystens Arv» (Mercell), tabell 7 «Standardverdier for internlaster etter
# NS 3031» og Simien-utskriften i samme rapport: belysning 8 W/m², utstyr 1 W/m²,
# varmtvann 1,6 W/m² på driftsdager og personer 3,2 W/m², luftmengde 10 og 2 m³/(h·m²).
# Rapporten skriver driftstid 12/5/52, men Simien-utskriften har 11 h, og energibehovene
# i tabell 7 (belysning 23, utstyr 2,9 og varmtvann 10 kWh/m²) stemmer med 11 h i
# 261 driftsdager.
#
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
kategori = "kulturbygg"
kilde = "NS 3031:2014 Tillegg A slik gjengitt i COWI, Energikonsept Kystens Arv"
[settpunkt]
oppvarming = 21.0
oppvarming_natt = 19.0 # utskriften oppgir årsmiddel 19,9 °C
kjoling = 22.0
ventilasjon_tilluft = 19.0
[driftstid]
oppvarming = { start = 10, slutt = 21 } # 11 h
belysning = { start = 10, slutt = 21 }
utstyr = { start = 10, slutt = 21 }
varmtvann = { start = 0, slutt = 24 }
personer = { start = 10, slutt = 21 }
ventilasjon = { start = 10, slutt = 21 }
dager_per_uke = 5
uker_per_aar = 52 # 261 driftsdager
[belysning]
effekt = 8.0 # → 23,0 kWh/(m²·år)
varmetilskudd = 8.0
[utstyr]
effekt = 1.0 # → 2,9 kWh/(m²·år)
varmetilskudd = 1.0
[varmtvann]
effekt = 1.6 # → 10,0 kWh/(m²·år)
varmetilskudd = 0.0
[personer]
varmetilskudd = 3.2
[ventilasjon]
luftmengde_driftstid = 10.0 # m³/(h·m²)
luftmengde_utenfor_driftstid = 2.0
sfp = 1.5
temperaturvirkningsgrad = 0.80
frostsikring_virkningsgrad = 0.80
[termisk]
varmekapasitet_normalisert = 72.6
kuldebroer_normalisert = 0.07
[pumper]
effekt_vannbaaren = 0.5
+58
View File
@@ -0,0 +1,58 @@
# Normerte inndata for skolebygg.
#
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra en offentlig
# Simien-utskrift, «Energiberegning og evaluering mot energikrav» for en skole
# (Mercell, 31.10.2018), sonene for arbeidsrom og klasserom: driftstid 10 h, belysning
# 10 W/m², utstyr 6 W/m², varmtvann 1,9 W/m² på driftsdager og personer 12 W/m².
# Budsjettet i rapporten (varmtvann 10,0 kWh/m²) svarer til 220 driftsdager, altså
# 5 dager i 44 uker.
#
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
kategori = "skolebygg"
kilde = "NS 3031:2014 Tillegg A slik gjengitt i en offentlig Simien-utskrift (2018)"
[settpunkt]
oppvarming = 21.0
oppvarming_natt = 19.0 # utskriften oppgir årsmiddel 19,8 °C
kjoling = 22.0
ventilasjon_tilluft = 19.0
[driftstid]
oppvarming = { start = 8, slutt = 18 } # 10 h
belysning = { start = 8, slutt = 18 }
utstyr = { start = 8, slutt = 18 }
varmtvann = { start = 0, slutt = 24 }
personer = { start = 8, slutt = 18 }
ventilasjon = { start = 8, slutt = 18 }
dager_per_uke = 5
uker_per_aar = 44 # 220 driftsdager
[belysning]
effekt = 10.0 # → 22,0 kWh/(m²·år)
varmetilskudd = 10.0
[utstyr]
effekt = 6.0 # → 13,2 kWh/(m²·år)
varmetilskudd = 6.0
[varmtvann]
effekt = 1.9 # → 10,0 kWh/(m²·år)
varmetilskudd = 0.0
[personer]
varmetilskudd = 12.0
[ventilasjon]
luftmengde_driftstid = 10.0 # m³/(h·m²)
luftmengde_utenfor_driftstid = 2.0
sfp = 1.5
temperaturvirkningsgrad = 0.80
frostsikring_virkningsgrad = 0.80
[termisk]
varmekapasitet_normalisert = 72.6
kuldebroer_normalisert = 0.07
[pumper]
effekt_vannbaaren = 0.5
@@ -1,15 +1,13 @@
# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur. # Eksempel: enebolig i tre fra 1948, 1,5 etasje på betong grunnmur.
# Bygningsdata er hentet fra termografirapporten av 10.3.2016 og originale byggetegninger. # Bygningsdataene er typiske for et utett hus fra perioden.
# Kjelleren er uoppvarmet, men varmes indirekte av uisolerte varmerør. # Kjelleren er uoppvarmet, men varmes indirekte av uisolerte varmerør.
[bygning] [bygning]
navn = "Nordhusstykket 5" navn = "Eldre enebolig med kjeller"
kategori = "smaahus" kategori = "smaahus"
byggeaar = 1948 byggeaar = 1948
oppvarmet_bra = 140.0 oppvarmet_bra = 140.0
oppvarmet_volum = 336.0 oppvarmet_volum = 336.0
adresse = "Nordhusstykket 5, 1825 Tomter"
oppdragsgiver = "Ellen Brødholt"
beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 98 m², betong grunnmur" beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 98 m², betong grunnmur"
[klima] [klima]
+45 -2
View File
@@ -14,6 +14,11 @@ const MATERIALER: &str = include_str!("../data/materialer.toml");
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv"); const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml"); const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml"); const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
const NORMERTE_BARNEHAGE: &str = include_str!("../data/normerte/barnehage.toml");
const NORMERTE_KONTORBYGG: &str = include_str!("../data/normerte/kontorbygg.toml");
const NORMERTE_SKOLEBYGG: &str = include_str!("../data/normerte/skolebygg.toml");
const NORMERTE_IDRETTSBYGG: &str = include_str!("../data/normerte/idrettsbygg.toml");
const NORMERTE_KULTURBYGG: &str = include_str!("../data/normerte/kulturbygg.toml");
/// Katalog som kan overstyre de innebygde datafilene. /// Katalog som kan overstyre de innebygde datafilene.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@@ -63,8 +68,13 @@ impl Datakilde {
let (fil, innebygd) = match kategori { let (fil, innebygd) = match kategori {
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS), Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK), Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
Kategori::Barnehage => ("normerte/barnehage.toml", NORMERTE_BARNEHAGE),
Kategori::Kontorbygg => ("normerte/kontorbygg.toml", NORMERTE_KONTORBYGG),
Kategori::Skolebygg => ("normerte/skolebygg.toml", NORMERTE_SKOLEBYGG),
Kategori::Idrettsbygg => ("normerte/idrettsbygg.toml", NORMERTE_IDRETTSBYGG),
Kategori::Kulturbygg => ("normerte/kulturbygg.toml", NORMERTE_KULTURBYGG),
annen => { annen => {
// Andre kategorier krever egen datafil i data-katalogen. // Kategorier uten offentlig tilgjengelige normerte verdier krever egen datafil.
let fil = format!("normerte/{}.toml", annen.nokkel()); let fil = format!("normerte/{}.toml", annen.nokkel());
let p = self let p = self
.katalog .katalog
@@ -257,9 +267,12 @@ pub struct Settpunkt {
pub oppvarming_natt: f64, pub oppvarming_natt: f64,
pub kjoling: f64, pub kjoling: f64,
pub ventilasjon_tilluft: f64, pub ventilasjon_tilluft: f64,
/// Tilluftstemperatur når anlegget har kjølebatteri. Utelatt: lik `ventilasjon_tilluft`.
#[serde(default)]
pub ventilasjon_tilluft_kjoling: Option<f64>,
} }
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, serde::Serialize, Clone, Debug)]
pub struct Driftstider { pub struct Driftstider {
pub oppvarming: Tidsrom, pub oppvarming: Tidsrom,
pub belysning: Tidsrom, pub belysning: Tidsrom,
@@ -267,13 +280,43 @@ pub struct Driftstider {
pub varmtvann: Tidsrom, pub varmtvann: Tidsrom,
pub personer: Tidsrom, pub personer: Tidsrom,
pub ventilasjon: Tidsrom, pub ventilasjon: Tidsrom,
/// Driftsdager per uke, regnet fra mandag (NS 3031 Tabell A.3).
#[serde(default = "sju")] #[serde(default = "sju")]
pub dager_per_uke: u8, pub dager_per_uke: u8,
/// Driftsuker per år (NS 3031 Tabell A.3). Ukene uten drift legges i ferier, se [`FERIEUKER`].
#[serde(default = "femtito")]
pub uker_per_aar: u8,
} }
fn sju() -> u8 { fn sju() -> u8 {
7 7
} }
fn femtito() -> u8 {
52
}
/// Uker uten drift, i den rekkefølgen de tas ut når en kategori har færre enn 52 driftsuker:
/// fellesferien først, deretter jul, vinterferie, påske og høstferie, og så uke for uke
/// utover sommeren og nyttår.
pub const FERIEUKER: [u8; 16] = [28, 29, 30, 31, 27, 32, 52, 8, 14, 40, 26, 33, 1, 25, 34, 9];
impl Driftstider {
/// Om døgn nummer `dag` (0 = 1. januar) er en driftsdag.
///
/// Året regnes å begynne på en mandag. Døgn 364 hører til uke 52.
pub fn driftsdag(&self, dag: usize) -> bool {
if dag % 7 >= self.dager_per_uke.min(7) as usize {
return false;
}
let uke = (dag / 7 + 1).min(52) as u8;
let fri = (52u8.saturating_sub(self.uker_per_aar) as usize).min(FERIEUKER.len());
!FERIEUKER[..fri].contains(&uke)
}
pub fn driftsdager_per_aar(&self) -> usize {
(0..365).filter(|&d| self.driftsdag(d)).count()
}
}
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)] #[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
pub struct Tidsrom { pub struct Tidsrom {
+5 -5
View File
@@ -481,13 +481,13 @@ fn bygg(
let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon); let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon);
aapningsareal += areal; aapningsareal += areal;
let vt = vindustyper.get(&a.vindustype); let vt = vindustyper.get(&a.vindustype);
let u = vt.and_then(|v| v.u_verdi).unwrap_or_else(|| { let u = vt.and_then(|v| v.u_verdi);
if u.is_none() {
merknader.push(format!( merknader.push(format!(
"Åpningen «{}» mangler U-verdi i gbXML; satt til 2,4 W/(m²K).", "Åpningen «{}» mangler U-verdi i gbXML; verdien utledes av byggeåret.",
a.navn a.navn
)); ));
2.4 }
});
let dor = a.typ.to_lowercase().contains("door"); let dor = a.typ.to_lowercase().contains("door");
vinduer.push(Vindu { vinduer.push(Vindu {
navn: vt navn: vt
@@ -687,7 +687,7 @@ mod tester {
// Vinduet. // Vinduet.
assert_eq!(b.vindu.len(), 1); assert_eq!(b.vindu.len(), 1);
assert_eq!(b.vindu[0].areal, 3.0); assert_eq!(b.vindu[0].areal, 3.0);
assert_eq!(b.vindu[0].u_verdi, 1.2); assert_eq!(b.vindu[0].u_verdi, Some(1.2));
assert_eq!(b.vindu[0].g_verdi, Some(0.5)); assert_eq!(b.vindu[0].g_verdi, Some(0.5));
} }
+131 -22
View File
@@ -11,6 +11,7 @@
use crate::inndata::ifcgeom; use crate::inndata::ifcgeom;
use crate::inndata::step::{self, Arg, Entitet, Fil}; use crate::inndata::step::{self, Arg, Entitet, Fil};
use crate::inndata::toml_inn::Importvalg;
use crate::klima::geo; use crate::klima::geo;
use crate::modell::*; use crate::modell::*;
use crate::Result; use crate::Result;
@@ -29,17 +30,26 @@ const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"]; const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
pub fn les(sti: &Path) -> Result<Importrapport> { pub fn les(sti: &Path) -> Result<Importrapport> {
les_med(sti, &Importvalg::default())
}
pub fn les_med(sti: &Path, valg: &Importvalg) -> Result<Importrapport> {
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?; let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
let tekst = String::from_utf8_lossy(&raa); let tekst = String::from_utf8_lossy(&raa);
fra_tekst( fra_tekst_med(
&tekst, &tekst,
sti.file_stem() sti.file_stem()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("Bygning"), .unwrap_or("Bygning"),
valg,
) )
} }
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> { pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
fra_tekst_med(tekst, standardnavn, &Importvalg::default())
}
pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Result<Importrapport> {
let fil = step::les(tekst); let fil = step::les(tekst);
let mut merknader = Vec::new(); let mut merknader = Vec::new();
@@ -88,6 +98,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
let mut rom: Vec<(String, f64)> = Vec::new(); let mut rom: Vec<(String, f64)> = Vec::new();
let mut kjellerrom: Vec<(String, f64)> = Vec::new(); let mut kjellerrom: Vec<(String, f64)> = Vec::new();
let mut mistenkelige: Vec<String> = Vec::new(); let mut mistenkelige: Vec<String> = Vec::new();
let mut uoppvarmede: Vec<String> = Vec::new();
for (r, e) in fil.av_type("IFCSPACE") { for (r, e) in fil.av_type("IFCSPACE") {
let navn = romnavn(e); let navn = romnavn(e);
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0); let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
@@ -111,6 +122,14 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
kjellervolum += romvolum; kjellervolum += romvolum;
} else { } else {
let lavt = navn.to_lowercase(); let lavt = navn.to_lowercase();
if valg
.uoppvarmede_rom
.iter()
.any(|u| lavt.contains(&u.to_lowercase()))
{
uoppvarmede.push(format!("{navn} {areal:.1}"));
continue;
}
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"] if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
.iter() .iter()
.any(|o| lavt.contains(o)) .any(|o| lavt.contains(o))
@@ -136,6 +155,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
mistenkelige.join(", ") mistenkelige.join(", ")
)); ));
} }
if !uoppvarmede.is_empty() {
merknader.push(format!(
"Disse rommene er uoppvarmet etter tilleggsfilen og holdt utenfor oppvarmet BRA: {}.",
uoppvarmede.join(", ")
));
}
// ── plassering og geometri for alle bygningsdeler ─────────────── // ── plassering og geometri for alle bygningsdeler ───────────────
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect(); let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
@@ -220,7 +245,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
let mut kjellerflater: Vec<Flate> = Vec::new(); let mut kjellerflater: Vec<Flate> = Vec::new();
let mut flater: Vec<Flate> = Vec::new(); let mut flater: Vec<Flate> = Vec::new();
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new(); let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
let mut uten_uverdi = 0usize; let mut uten_uverdi: Vec<String> = Vec::new();
let mut uten_geometri = 0usize; let mut uten_geometri = 0usize;
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new(); let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
@@ -262,7 +287,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
(mot, None) (mot, None)
}; };
if d.u_verdi.is_none() { if d.u_verdi.is_none() {
uten_uverdi += 1; uten_uverdi.push(d.navn.clone());
} }
let mal = if i_kjeller { let mal = if i_kjeller {
&mut kjellerflater &mut kjellerflater
@@ -333,14 +358,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
.and_then(|v| asimut_for_vegg.get(v)) .and_then(|v| asimut_for_vegg.get(v))
.copied() .copied()
.unwrap_or(0.0); .unwrap_or(0.0);
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| { // Uten U-verdi i modellen lar vi feltet stå tomt, slik at beregningen bruker en
uten_uverdi += 1; // typisk verdi for byggeåret i stedet for en fast standardverdi.
if dor { let u = u_verdi(&egenskaper, r);
2.0 if u.is_none() {
} else { uten_uverdi.push(navn_av(e, r, if dor { "Dør" } else { "Vindu" }));
2.4
} }
});
vinduer.push(Vindu { vinduer.push(Vindu {
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }), navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
areal, areal,
@@ -398,6 +421,10 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
)); ));
} }
// Tilleggsfiler og scenarioer viser til flatene med navn, så navnene må være entydige.
unike_navn(&mut flater);
unike_navn(&mut kjellerflater);
// ── merknader ─────────────────────────────────────────────────── // ── merknader ───────────────────────────────────────────────────
if bra <= 0.0 { if bra <= 0.0 {
bra = flater bra = flater
@@ -416,9 +443,31 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal." "{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
)); ));
} }
if uten_uverdi > 0 { if !uten_uverdi.is_empty() {
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
// konstruksjon nevnes én gang, med antallet.
let mut antall: Vec<(String, usize)> = Vec::new();
for navn in &uten_uverdi {
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
match antall.iter_mut().find(|(n, _)| *n == kort) {
Some((_, a)) => *a += 1,
None => antall.push((kort, 1)),
}
}
let liste: Vec<String> = antall
.iter()
.map(|(n, a)| {
if *a > 1 {
format!("{n} ×{a}")
} else {
n.clone()
}
})
.collect();
merknader.push(format!( 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." "{} bygningsdeler mangler U-verdi i IFC-filen: {}. Legg inn U-verdier i modellen, eller la enok bruke typiske verdier for byggeåret.",
uten_uverdi.len(),
liste.join(", ")
)); ));
} }
merknader.push( merknader.push(
@@ -571,6 +620,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
} }
}; };
vinduer.sort_by(|a, b| a.navn.cmp(&b.navn));
let bygning = Bygning { let bygning = Bygning {
bygning: BygningInfo { bygning: BygningInfo {
navn, navn,
@@ -668,9 +719,13 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
for f in flater.drain(..) { for f in flater.drain(..) {
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) { match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
Some(g) => { Some(g) => {
// Arealvektet himmelretning, slik at summen peker riktig vei. // Arealvektet himmelretning og helning, slik at summen peker riktig vei og ikke
// avhenger av rekkefølgen elementene kommer i.
let sum = g.areal + f.areal; let sum = g.areal + f.areal;
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum; g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
if let (Some(a), Some(b)) = (g.helning, f.helning) {
g.helning = Some((a * g.areal + b * f.areal) / sum);
}
g.areal = sum; g.areal = sum;
} }
None => samlet.push(f), None => samlet.push(f),
@@ -689,6 +744,36 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
*flater = samlet; *flater = samlet;
} }
/// Gjør flatenavnene entydige. Vegger får himmelretningen etter konstruksjonsnavnet, og andre
/// flater som fortsatt deler navn, nummereres.
fn unike_navn(flater: &mut [Flate]) {
// Fast rekkefølge, slik at samme modell alltid gir samme fil og samme nummerering.
flater.sort_by(|a, b| {
(a.typ as u8, &a.navn)
.cmp(&(b.typ as u8, &b.navn))
.then(a.orientering.total_cmp(&b.orientering))
.then(b.areal.total_cmp(&a.areal))
});
for f in flater.iter_mut().filter(|f| f.typ == FlateType::Vegg) {
f.navn = format!("{}, {}", f.navn, himmelretning(f.orientering));
}
let mut antall: HashMap<String, usize> = HashMap::new();
for f in flater.iter_mut() {
let n = antall.entry(f.navn.clone()).or_insert(0);
*n += 1;
if *n > 1 {
f.navn = format!("{} ({n})", f.navn);
}
}
}
fn himmelretning(grader: f64) -> &'static str {
const RETNINGER: [&str; 8] = [
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
];
RETNINGER[((grader / 45.0).round() as i64).rem_euclid(8) as usize]
}
/// En etasje i modellen, med navn og høyde over byggets referansenivå. /// En etasje i modellen, med navn og høyde over byggets referansenivå.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Etasje { pub struct Etasje {
@@ -1150,7 +1235,10 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]]; 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(); let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
if vannrett > 0.2 { // Et skrått tak har også en vannrett komponent, men skal alltid ha normalen oppover;
// ellers kan undersiden bli hovedflaten og gi helning 180° minus takvinkelen.
let skraa_plate = PLATETYPER.contains(&d.typ.as_str()) && g[2].abs() > 0.1;
if vannrett > 0.2 && !skraa_plate {
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt. // Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
if let Some((x, y, _, _, _)) = d.plass { if let Some((x, y, _, _, _)) = d.plass {
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 { if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
@@ -1686,6 +1774,27 @@ mod tester {
); );
} }
#[test]
fn vegger_faar_himmelretning_i_navnet() {
let r = fra_tekst(&prove(), "prove").unwrap();
let mut navn: Vec<&str> = r.bygning.flate.iter().map(|f| f.navn.as_str()).collect();
navn.sort();
assert_eq!(navn, vec!["Nordvegg, nord", "Sorvegg, sør"]);
}
#[test]
fn uoppvarmede_rom_fra_tilleggsfilen_holdes_utenfor_bra() {
let valg = Importvalg {
uoppvarmede_rom: vec!["STUE".into()],
};
let r = fra_tekst_med(&prove(), "prove", &valg).unwrap();
assert_ne!(r.bygning.bygning.oppvarmet_bra, 100.0);
assert!(r
.merknader
.iter()
.any(|m| m.contains("uoppvarmet etter tilleggsfilen")));
}
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete /// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter. /// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
#[test] #[test]
@@ -1744,21 +1853,21 @@ mod tester {
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() { fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() {
let t = prove().replace( let t = prove().replace(
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);", "#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
"#501= IFCPOSTALADDRESS($,$,$,$,('Hob\\X\\F8lveien 6'),$,'Tomter',$,'1825','Norge');\n\ "#501= IFCPOSTALADDRESS($,$,$,$,('Firmaveien 1'),$,'Firmaby',$,'9999','Norge');\n\
#502= IFCORGANIZATION('976015711','Klingenberg Bygg',$,$,(#501));\n\ #502= IFCORGANIZATION('999999999','Eksempel AS',$,$,(#501));\n\
#503= IFCPERSON($,'Lynghaug','Fredrik',$,$,$,$,$);\n\ #503= IFCPERSON($,'Nordmann','Kari',$,$,$,$,$);\n\
#504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\ #504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\
#500= IFCPOSTALADDRESS($,$,$,$,('Nordhusstykket 5'),$,'Tomter',$,'1825','Norge');\n\ #500= IFCPOSTALADDRESS($,$,$,$,('Eksempelveien 12'),$,'Eksempelby',$,'1234','Norge');\n\
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);", #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
); );
let r = fra_tekst(&t, "prove").unwrap(); let r = fra_tekst(&t, "prove").unwrap();
assert_eq!( assert_eq!(
r.bygning.bygning.adresse.as_deref(), r.bygning.bygning.adresse.as_deref(),
Some("Nordhusstykket 5, 1825 Tomter") Some("Eksempelveien 12, 1234 Eksempelby")
); );
assert_eq!( assert_eq!(
r.bygning.bygning.energiraadgiver.as_deref(), r.bygning.bygning.energiraadgiver.as_deref(),
Some("Fredrik Lynghaug, Klingenberg Bygg") Some("Kari Nordmann, Eksempel AS")
); );
} }
@@ -1772,11 +1881,11 @@ mod tester {
) )
.replace( .replace(
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);", "#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);",
"#3= IFCPROJECT('p',$,'Nordhusstykket 5','Energianalyse',$,'Nordhusstykket 5','Eksisterende',(#40),#2);", "#3= IFCPROJECT('p',$,'Eksempelhuset','Energianalyse',$,'Eksempelhuset','Eksisterende',(#40),#2);",
); );
let r = fra_tekst(&t, "prove").unwrap(); let r = fra_tekst(&t, "prove").unwrap();
assert_eq!(r.bygning.bygning.adresse, None); assert_eq!(r.bygning.bygning.adresse, None);
assert_eq!(r.bygning.bygning.navn, "Nordhusstykket 5"); assert_eq!(r.bygning.bygning.navn, "Eksempelhuset");
} }
#[test] #[test]
+3 -1
View File
@@ -7,4 +7,6 @@ pub mod regneark;
pub mod step; pub mod step;
pub mod toml_inn; pub mod toml_inn;
pub use toml_inn::{les_bygning, slaa_sammen_scenario}; pub use toml_inn::{
differanse, flett_tillegg, les_bygning, les_tillegg, slaa_sammen_scenario, Importvalg, Tillegg,
};
+4 -2
View File
@@ -6,7 +6,7 @@
//! //!
//! ```text //! ```text
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall //! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
//! bygning,Nordhusstykket 5,140,,,,smaahus,1948, //! bygning,Eldre enebolig,140,,,,smaahus,1948,
//! vegg,Yttervegg sør,32,1.0,180,90,ute,, //! vegg,Yttervegg sør,32,1.0,180,90,ute,,
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2 //! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
//! ``` //! ```
@@ -84,6 +84,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
"kontorbygg" | "kontor" => Kategori::Kontorbygg, "kontorbygg" | "kontor" => Kategori::Kontorbygg,
"skolebygg" | "skole" => Kategori::Skolebygg, "skolebygg" | "skole" => Kategori::Skolebygg,
"barnehage" => Kategori::Barnehage, "barnehage" => Kategori::Barnehage,
"idrettsbygg" | "idrett" => Kategori::Idrettsbygg,
"kulturbygg" | "kultur" => Kategori::Kulturbygg,
annen => { annen => {
merknader merknader
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus.")); .push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
@@ -151,7 +153,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
antall: tall(felt(8)).unwrap_or(1.0), antall: tall(felt(8)).unwrap_or(1.0),
orientering: tall(felt(4)).unwrap_or(0.0), orientering: tall(felt(4)).unwrap_or(0.0),
helning: tall(felt(5)).unwrap_or(90.0), helning: tall(felt(5)).unwrap_or(90.0),
u_verdi: u, u_verdi: Some(u),
g_verdi: tall(felt(7)), g_verdi: tall(felt(7)),
karmandel: None, karmandel: None,
solskjerming: None, solskjerming: None,
+4 -3
View File
@@ -3,7 +3,7 @@
//! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen //! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen
//! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet. //! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet.
use std::collections::HashMap; use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Arg { pub enum Arg {
@@ -58,7 +58,8 @@ impl Entitet {
} }
pub struct Fil { pub struct Fil {
pub entiteter: HashMap<u64, Entitet>, /// Sortert på entitetsnummer, slik at gjennomløp alltid kommer i samme rekkefølge.
pub entiteter: BTreeMap<u64, Entitet>,
} }
impl Fil { impl Fil {
@@ -83,7 +84,7 @@ impl Fil {
/// Deler en STEP-fil i entiteter. /// Deler en STEP-fil i entiteter.
pub fn les(tekst: &str) -> Fil { pub fn les(tekst: &str) -> Fil {
let mut entiteter = HashMap::new(); let mut entiteter = BTreeMap::new();
let mut setning = String::new(); let mut setning = String::new();
let mut i_streng = false; let mut i_streng = false;
let mut i_data = false; let mut i_data = false;
+250 -6
View File
@@ -1,8 +1,9 @@
//! Leser `bygning.toml` og fletter inn scenarioendringer. //! Leser `bygning.toml` og fletter inn scenarioendringer og tilleggsfiler.
use crate::modell::{Bygning, Scenario}; use crate::modell::{Bygning, Scenario};
use crate::Result; use crate::Result;
use anyhow::Context; use anyhow::Context;
use serde::Deserialize;
use std::path::Path; use std::path::Path;
use toml::Value; use toml::Value;
@@ -11,19 +12,165 @@ pub fn les_bygning(sti: &Path) -> Result<Bygning> {
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display())) toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
} }
/// Valg for selve importen, fra tabellen `[import]` i en tilleggsfil.
#[derive(Deserialize, Default, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Importvalg {
/// Rom som står uoppvarmet og holdes utenfor oppvarmet BRA. Et rom treffes når navnet
/// inneholder en av tekstene, uten hensyn til store og små bokstaver.
#[serde(default)]
pub uoppvarmede_rom: Vec<String>,
}
/// En tilleggsfil til en importert modell: det modellen ikke beskriver, som drift, varmeanlegg
/// og scenarioer, og rettelser til det den beskriver.
pub struct Tillegg {
pub endringer: Value,
pub import: Importvalg,
}
pub fn les_tillegg(sti: &Path) -> Result<Tillegg> {
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
let mut endringer: Value =
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))?;
let import = match endringer.as_table_mut().and_then(|t| t.remove("import")) {
Some(v) => v
.try_into()
.with_context(|| format!("tolker [import] i {}", sti.display()))?,
None => Importvalg::default(),
};
Ok(Tillegg { endringer, import })
}
/// Fletter en tilleggsfil inn i en importert bygning, etter de samme reglene som scenarioene.
///
/// Returnerer også navnemønstrene med `*` som ikke traff noen oppføring, slik at en tilleggsfil
/// som har gått ut av takt med modellen blir synlig.
pub fn flett_tillegg(b: &Bygning, tillegg: &Value) -> Result<(Bygning, Vec<String>)> {
let mut grunn = Value::try_from(b).context("serialiserer den importerte bygningen")?;
let mut bom = Vec::new();
flett(&mut grunn, tillegg, &mut bom);
let bygning = grunn.try_into().context(
"tilleggsfilen gir en ugyldig bygningsbeskrivelse. Et navn som ikke finnes i modellen, \
legges til som ny oppføring og må da være komplett",
)?;
Ok((bygning, bom))
}
/// Lager en ny bygning der scenarioets endringer er flettet inn. /// Lager en ny bygning der scenarioets endringer er flettet inn.
/// ///
/// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches /// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches
/// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges /// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges
/// til, og `slett = true` fjerner et element. Andre verdier erstattes. /// til, og `slett = true` fjerner et element. Et navn med `*` er et mønster som treffer alle
/// oppføringer det passer på, for eksempel `"Yttervegg*"`. Andre verdier erstattes.
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> { pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?; let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
flett(&mut grunn, &s.endringer); flett(&mut grunn, &s.endringer, &mut Vec::new());
grunn grunn
.try_into() .try_into()
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn)) .with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
} }
/// Forskjellen mellom to bygningsbeskrivelser, som en tilleggsfil.
///
/// Resultatet er det minste som må flettes inn i `fra` for å få `til`: endrede og nye felter,
/// nye elementer i lister, og `slett = true` for elementer som bare finnes i `fra`. Fletting
/// kan ikke fjerne et enkeltfelt, så felter som bare finnes i `fra`, listes som merknader.
pub fn differanse(fra: &Value, til: &Value) -> (Value, Vec<String>) {
let mut merknader = Vec::new();
let v = diff_verdi(fra, til, "", &mut merknader).unwrap_or(Value::Table(Default::default()));
(v, merknader)
}
fn diff_verdi(fra: &Value, til: &Value, sti: &str, merknader: &mut Vec<String>) -> Option<Value> {
if fra == til {
return None;
}
match (fra, til) {
(Value::Table(f), Value::Table(t)) => {
let mut ut = toml::map::Map::new();
for (k, v) in t {
let bane = if sti.is_empty() {
k.clone()
} else {
format!("{sti}.{k}")
};
match f.get(k) {
Some(gammel) => {
if let Some(d) = diff_verdi(gammel, v, &bane, merknader) {
ut.insert(k.clone(), d);
}
}
None => {
ut.insert(k.clone(), v.clone());
}
}
}
for k in f.keys() {
if !t.contains_key(k) {
let bane = if sti.is_empty() {
k.clone()
} else {
format!("{sti}.{k}")
};
merknader.push(format!(
"«{bane}» finnes i modellen, men ikke i bygningsbeskrivelsen. En tilleggsfil kan ikke fjerne et felt."
));
}
}
(!ut.is_empty()).then_some(Value::Table(ut))
}
(Value::Array(f), Value::Array(t)) if t.iter().all(|e| navnenokkel(e).is_some()) => {
let mut ut: Vec<Value> = Vec::new();
for element in t {
let navn = navnenokkel(element).unwrap_or_default();
match f.iter().find(|e| navnenokkel(e).as_deref() == Some(&navn)) {
Some(gammel) => {
let bane = format!("{sti}.{navn}");
if let Some(Value::Table(mut endring)) =
diff_verdi(gammel, element, &bane, merknader)
{
endring.insert("navn".into(), Value::String(navn));
ut.push(Value::Table(endring));
}
}
None => ut.push(element.clone()),
}
}
for gammel in f {
let navn = navnenokkel(gammel).unwrap_or_default();
if !t.iter().any(|e| navnenokkel(e).as_deref() == Some(&navn)) {
let mut slett = toml::map::Map::new();
slett.insert("navn".into(), Value::String(navn));
slett.insert("slett".into(), Value::Boolean(true));
ut.push(Value::Table(slett));
}
}
(!ut.is_empty()).then_some(Value::Array(ut))
}
_ => Some(til.clone()),
}
}
/// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst.
fn passer(moenster: &str, navn: &str) -> bool {
let deler: Vec<&str> = moenster.split('*').collect();
let (forst, sist) = (deler[0], deler[deler.len() - 1]);
if deler.len() == 1 {
return moenster == navn;
}
let Some(mut rest) = navn.strip_prefix(forst) else {
return false;
};
for del in &deler[1..deler.len() - 1] {
match rest.find(del) {
Some(i) => rest = &rest[i + del.len()..],
None => return false,
}
}
rest.ends_with(sist)
}
fn navnenokkel(v: &Value) -> Option<String> { fn navnenokkel(v: &Value) -> Option<String> {
let t = v.as_table()?; let t = v.as_table()?;
t.get("navn") t.get("navn")
@@ -39,7 +186,7 @@ fn skal_slettes(v: &Value) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
fn flett(grunn: &mut Value, over: &Value) { fn flett(grunn: &mut Value, over: &Value, bom: &mut Vec<String>) {
match (grunn, over) { match (grunn, over) {
(Value::Table(g), Value::Table(o)) => { (Value::Table(g), Value::Table(o)) => {
for (k, v) in o { for (k, v) in o {
@@ -47,7 +194,7 @@ fn flett(grunn: &mut Value, over: &Value) {
continue; continue;
} }
match g.get_mut(k) { match g.get_mut(k) {
Some(eksisterende) => flett(eksisterende, v), Some(eksisterende) => flett(eksisterende, v, bom),
None => { None => {
g.insert(k.clone(), v.clone()); g.insert(k.clone(), v.clone());
} }
@@ -57,6 +204,25 @@ fn flett(grunn: &mut Value, over: &Value) {
(Value::Array(g), Value::Array(o)) => { (Value::Array(g), Value::Array(o)) => {
for element in o { for element in o {
match navnenokkel(element) { match navnenokkel(element) {
Some(moenster) if moenster.contains('*') => {
let treffer =
|e: &Value| navnenokkel(e).is_some_and(|n| passer(&moenster, &n));
let antall = g.iter().filter(|e| treffer(e)).count();
if antall == 0 {
bom.push(moenster.clone());
} else if skal_slettes(element) {
g.retain(|e| !treffer(e));
} else {
// Mønsteret skal ikke erstatte navnene det treffer.
let mut endring = element.clone();
if let Some(t) = endring.as_table_mut() {
t.remove("navn");
}
for e in g.iter_mut().filter(|e| treffer(e)) {
flett(e, &endring, bom);
}
}
}
Some(navn) => { Some(navn) => {
let treff = g let treff = g
.iter() .iter()
@@ -65,7 +231,7 @@ fn flett(grunn: &mut Value, over: &Value) {
Some(i) if skal_slettes(element) => { Some(i) if skal_slettes(element) => {
g.remove(i); g.remove(i);
} }
Some(i) => flett(&mut g[i], element), Some(i) => flett(&mut g[i], element, bom),
None if skal_slettes(element) => {} None if skal_slettes(element) => {}
None => g.push(element.clone()), None => g.push(element.clone()),
} }
@@ -133,6 +299,84 @@ u_verdi = 0.8
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]); assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
} }
#[test]
fn moenster_i_navn_treffer_flere_oppforinger() {
let b: Bygning = toml::from_str(GRUNN).unwrap();
let mut grunn = Value::try_from(&b).unwrap();
let mut bom = Vec::new();
let over: Value = toml::from_str(
r#"flate = [{ navn = "*", masseklasse = "lett" }, { navn = "Yttervegg*", u_verdi = 0.3 }, { navn = "Kjeller*", u_verdi = 0.2 }]"#,
)
.unwrap();
flett(&mut grunn, &over, &mut bom);
let ny: Bygning = grunn.try_into().unwrap();
assert_eq!(ny.flate.len(), 2);
assert_eq!(ny.flate[0].navn, "Yttervegg");
assert_eq!(ny.flate[0].u_verdi, Some(0.3));
assert_eq!(ny.flate[1].u_verdi, Some(0.8));
assert!(ny.flate.iter().all(|f| f.masseklasse.is_some()));
assert_eq!(bom, vec!["Kjeller*".to_string()]);
}
#[test]
fn differansen_gjenskaper_den_manuelle_filen() {
let importert: Bygning = toml::from_str(GRUNN).unwrap();
let manuell: Bygning = toml::from_str(
r#"
[bygning]
navn = "Testhus"
kategori = "smaahus"
byggeaar = 1948
oppvarmet_bra = 100.0
[[flate]]
navn = "Yttervegg"
type = "vegg"
areal = 80.0
u_verdi = 0.22
[[flate]]
navn = "Kjellervegg"
type = "vegg"
areal = 30.0
u_verdi = 3.13
[[scenario]]
navn = "Nye vinduer"
endringer.flate = [{ navn = "Yttervegg", u_verdi = 0.18 }]
"#,
)
.unwrap();
let (patch, merknader) = differanse(
&Value::try_from(&importert).unwrap(),
&Value::try_from(&manuell).unwrap(),
);
// Taket finnes bare i importen, og skal slettes av tillegget.
let tekst = toml::to_string(&patch).unwrap();
assert!(tekst.contains("slett"), "{tekst}");
assert!(merknader.is_empty(), "{merknader:?}");
let (gjenskapt, bom) = flett_tillegg(&importert, &patch).unwrap();
assert!(bom.is_empty());
assert_eq!(
Value::try_from(&gjenskapt).unwrap(),
Value::try_from(&manuell).unwrap()
);
}
#[test]
fn moenster_passer() {
assert!(passer("V-2*", "V-2 #3306"));
assert!(passer("*sør", "Yttervegg, sør"));
assert!(passer(
"Kjellermur*uisolert*",
"Kjellermur 30 cm uisolert, øst"
));
assert!(!passer("V-2*", "VD-2 #11162"));
assert!(passer("Tak", "Tak"));
assert!(!passer("Tak", "Taket"));
}
#[test] #[test]
fn scenario_endrer_skalarfelt() { fn scenario_endrer_skalarfelt() {
let b: Bygning = toml::from_str(GRUNN).unwrap(); let b: Bygning = toml::from_str(GRUNN).unwrap();
+197 -24
View File
@@ -31,6 +31,8 @@ enum Kommando {
Beregn(Beregn), Beregn(Beregn),
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml. /// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
Import(Import), Import(Import),
/// Lager en tilleggsfil av forskjellen mellom en modell og en ferdig bygningsbeskrivelse.
Tillegg(TilleggAv),
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted. /// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
Klima(Klima), Klima(Klima),
/// Viser energikarakterskalaen for en bygningskategori. /// Viser energikarakterskalaen for en bygningskategori.
@@ -56,15 +58,36 @@ struct Beregn {
/// Beregner bare hovedalternativet, uten scenarioene i filen. /// Beregner bare hovedalternativet, uten scenarioene i filen.
#[arg(long)] #[arg(long)]
uten_scenarier: bool, uten_scenarier: bool,
/// Tilleggsfil som flettes inn: det modellen ikke beskriver, og rettelser.
#[arg(short, long, value_name = "FIL")]
tillegg: Option<PathBuf>,
} }
#[derive(Args)] #[derive(Args)]
struct Import { struct Import {
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet. /// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
fil: PathBuf, fil: PathBuf,
/// Tilleggsfil som flettes inn i den importerte bygningen: det modellen ikke beskriver
/// (drift, varmeanlegg, scenarioer) og rettelser til det den beskriver.
#[arg(short, long, value_name = "FIL")]
tillegg: Option<PathBuf>,
/// Utdatafil. Standard: samme navn med endelsen .toml. /// Utdatafil. Standard: samme navn med endelsen .toml.
#[arg(short, long, value_name = "FIL")] #[arg(short, long, value_name = "FIL")]
ut: Option<PathBuf>, ut: Option<PathBuf>,
/// Skriver over en utdatafil enok ikke har laget selv.
#[arg(long)]
overskriv: bool,
}
#[derive(Args)]
struct TilleggAv {
/// BIM-modellen: .ifc, .gbxml eller .csv.
modell: PathBuf,
/// Bygningsbeskrivelsen med de manuelle endringene, som tillegget skal gjenskape.
bygning: PathBuf,
/// Utdatafil. Standard: prosjekt.toml ved siden av bygningsbeskrivelsen.
#[arg(short, long, value_name = "FIL")]
ut: Option<PathBuf>,
} }
#[derive(Args)] #[derive(Args)]
@@ -93,19 +116,83 @@ fn main() -> Result<()> {
match cli.kommando { match cli.kommando {
Kommando::Beregn(a) => beregn(a, &kilde), Kommando::Beregn(a) => beregn(a, &kilde),
Kommando::Import(a) => import(a), Kommando::Import(a) => import(a),
Kommando::Tillegg(a) => tillegg(a),
Kommando::Klima(a) => klima(a), Kommando::Klima(a) => klima(a),
Kommando::Skala(a) => skala(a, &kilde), Kommando::Skala(a) => skala(a, &kilde),
} }
} }
/// Leser en bygningsbeskrivelse eller en BIM-modell, med en valgfri tilleggsfil flettet inn.
///
/// En `.toml`-fil er bygningsbeskrivelsen selv; andre filtyper importeres. Returnerer også
/// merknadene fra importen, og om filen var en modell.
fn les_modell(
fil: &Path,
tillegg: Option<&Path>,
) -> Result<(enok::modell::Bygning, Vec<String>, bool)> {
let endelse = fil
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let tillegg = tillegg.map(inndata::les_tillegg).transpose()?;
let valg = tillegg
.as_ref()
.map(|t| t.import.clone())
.unwrap_or_default();
let (bygning, mut merknader, modell) = match endelse.as_str() {
"toml" => (inndata::les_bygning(fil)?, Vec::new(), false),
"gbxml" | "xml" => {
let r = inndata::gbxml::les(fil)?;
(r.bygning, r.merknader, true)
}
"ifc" => {
let r = inndata::ifc::les_med(fil, &valg)?;
(r.bygning, r.merknader, true)
}
"csv" => {
let r = inndata::regneark::les(fil)?;
(r.bygning, r.merknader, true)
}
annen => bail!(
"ukjent filtype «{annen}». enok leser bygning.toml og importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
),
};
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
}
let Some(t) = tillegg else {
return Ok((bygning, merknader, modell));
};
let (bygning, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
for m in bom {
merknader.push(format!(
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
));
}
Ok((bygning, merknader, modell))
}
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> { fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
let bygning = inndata::les_bygning(&a.fil)?; let (bygning, merknader, modell) = les_modell(&a.fil, a.tillegg.as_deref())?;
for m in &merknader {
eprintln!(" · {m}");
}
for m in enok::modell::kontroll::kontroller(&bygning) { for m in enok::modell::kontroll::kontroller(&bygning) {
eprintln!(" ! {}: {}", m.felt, m.melding); eprintln!(" ! {}: {}", m.felt, m.melding);
} }
let ut = let ut =
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn))); a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
std::fs::create_dir_all(&ut)?; std::fs::create_dir_all(&ut)?;
if modell {
// Bygningsbeskrivelsen modellen ga, skrives med, slik at beregningen kan etterprøves.
let sti = ut.join("bygning.toml");
std::fs::write(
&sti,
skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?,
)?;
eprintln!("Bygningsbeskrivelsen fra modellen: {}", sti.display());
}
eprintln!("Henter klimadata …"); eprintln!("Henter klimadata …");
let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?; let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?;
@@ -210,39 +297,125 @@ fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)])
println!(); println!();
} }
const GENERERT: &str = "# Generert av enok";
fn import(a: Import) -> Result<()> { fn import(a: Import) -> Result<()> {
let endelse = a let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
.fil let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
.extension() // En fil som ikke er skrevet av enok, kan være redigert for hånd. Da er det bedre å
.and_then(|e| e.to_str()) // stoppe enn å skrive over arbeidet.
.unwrap_or("") if !a.overskriv && ut.exists() {
.to_lowercase(); let gammel = std::fs::read_to_string(&ut).unwrap_or_default();
let (bygning, merknader) = match endelse.as_str() { if !gammel.starts_with(GENERERT) {
"gbxml" | "xml" => { bail!(
let r = inndata::gbxml::les(&a.fil)?; "{} er ikke laget av enok og kan være redigert for hånd. \n\
(r.bygning, r.merknader) Ta vare på endringene som en tilleggsfil med «enok tillegg {} {} --ut prosjekt.toml», \n\
og importer med «--tillegg prosjekt.toml». Bruk --overskriv for å skrive over.",
ut.display(),
a.fil.display(),
ut.display()
);
} }
"ifc" => {
let r = inndata::ifc::les(&a.fil)?;
(r.bygning, r.merknader)
} }
"csv" => { let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
let r = inndata::regneark::les(&a.fil)?; std::fs::write(&ut, innhold)?;
(r.bygning, r.merknader) println!("Skrev {}", ut.display());
for m in &merknader {
println!("{m}");
} }
annen => bail!( Ok(())
"ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)" }
/// Bygningsbeskrivelsen som TOML, med et hode som sier hvor den kommer fra.
fn skriv_bygning(
bygning: &enok::modell::Bygning,
merknader: &[String],
fil: &Path,
tillegg: Option<&Path>,
) -> Result<String> {
let tekst = toml::to_string_pretty(bygning).context("serialiserer bygningen til TOML")?;
let mut innhold = match tillegg {
Some(t) => format!(
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
# Merknader fra importen:\n",
fil.display(),
t.display()
),
None => format!(
"# Generert av enok fra «{}».\n\
# Merknader fra importen:\n",
fil.display()
), ),
}; };
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml")); for m in merknader {
let tekst = toml::to_string_pretty(&bygning).context("serialiserer bygningen til TOML")?;
let mut innhold =
String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n");
for m in &merknader {
innhold.push_str(&format!("# {m}\n")); innhold.push_str(&format!("# {m}\n"));
} }
innhold.push('\n'); innhold.push('\n');
innhold.push_str(&tekst); innhold.push_str(&tekst);
Ok(innhold)
}
/// Skriver forskjellen mellom modellen og en ferdig bygningsbeskrivelse som en tilleggsfil.
fn tillegg(a: TilleggAv) -> Result<()> {
let (importert, _, modell) = les_modell(&a.modell, None)?;
if !modell {
bail!("{} er ikke en BIM-modell", a.modell.display());
}
let manuell = inndata::les_bygning(&a.bygning)?;
let fra = toml::Value::try_from(&importert).context("serialiserer den importerte bygningen")?;
let til = toml::Value::try_from(&manuell).context("serialiserer bygningsbeskrivelsen")?;
let (patch, detaljer) = inndata::differanse(&fra, &til);
// Felter modellen har og filen ikke, kan et tillegg ikke fjerne. De er som regel
// standardverdier importen fyller ut, så de samles i én merknad i stedet for én hver.
let (ufjernbare, mut merknader): (Vec<String>, Vec<String>) = detaljer
.into_iter()
.partition(|m| m.contains("finnes i modellen"));
if !ufjernbare.is_empty() {
let felter: Vec<&str> = ufjernbare
.iter()
.filter_map(|m| m.split('«').nth(1)?.split('»').next())
.collect();
merknader.push(format!(
"{} felter finnes i modellen, men ikke i bygningsbeskrivelsen, og blir stående: {}{}. En tilleggsfil kan ikke fjerne et felt.",
felter.len(),
felter.iter().take(3).copied().collect::<Vec<_>>().join(", "),
if felter.len() > 3 { "" } else { "" }
));
}
// Kontroller at tillegget faktisk gjenskaper filen det er laget av.
let (gjenskapt, bom) = inndata::flett_tillegg(&importert, &patch)?;
merknader.extend(
bom.into_iter()
.map(|m| format!("Navnemønsteret «{m}» traff ingenting.")),
);
if toml::Value::try_from(&gjenskapt).ok().as_ref() != Some(&til) {
merknader.push(
"Tillegget gjenskaper ikke bygningsbeskrivelsen helt. Gå gjennom filen før den brukes."
.into(),
);
}
let ut =
a.ut.unwrap_or_else(|| a.bygning.with_file_name("prosjekt.toml"));
let mut innhold = format!(
"# Tillegg til «{}», laget av forskjellen mot «{}».\n\
# Alt som ikke står i modellen, ligger her, og flettes inn ved hver import:\n\
# enok beregn \"{}\" --tillegg {}\n",
a.modell.display(),
a.bygning.display(),
a.modell.display(),
ut.file_name()
.and_then(|n| n.to_str())
.unwrap_or("prosjekt.toml")
);
for m in &merknader {
innhold.push_str(&format!("# {m}\n"));
}
innhold.push('\n');
innhold.push_str(&toml::to_string_pretty(&patch).context("serialiserer tillegget")?);
std::fs::write(&ut, innhold)?; std::fs::write(&ut, innhold)?;
println!("Skrev {}", ut.display()); println!("Skrev {}", ut.display());
for m in &merknader { for m in &merknader {
+64 -14
View File
@@ -130,6 +130,8 @@ pub struct LostVentilasjon {
pub sfp: f64, pub sfp: f64,
pub varmebatteri: bool, pub varmebatteri: bool,
pub tilluft_settpunkt: f64, pub tilluft_settpunkt: f64,
/// Kjølebatteri: tilluften holdes på denne temperaturen i driftstiden.
pub tilluft_kjoling: Option<f64>,
pub driftstid: crate::data::Tidsrom, pub driftstid: crate::data::Tidsrom,
} }
@@ -142,7 +144,15 @@ pub struct LostLaster {
pub personer: [f64; 24], pub personer: [f64; 24],
pub varmtvann: [f64; 24], pub varmtvann: [f64; 24],
pub pumper: [f64; 24], pub pumper: [f64; 24],
pub oppvarming_driftstid: crate::data::Tidsrom, /// Driftstider med driftsdager og driftsuker. Profilene over gjelder bare på driftsdager.
pub driftstider: crate::data::Driftstider,
}
impl LostLaster {
/// Om time `t` i året faller på en driftsdag.
pub fn driftsdag(&self, t: usize) -> bool {
self.driftstider.driftsdag(t / 24)
}
} }
const R_SI: f64 = 0.13; const R_SI: f64 = 0.13;
@@ -208,7 +218,7 @@ impl Lost {
)?; )?;
} }
for v in &b.vindu { for v in &b.vindu {
flater.push(los_vindu(v, &mut f)); flater.push(los_vindu(v, periode, &mut f));
} }
if flater.is_empty() { if flater.is_empty() {
bail!("bygningen har ingen flater; legg inn minst én [[flate]]"); bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
@@ -326,7 +336,10 @@ impl Lost {
let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0); let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0);
// Vinduslufting brukes når bygningen ikke har kjøleanlegg. // Vinduslufting brukes når bygningen ikke har kjøleanlegg.
let lufting = if b.lufting.ingen || settpunkt_kjoling.is_some() { let lufting = if b.lufting.ingen
|| settpunkt_kjoling.is_some()
|| ventilasjon.tilluft_kjoling.is_some()
{
None None
} else { } else {
// Uten oppgitt grense brukes den normerte settpunkttemperaturen for kjøling. // Uten oppgitt grense brukes den normerte settpunkttemperaturen for kjøling.
@@ -633,8 +646,25 @@ fn los_flate(
}) })
} }
fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate { fn los_vindu(
v: &Vindu,
periode: Option<&crate::data::Periode>,
logg: &mut Vec<Forutsetning>,
) -> LostFlate {
let areal = v.areal * v.antall; let areal = v.areal * v.antall;
// U-verdi: oppgitt, eller typisk for byggeperioden. Et vindu uten oppgitt U-verdi er
// vanligvis originalvinduet, og da sier byggeåret mer enn en fast standardverdi.
let u = v.u_verdi.unwrap_or_else(|| {
let antatt = periode.map(|p| p.vindu).unwrap_or(2.8);
logg.push(Forutsetning {
felt: format!("vindu.{}.u_verdi", v.navn),
verdi: format!("{antatt:.2} W/(m²K)"),
kilde: periode
.map(|p| format!("typisk for {}", p.navn))
.unwrap_or_else(|| "antatt".into()),
});
antatt
});
let karm = v.karmandel.unwrap_or_else(|| { let karm = v.karmandel.unwrap_or_else(|| {
logg.push(Forutsetning { logg.push(Forutsetning {
felt: format!("vindu.{}.karmandel", v.navn), felt: format!("vindu.{}.karmandel", v.navn),
@@ -647,11 +677,11 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
0.0 0.0
} else { } else {
v.g_verdi.unwrap_or_else(|| { v.g_verdi.unwrap_or_else(|| {
let antatt = if v.u_verdi <= 1.2 { 0.50 } else { 0.75 }; let antatt = if u <= 1.2 { 0.50 } else { 0.75 };
logg.push(Forutsetning { logg.push(Forutsetning {
felt: format!("vindu.{}.g_verdi", v.navn), felt: format!("vindu.{}.g_verdi", v.navn),
verdi: format!("{antatt:.2}"), verdi: format!("{antatt:.2}"),
kilde: if v.u_verdi <= 1.2 { kilde: if u <= 1.2 {
"antatt 3-lags energiglass" "antatt 3-lags energiglass"
} else { } else {
"antatt 2-lags glass" "antatt 2-lags glass"
@@ -675,8 +705,8 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
navn: v.navn.clone(), navn: v.navn.clone(),
typ: FlateType::Vegg, typ: FlateType::Vegg,
areal, areal,
u_verdi: v.u_verdi, u_verdi: u,
u_konstruksjon: v.u_verdi, u_konstruksjon: u,
helning: v.helning, helning: v.helning,
orientering: v.orientering, orientering: v.orientering,
kappa: 0.0, kappa: 0.0,
@@ -724,6 +754,22 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
let varmebatteri = v let varmebatteri = v
.varmebatteri .varmebatteri
.unwrap_or(matches!(v.typ, VentilasjonType::Balansert)); .unwrap_or(matches!(v.typ, VentilasjonType::Balansert));
// Kjølebatteriet holder konstant tilluftstemperatur i driftstiden, slik DiBKs modellbygg
// for energirammene er regnet (Multiconsult 2014, avsnitt 7.3).
let tilluft_kjoling = (v.kjolebatteri == Some(true)).then(|| {
v.tilluft_kjoling.unwrap_or_else(|| {
let t = n
.settpunkt
.ventilasjon_tilluft_kjoling
.unwrap_or(n.settpunkt.ventilasjon_tilluft);
logg.push(Forutsetning {
felt: "ventilasjon.tilluft_kjoling".into(),
verdi: format!("{t:.0} °C"),
kilde: format!("normert tilluftstemperatur, {}", n.kilde),
});
t
})
});
LostVentilasjon { LostVentilasjon {
typ: v.typ, typ: v.typ,
luftmengde, luftmengde,
@@ -732,6 +778,7 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
sfp, sfp,
varmebatteri, varmebatteri,
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft, tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
tilluft_kjoling,
driftstid: n.driftstid.ventilasjon, driftstid: n.driftstid.ventilasjon,
} }
} }
@@ -754,9 +801,11 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
logg.push(Forutsetning { logg.push(Forutsetning {
felt: "belysning".into(), felt: "belysning".into(),
verdi: format!( verdi: format!(
"{:.2} W/m² i {:.0} h/døgn", "{:.2} W/m² i {:.0} h/døgn, {} dager/uke, {} uker/år",
n.belysning.effekt, n.belysning.effekt,
n.driftstid.belysning.timer() n.driftstid.belysning.timer(),
n.driftstid.dager_per_uke,
n.driftstid.uker_per_aar
), ),
kilde: format!("normert, {}", n.kilde), kilde: format!("normert, {}", n.kilde),
}); });
@@ -772,16 +821,17 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
.drift .drift
.personer_varmetilskudd .personer_varmetilskudd
.unwrap_or(n.personer.varmetilskudd); .unwrap_or(n.personer.varmetilskudd);
let driftsdager = n.driftstid.driftsdager_per_aar() as f64;
let tappevann = b.tappevann.energibehov.map(|kwh| { let tappevann = b.tappevann.energibehov.map(|kwh| {
// kWh/(m²·år) → W/m² i driftstiden. // kWh/(m²·år) → W/m² i driftstiden på driftsdager.
kwh * 1000.0 / (365.0 * n.driftstid.varmtvann.timer()) kwh * 1000.0 / (driftsdager * n.driftstid.varmtvann.timer())
}); });
let vv = tappevann.unwrap_or_else(|| { let vv = tappevann.unwrap_or_else(|| {
logg.push(Forutsetning { logg.push(Forutsetning {
felt: "tappevann.energibehov".into(), felt: "tappevann.energibehov".into(),
verdi: format!( verdi: format!(
"{:.1} kWh/(m²·år)", "{:.1} kWh/(m²·år)",
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * 365.0 / 1000.0 n.varmtvann.effekt * n.driftstid.varmtvann.timer() * driftsdager / 1000.0
), ),
kilde: format!("normert, {}", n.kilde), kilde: format!("normert, {}", n.kilde),
}); });
@@ -801,6 +851,6 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
personer: profil(personer, n.driftstid.personer, n.profil_personer), personer: profil(personer, n.driftstid.personer, n.profil_personer),
varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann), varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann),
pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None), pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None),
oppvarming_driftstid: n.driftstid.oppvarming, driftstider: n.driftstid.clone(),
} }
} }
+9 -1
View File
@@ -268,7 +268,9 @@ pub struct Vindu {
pub orientering: f64, pub orientering: f64,
#[serde(default = "nitti")] #[serde(default = "nitti")]
pub helning: f64, pub helning: f64,
pub u_verdi: f64, /// U-verdi inkl. karm, W/(m²K). Utelatt: typisk for byggeåret.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub u_verdi: Option<f64>,
/// Solfaktor for glasset (g-verdi). Utelatt: 0,75 for 2-lags, 0,5 for 3-lags energiglass. /// Solfaktor for glasset (g-verdi). Utelatt: 0,75 for 2-lags, 0,5 for 3-lags energiglass.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub g_verdi: Option<f64>, pub g_verdi: Option<f64>,
@@ -397,6 +399,12 @@ pub struct Ventilasjon {
/// Har anlegget varmebatteri som ettervarmer tilluften? /// Har anlegget varmebatteri som ettervarmer tilluften?
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub varmebatteri: Option<bool>, pub varmebatteri: Option<bool>,
/// Har anlegget kjølebatteri? Tilluften holdes da på `tilluft_kjoling` i driftstiden.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kjolebatteri: Option<bool>,
/// Tilluftstemperatur med kjølebatteri, °C. Utelatt: normert.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tilluft_kjoling: Option<f64>,
} }
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)] #[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
+22 -11
View File
@@ -188,17 +188,20 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
} }
for t in timer { for t in timer {
let klokke = t % 24; let klokke = t % 24;
let driftsdag = l.laster.driftsdag(t);
let t_ute = v.t_ute[t]; let t_ute = v.t_ute[t];
let t_himmel = sol::himmeltemperatur(v, t); let t_himmel = sol::himmeltemperatur(v, t);
// ── laster ────────────────────────────────────────────── // ── laster ──────────────────────────────────────────────
let bel = l.laster.belysning[klokke] * l.bra; // Lastprofilene gjelder driftsdager; på andre dager er bygningen ute av bruk.
let utstyr_el = l.laster.utstyr_effekt[klokke] * l.bra; let skala = if driftsdag { l.bra } else { 0.0 };
let utstyr_varme = l.laster.utstyr_varme[klokke] * l.bra; let bel = l.laster.belysning[klokke] * skala;
let personer = l.laster.personer[klokke] * l.bra; let utstyr_el = l.laster.utstyr_effekt[klokke] * skala;
let vv = l.laster.varmtvann[klokke] * l.bra; let utstyr_varme = l.laster.utstyr_varme[klokke] * skala;
let pumper = l.laster.pumper[klokke] * l.bra; let personer = l.laster.personer[klokke] * skala;
let vifter = ventilasjon::vifteeffekt(l, klokke); let vv = l.laster.varmtvann[klokke] * skala;
let pumper = l.laster.pumper[klokke] * skala;
let vifter = ventilasjon::vifteeffekt(l, t);
// Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten). // Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten).
let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter; let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter;
@@ -211,21 +214,28 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
} }
// ── ventilasjon ───────────────────────────────────────── // ── ventilasjon ─────────────────────────────────────────
let q_vent = ventilasjon::luftmengde(l, klokke); let q_vent = ventilasjon::luftmengde(l, t);
let h_ve = q_vent * RHO_C_LUFT; let h_ve = q_vent * RHO_C_LUFT;
let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige); let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige);
let (t_sup, vent_varme) = let kjol_tilluft = l
.ventilasjon
.tilluft_kjoling
.filter(|&k| ventilasjon::i_drift(l, t) && t_sup_hx > k);
let (t_sup, vent_varme, vent_kjol) =
if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt { if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt {
( (
l.ventilasjon.tilluft_settpunkt, l.ventilasjon.tilluft_settpunkt,
h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx), h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx),
0.0,
) )
} else if let Some(k) = kjol_tilluft {
(k, 0.0, h_ve * (t_sup_hx - k))
} else { } else {
(t_sup_hx, 0.0) (t_sup_hx, 0.0, 0.0)
}; };
// ── settpunkt ─────────────────────────────────────────── // ── settpunkt ───────────────────────────────────────────
let sett_varme = if l.laster.oppvarming_driftstid.aktiv(klokke) { let sett_varme = if driftsdag && l.laster.driftstider.oppvarming.aktiv(klokke) {
l.settpunkt_oppvarming l.settpunkt_oppvarming
} else { } else {
l.settpunkt_natt l.settpunkt_natt
@@ -339,6 +349,7 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
ts.romoppvarming[t] = fi.max(0.0); ts.romoppvarming[t] = fi.max(0.0);
ts.romkjoling[t] = (-fi).max(0.0); ts.romkjoling[t] = (-fi).max(0.0);
ts.ventilasjonsvarme[t] = vent_varme; ts.ventilasjonsvarme[t] = vent_varme;
ts.ventilasjonskjoling[t] = vent_kjol;
ts.varmtvann[t] = vv; ts.varmtvann[t] = vv;
ts.vifter[t] = vifter; ts.vifter[t] = vifter;
ts.pumper[t] = pumper; ts.pumper[t] = pumper;
+23 -7
View File
@@ -33,9 +33,14 @@ pub fn infiltrasjonsluftmengde(l: &Lost) -> f64 {
n_inf * v n_inf * v
} }
/// Ventilasjonsluftmengde for en klokketime, m³/h. /// Om ventilasjonen går med driftsluftmengde i time `t` av året.
pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 { pub fn i_drift(l: &Lost, t: usize) -> bool {
let spesifikk = if l.ventilasjon.driftstid.aktiv(klokketime) { l.laster.driftsdag(t) && l.ventilasjon.driftstid.aktiv(t % 24)
}
/// Ventilasjonsluftmengde i time `t` av året, m³/h.
pub fn luftmengde(l: &Lost, t: usize) -> f64 {
let spesifikk = if i_drift(l, t) {
l.ventilasjon.luftmengde l.ventilasjon.luftmengde
} else { } else {
l.ventilasjon.luftmengde_natt l.ventilasjon.luftmengde_natt
@@ -44,18 +49,29 @@ pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
} }
/// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C. /// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C.
///
/// Gjenvinneren reguleres ned når den ellers ville varmet tilluften over settpunktet, slik
/// en roterende gjenvinner med turtallsregulering eller et omløpsspjeld gjør. Når det er
/// varmere ute enn inne, kjøler gjenvinneren tilluften og går for fullt.
pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 { pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 {
match l.ventilasjon.typ { match l.ventilasjon.typ {
VentilasjonType::Balansert => t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute), VentilasjonType::Balansert => {
let gjenvunnet = t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute);
if gjenvunnet > t_ute {
gjenvunnet.min(l.ventilasjon.tilluft_settpunkt.max(t_ute))
} else {
gjenvunnet
}
}
_ => t_ute, _ => t_ute,
} }
} }
/// Vifteeffekt for en klokketime, W. /// Vifteeffekt i time `t` av året, W.
pub fn vifteeffekt(l: &Lost, klokketime: usize) -> f64 { pub fn vifteeffekt(l: &Lost, t: usize) -> f64 {
if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) { if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) {
return 0.0; return 0.0;
} }
let q = luftmengde(l, klokketime) / 3600.0; // m³/s let q = luftmengde(l, t) / 3600.0; // m³/s
l.ventilasjon.sfp * 1000.0 * q l.ventilasjon.sfp * 1000.0 * q
} }
+1 -1
View File
@@ -149,7 +149,7 @@ pub fn kilde(r: &Resultat) -> String {
l.settpunkt_kjoling.unwrap_or(0.0) l.settpunkt_kjoling.unwrap_or(0.0)
); );
let _ = writeln!(s, " <Driftstider>"); let _ = writeln!(s, " <Driftstider>");
let d = &l.laster.oppvarming_driftstid; let d = &l.laster.driftstider.oppvarming;
let _ = writeln!( let _ = writeln!(
s, s,
" <Ventilasjon>{:.0}</Ventilasjon>", " <Ventilasjon>{:.0}</Ventilasjon>",
+2 -1
View File
@@ -310,7 +310,8 @@ fn baerer_for(t: VarmekildeType, lukket: bool) -> Energibaerer {
/// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står. /// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står.
pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 { pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 {
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra; let driftsandel = l.laster.driftstider.driftsdager_per_aar() as f64 / 365.0;
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra * driftsandel;
netto_w * s.tappevann_tap_andel netto_w * s.tappevann_tap_andel
} }
+143
View File
@@ -0,0 +1,143 @@
//! Kontroll av de normerte inndataene for yrkesbyggkategoriene: driftsdager, faste energiposter
//! og kjølebatteri.
use enok::data::Datakilde;
use enok::kjor;
use enok::modell::{Bygning, Kategori};
fn prøvebygg(kategori: &str, kjolebatteri: bool) -> Bygning {
let s = format!(
r#"
[bygning]
navn = "Prøvebygg"
kategori = "{kategori}"
byggeaar = 2020
oppvarmet_bra = 1200.0
oppvarmet_volum = 3840.0
[klima]
kommune = "Oslo"
[[flate]]
navn = "Vegger"
type = "vegg"
areal = 560.0
u_verdi = 0.18
[[flate]]
navn = "Tak"
type = "tak"
areal = 600.0
u_verdi = 0.13
[[flate]]
navn = "Gulv"
type = "gulv"
areal = 600.0
u_verdi = 0.10
[[vindu]]
navn = "Sør"
areal = 150.0
orientering = 180
u_verdi = 0.8
g_verdi = 0.5
[infiltrasjon]
n50 = 0.6
[ventilasjon]
type = "balansert"
gjenvinning = 0.85
kjolebatteri = {kjolebatteri}
[[oppvarming.kilde]]
type = "direkte_el"
andel_rom = 1.0
andel_tappevann = 1.0
"#
);
toml::from_str(&s).unwrap()
}
fn post(r: &kjor::Resultat, nr: &str) -> f64 {
let p = r.budsjett.poster.iter().find(|p| p.nr == nr).unwrap();
p.netto / r.lost.bra
}
fn naer(faktisk: f64, forventet: f64, hva: &str) {
assert!(
(faktisk - forventet).abs() <= 0.1,
"{hva}: beregnet {faktisk:.2}, forventet {forventet:.2}"
);
}
#[test]
fn driftsdager_per_aar() {
let kilde = Datakilde::default();
for (kategori, dager) in [
(Kategori::Smaahus, 365),
(Kategori::Boligblokk, 365),
(Kategori::Barnehage, 261),
(Kategori::Kontorbygg, 261),
(Kategori::Kulturbygg, 261),
(Kategori::Skolebygg, 220),
(Kategori::Idrettsbygg, 220),
] {
let n = kilde.normerte(kategori).unwrap();
assert_eq!(
n.driftstid.driftsdager_per_aar(),
dager,
"{}",
kategori.nokkel()
);
}
}
/// Fellesferien og helgene er uten drift i skolebygg.
#[test]
fn skolebygg_stengt_i_helg_og_ferie() {
let n = Datakilde::default().normerte(Kategori::Skolebygg).unwrap();
let d = &n.driftstid;
assert!(d.driftsdag(0), "1. januar er mandag i uke 1");
assert!(!d.driftsdag(5), "lørdag");
assert!(!d.driftsdag(6), "søndag");
assert!(!d.driftsdag(7 * 28), "mandag i uke 29");
assert!(d.driftsdag(7 * 34), "mandag i uke 35");
}
/// Belysning, utstyr og varmtvann skal gi energibehovene som følger av tabellverdiene.
#[test]
fn faste_poster_som_tabellverdiene() {
let kilde = Datakilde::default();
for (kategori, belysning, utstyr, varmtvann) in [
("barnehage", 20.9, 5.2, 10.0),
("kontorbygg", 25.1, 34.5, 5.0),
("skolebygg", 22.0, 13.2, 10.0),
("idrettsbygg", 21.1, 2.6, 50.2),
("kulturbygg", 23.0, 2.9, 10.0),
] {
let r = kjor::kjor(&prøvebygg(kategori, false), &kilde).unwrap();
naer(post(&r, "5"), belysning, &format!("{kategori} belysning"));
naer(post(&r, "6"), utstyr, &format!("{kategori} utstyr"));
naer(post(&r, "2"), varmtvann, &format!("{kategori} varmtvann"));
}
}
/// Kjølebatteriet gir ventilasjonskjøling; uten det er posten null.
#[test]
fn kjolebatteri_gir_ventilasjonskjoling() {
let kilde = Datakilde::default();
let uten = kjor::kjor(&prøvebygg("kontorbygg", false), &kilde).unwrap();
let med = kjor::kjor(&prøvebygg("kontorbygg", true), &kilde).unwrap();
assert_eq!(post(&uten, "3b"), 0.0);
assert!(
post(&med, "3b") > 0.5,
"ventilasjonskjøling {:.2}",
post(&med, "3b")
);
// Kjøling skjer bare i driftstiden på driftsdager.
let ts = &med.timeserie;
let lørdag_middag = 5 * 24 + 12 + 24 * 7 * 26;
assert_eq!(ts.ventilasjonskjoling[lørdag_middag], 0.0);
}
+1 -1
View File
@@ -5,7 +5,7 @@ use enok::klima::geo::Posisjon;
use enok::klima::tmy::naermeste_kommune; use enok::klima::tmy::naermeste_kommune;
#[test] #[test]
fn naermeste_klimasted_for_tomter_er_indre_ostfold() { fn naermeste_klimasted_i_indre_ostfold() {
let (kommune, km) = naermeste_kommune(&Posisjon { let (kommune, km) = naermeste_kommune(&Posisjon {
bredde: 59.63, bredde: 59.63,
lengde: 11.01, lengde: 11.01,
+2 -2
View File
@@ -86,13 +86,13 @@ fn regresjonsvakt_for_romoppvarming() {
let r = kjor_referansebygg(); let r = kjor_referansebygg();
naer( naer(
post(&r, "1a"), post(&r, "1a"),
27.2, 27.1,
1.5, 1.5,
"romoppvarming (regresjon, publisert verdi 46,2)", "romoppvarming (regresjon, publisert verdi 46,2)",
); );
naer( naer(
r.budsjett.netto_spesifikk(r.lost.bra), r.budsjett.netto_spesifikk(r.lost.bra),
86.7, 86.6,
2.0, 2.0,
"netto energibehov (regresjon, publisert verdi 106,4)", "netto energibehov (regresjon, publisert verdi 106,4)",
); );