Energiberegning etter NS 3031:2025 med timesmetode og energirapport
enok leser en bygningsbeskrivelse i TOML, henter typisk klimaår for kommunen, simulerer 8760 timer etter NS-EN ISO 52016-1 og skriver energirapport, CSV og JSON. Motor - Femnodemodell for opake bygningsdeler, tonodemodell for vinduer, luftnode og strålingsnode per sone, implisitt løsning per time med egen tett LU-løser. - Varme- og kjølebehov ved eksakt interpolasjon mellom to løsninger, siden systemet er lineært. Vinduslufting for bygninger uten kjøleanlegg. - Solberegning etter ISO 52010-1, kontrollert mot pvlib med avvik under 1 prosent. Beregningsgang - Beregningspunktene A til E: netto, brutto, tilført, levert per energibærer og vektet levert energi, med solceller, klimakorrigering og effektbehov. - Energikarakter etter Enovas skala fra 1.1.2026, med arealkorreksjon. Inndata - TOML med scenarier for tiltak, og import fra IFC, gbXML og regneark. - Normerte verdier, karakterskala, vektingsfaktorer og klimadata i data/, som kan overstyres med --data. Kontroll - Stasjonært varmetap mot U·A·ΔT innenfor 2 prosent, energibalansen innenfor 2 prosent, varmetapsbudsjettet for TEK17-referansebygget identisk med publisert tabell. Kjent avvik i romoppvarming mot Simien 8 er dokumentert i README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
name: rapport
|
||||
|
||||
# Bygger energirapporter for alle bygningene i `bygninger/` og legger dem ved utgivelsen.
|
||||
#
|
||||
# Legg én fil per bygning i `bygninger/`:
|
||||
# * `bygning.toml` — ferdig bygningsbeskrivelse
|
||||
# * `bygning.ifc` — IFC-eksport fra Vectorworks («Export IFC Project»)
|
||||
# * `bygning.gbxml` — gbXML fra BIM-verktøy som støtter det
|
||||
# * `bygning.csv` — regneark fra Vectorworks («Export Worksheet»)
|
||||
#
|
||||
# IFC-, gbXML- og CSV-filer importeres automatisk til TOML dersom det ikke allerede finnes en
|
||||
# TOML-fil med samme navn. Importerte filer må vanligvis suppleres med ventilasjon,
|
||||
# lekkasjetall og varmekilder før tallene er til å stole på; importen skriver merknader om det.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*', 'rapport-*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
rapporter:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Installer Rust og Typst
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
TYPST=0.15.1
|
||||
curl -sSL "https://github.com/typst/typst/releases/download/v${TYPST}/typst-x86_64-unknown-linux-musl.tar.xz" \
|
||||
| tar -xJ --strip-components=1 -C /usr/local/bin typst-x86_64-unknown-linux-musl/typst
|
||||
|
||||
- name: Hurtiglager for klimadata og cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cache/enok
|
||||
target
|
||||
key: enok-rapport-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: enok-rapport-${{ runner.os }}-
|
||||
|
||||
- name: Bygg enok
|
||||
run: cargo build --release --locked
|
||||
|
||||
- name: Importer BIM-filer som ikke har en TOML fra før
|
||||
run: |
|
||||
shopt -s nullglob nocaseglob
|
||||
for f in bygninger/*.ifc bygninger/*.gbxml bygninger/*.xml bygninger/*.csv; do
|
||||
toml="${f%.*}.toml"
|
||||
if [ ! -f "$toml" ]; then
|
||||
echo "Importerer $f"
|
||||
./target/release/enok import "$f" --ut "$toml"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Beregn og lag rapporter
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
mkdir -p dist
|
||||
feil=0
|
||||
for t in bygninger/*.toml; do
|
||||
navn="$(basename "$t" .toml)"
|
||||
echo "::group::$navn"
|
||||
if ./target/release/enok beregn "$t" --ut "ut/$navn"; then
|
||||
cp "ut/$navn/rapport.pdf" "dist/$navn-energirapport.pdf"
|
||||
cp "ut/$navn/resultat.json" "dist/$navn-resultat.json"
|
||||
cp "ut/$navn/maaned.csv" "dist/$navn-maaned.csv"
|
||||
gzip -c "ut/$navn/timeserie.csv" > "dist/$navn-timeserie.csv.gz"
|
||||
else
|
||||
echo "Beregningen av $navn feilet"
|
||||
feil=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
ls -la dist
|
||||
exit $feil
|
||||
|
||||
- name: Last opp som byggeartefakt
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: energirapporter
|
||||
path: dist/
|
||||
|
||||
- name: Legg ved utgivelsen
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
SERVER: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
api="$SERVER/api/v1/repos/$REPO/releases"
|
||||
id=$(curl -sS --fail -H "Authorization: token $TOKEN" "$api/tags/$TAG" | jq -r '.id // empty')
|
||||
if [ -z "$id" ]; then
|
||||
id=$(curl -sS --fail -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Energirapporter generert av enok.\"}" \
|
||||
"$api" | jq -r '.id')
|
||||
fi
|
||||
for f in dist/*; do
|
||||
echo "Laster opp $(basename "$f")"
|
||||
curl -sS --fail -H "Authorization: token $TOKEN" \
|
||||
--form "attachment=@$f" \
|
||||
"$api/$id/assets?name=$(basename "$f")" > /dev/null
|
||||
done
|
||||
@@ -0,0 +1,37 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Installer Rust
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --component rustfmt,clippy
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Hurtiglager for klimadata og cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cache/enok
|
||||
target
|
||||
key: enok-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: enok-${{ runner.os }}-
|
||||
|
||||
- name: Formatering
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
- name: Tester
|
||||
run: cargo test --release
|
||||
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/ut
|
||||
/dist
|
||||
Generated
+1065
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "enok"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
serde_json = "1"
|
||||
csv = "1"
|
||||
quick-xml = "0.37"
|
||||
ureq = "2"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@@ -0,0 +1,233 @@
|
||||
# enok
|
||||
|
||||
Årlig energiberegning og energirapport for bygninger etter **NS 3031:2025**, med
|
||||
timesoppløsning gjennom hele året og energikarakter etter Enovas skala som gjelder fra
|
||||
1. januar 2026.
|
||||
|
||||
Programmet tar en beskrivelse av bygningen — geometri, konstruksjoner, vinduer, ventilasjon,
|
||||
varmekilder og drift — og et typisk klimaår, og gir 8760 timesverdier, et fullt energibudsjett
|
||||
fra netto energibehov til vektet levert energi, en energikarakter og en ferdig rapport i PDF.
|
||||
|
||||
```
|
||||
enok beregn bygninger/nordhusstykket-5.toml
|
||||
```
|
||||
|
||||
```
|
||||
Nordhusstykket 5 (Småhus, 140 m², byggeår 1948)
|
||||
Klimadata: Indre Østfold
|
||||
|
||||
Varmetapstall 3,00 W/(m²K)
|
||||
Netto energibehov 351,2 kWh/(m²·år)
|
||||
Levert energi 327,6 kWh/(m²·år)
|
||||
Vektet levert energi 327,6 kWh/(m²·år)
|
||||
Energikarakter F
|
||||
|
||||
Tiltak:
|
||||
Isolerte varmerør og kjellervegger 310,9 kWh/(m²·år) F
|
||||
Nye vinduer 304,9 kWh/(m²·år) E
|
||||
Etterisolert klimaskjerm 205,6 kWh/(m²·år) D
|
||||
Væske-vann-varmepumpe 162,3 kWh/(m²·år) C
|
||||
Full pakke 67,9 kWh/(m²·år) A
|
||||
```
|
||||
|
||||
## Hva programmet gjør
|
||||
|
||||
**Beregningsmetode.** Dynamisk timesberegning etter NS-EN ISO 52016-1, slik NS 3031:2025
|
||||
krever. Hver opake bygningsdel modelleres med fem noder der varmekapasiteten fordeles etter
|
||||
masseplassering, vinduer med to noder, og sonen har en luftnode og en strålingsnode som
|
||||
fordeler langbølget utveksling mellom innvendige overflater. Systemet løses implisitt for hver
|
||||
time. Varme- og kjølebehovet bestemmes ved å løse uten tilført effekt og med en prøveeffekt;
|
||||
siden systemet er lineært treffer interpolasjonen settpunktet nøyaktig.
|
||||
|
||||
**Beregningspunkter.** Budsjettet følger NS 3031:2025 fra A til E: netto energibehov,
|
||||
brutto etter distribusjons- og akkumuleringstap, tilført energi etter systemvirkningsgrad og
|
||||
årsvarmefaktor for hver varmekilde, levert energi per energibærer med fradrag for
|
||||
egenprodusert solstrøm, og til slutt vektet levert energi.
|
||||
|
||||
**Energikarakter.** Vektet levert energi per kvadratmeter oppvarmet BRA slås opp i Enovas
|
||||
skala, med arealkorreksjonen som gjelder for småhus og boligblokk. Vektingsfaktorene er
|
||||
elektrisitet 1,0, fjernvarme og biobrensel 0,45, eksportert energi 0. Vedovn regnes å dekke
|
||||
inntil 10 prosent av romoppvarmingen.
|
||||
|
||||
**Klimadata.** Timesverdier for alle norske kommuner fra datasettet som ble laget til
|
||||
NS 3031:2025 (CERRA 1991–2020, CC-BY-4.0). Beregnes bygningen i lokalt klima, korrigeres de
|
||||
klimaavhengige energipostene til referanseklima Oslo med klimakorrigeringsfaktorene fra
|
||||
standardens tillegg, slik energimerkeforskriften krever. Arkivet lastes ned én gang og
|
||||
hurtiglagres.
|
||||
|
||||
**Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell.
|
||||
|
||||
## Installasjon
|
||||
|
||||
```sh
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen-pdf`; da skrives
|
||||
`rapport.typ` som du kan kompilere senere.
|
||||
|
||||
## Bruk
|
||||
|
||||
| Kommando | Hva den gjør |
|
||||
| --- | --- |
|
||||
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
|
||||
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
||||
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
||||
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
||||
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
||||
|
||||
Nyttige valg til `beregn`: `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
|
||||
`--uten-scenarier`, `--ut KATALOG`.
|
||||
|
||||
### Utdata
|
||||
|
||||
| Fil | Innhold |
|
||||
| --- | --- |
|
||||
| `rapport.pdf` | Energirapport med karakter, varmetapsbudsjett, energibudsjett og figurer |
|
||||
| `rapport.typ`, `stil.typ` | Typst-kilde, slik at rapporten kan tilpasses |
|
||||
| `resultat.json` | Hele resultatet, inkludert forutsetninger og månedsverdier |
|
||||
| `timeserie.csv` | 8760 rader med temperaturer, laster, sol og energistrømmer |
|
||||
| `maaned.csv` | Månedssummer |
|
||||
| `enova-2014.xml` | XML for energimerkesystemet, med `--enova-xml` |
|
||||
|
||||
## Bygningsbeskrivelsen
|
||||
|
||||
En bygning beskrives i TOML. Alt som utelates fylles med normerte verdier for
|
||||
bygningskategorien, eller med typiske verdier for byggeåret, og rapporten viser hva som er
|
||||
antatt og hvor det kommer fra.
|
||||
|
||||
```toml
|
||||
[bygning]
|
||||
navn = "Nordhusstykket 5"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 1948
|
||||
oppvarmet_bra = 140.0
|
||||
|
||||
[klima]
|
||||
kommune = "Indre Østfold"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg sør"
|
||||
type = "vegg"
|
||||
areal = 26.0
|
||||
orientering = 180 # grader fra nord med klokka
|
||||
u_verdi = 1.0 # eller `lag = [{ materiale = "tre", tykkelse = 0.02 }, …]`
|
||||
masseklasse = "lett"
|
||||
mot = "ute" # ute | grunn | uoppvarmet
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer sør"
|
||||
areal = 6.0
|
||||
orientering = 180
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 8.0
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig" # naturlig | avtrekk | balansert
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, …
|
||||
andel_rom = 0.75
|
||||
andel_tappevann = 1.0
|
||||
|
||||
[[scenario]]
|
||||
navn = "Nye vinduer"
|
||||
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
|
||||
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
|
||||
|
||||
Ferdige eksempler ligger i `eksempler/`.
|
||||
|
||||
## Import fra Vectorworks og andre BIM-verktøy
|
||||
|
||||
| Format | Slik henter du det | Hva som kommer med |
|
||||
| --- | --- | --- |
|
||||
| IFC | *Fil → Eksporter → Export IFC Project* | Flater, areal, U-verdier fra egenskapssett, himmelretning fra plassering, vinduer og dører koblet til vegg |
|
||||
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
|
||||
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
|
||||
|
||||
Himmelretningene i IFC utledes av elementenes plassering: veggens lokale akse gir planet, og
|
||||
ytre normal velges som den som peker bort fra bygningens tyngdepunkt, korrigert for prosjektets
|
||||
sanne nord. Kontroller dem mot tegningen etter import.
|
||||
|
||||
Ingen av formatene beskriver ventilasjon, lekkasjetall eller varmekilder. Importen skriver
|
||||
merknader øverst i TOML-filen om hva som må fylles ut.
|
||||
|
||||
Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet:
|
||||
|
||||
```csv
|
||||
type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||
bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
||||
vegg,Yttervegg sør,26,1.0,180,90,ute,,
|
||||
gulv,Mot kjeller,98,0.8,,180,uoppvarmet,,
|
||||
vindu,Stue sør,3.0,2.8,180,90,,0.76,2
|
||||
```
|
||||
|
||||
## Rapporter fra byggeserveren
|
||||
|
||||
`.gitea/workflows/rapport.yml` bygger rapporter for alle bygningene i `bygninger/` og legger
|
||||
PDF, JSON og CSV ved utgivelsen når du setter en tagg som begynner med `v` eller `rapport-`.
|
||||
IFC-, gbXML- og CSV-filer importeres automatisk til TOML dersom det ikke allerede finnes en
|
||||
TOML-fil med samme navn. Se `bygninger/LES-MEG.md`.
|
||||
|
||||
## Kontroll av beregningen
|
||||
|
||||
Motoren er kontrollert mot uavhengige referanser:
|
||||
|
||||
| Kontroll | Resultat |
|
||||
| --- | --- |
|
||||
| Solinnstråling på skrå og orienterte flater, mot pvlib (Hay–Davies) | avvik under 1 % for alle himmelretninger i Oslo-klima |
|
||||
| Stasjonært varmetap mot U·A·ΔT | innenfor 2 % |
|
||||
| Årlig energibalanse | går opp innenfor 2 % |
|
||||
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
|
||||
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
|
||||
| Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) |
|
||||
|
||||
### Kjent avvik
|
||||
|
||||
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
|
||||
beregner 24,7 og 84,1 for den samme bygningen.
|
||||
|
||||
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
|
||||
Tillegg A er ikke offentlig tilgjengelig, så de normerte solfaktorene, skjermingsforutsetningene
|
||||
og timesprofilene Simien bruker, lar seg ikke etterprøve her. `tests/referansebygg.rs` låser
|
||||
dagens verdier slik at endringer i motoren blir synlige, og oppdateres når standardens
|
||||
forutsetninger er tilgjengelige.
|
||||
|
||||
Bruk derfor tallene til å sammenligne tiltak mot hverandre og til å se hvor varmen forsvinner.
|
||||
Til en energiattest som skal leveres til Enova må resultatet kontrolleres mot et validert
|
||||
beregningsprogram.
|
||||
|
||||
## Datagrunnlag
|
||||
|
||||
Alle normerte verdier ligger i `data/` og kan overstyres med `--data KATALOG`.
|
||||
|
||||
| Fil | Innhold og kilde |
|
||||
| --- | --- |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 |
|
||||
| `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode |
|
||||
| `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 |
|
||||
|
||||
Klimadata: «TMY files for Norway based on CERRA data from 1991-2020», doi
|
||||
10.5281/zenodo.14779899, CC-BY-4.0.
|
||||
|
||||
## Regelverk
|
||||
|
||||
- Forskrift om energimerking av bygninger og energivurdering av varme- og klimaanlegg,
|
||||
§§ 10a og 10b, med NVEs veileder
|
||||
- NS 3031:2025 *Bygningers energiytelse — Beregning av energi- og effektbehov*
|
||||
- NS-EN ISO 52016-1:2017 og NS-EN ISO 52010-1:2017
|
||||
|
||||
## Lisens
|
||||
|
||||
MIT.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Bygninger
|
||||
|
||||
Legg én fil per bygning her. Byggeserveren regner ut energirapporten og legger PDF, JSON og
|
||||
CSV ved utgivelsen når du setter en tagg som begynner med `v` eller `rapport-`.
|
||||
|
||||
| Filtype | Kommer fra |
|
||||
| -------- | --------------------------------------------------- |
|
||||
| `.toml` | Bygningsbeskrivelse skrevet for hånd, eller importert |
|
||||
| `.ifc` | Vectorworks: *Fil → Eksporter → Export IFC Project* |
|
||||
| `.gbxml` | BIM-verktøy som eksporterer gbXML |
|
||||
| `.csv` | Vectorworks: *Fil → Eksporter → Export Worksheet* |
|
||||
|
||||
IFC-, gbXML- og CSV-filer blir importert til TOML automatisk dersom det ikke allerede finnes
|
||||
en TOML-fil med samme navn. Importen får med geometri, U-verdier og himmelretninger, men ikke
|
||||
ventilasjon, lekkasjetall eller varmekilder. Åpne den importerte TOML-filen, fyll ut det som
|
||||
mangler, og sjekk den inn — da brukes den i stedet for importen neste gang.
|
||||
|
||||
Eksempler på ferdige bygningsbeskrivelser ligger i `../eksempler/`.
|
||||
@@ -0,0 +1,361 @@
|
||||
klimasted,fylke,universitet,sykehus,sykehjem,smaahus,skolebygg,kulturbygg,kontorbygg,industribygg,idrettsbygg,hotell,forretningsbygg,boligblokk,barnehage
|
||||
Bø i Vesterålen,Nordland,1.3085,1.2240,1.2382,1.4101,1.3798,1.3408,1.3428,1.3491,1.3702,1.2955,1.0831,1.5172,1.2622
|
||||
Herøy (Nordland),Nordland,1.0411,1.0432,1.0744,1.1316,1.0913,1.1061,1.0757,1.1018,1.0542,1.0868,0.8505,1.1992,1.0979
|
||||
Alstahaug,Nordland,1.1003,1.0707,1.0986,1.1797,1.1346,1.1490,1.1277,1.1526,1.1084,1.1219,0.9269,1.2490,1.1270
|
||||
Alta,Finnmark,1.5707,1.4519,1.4203,1.6911,1.6250,1.5307,1.5650,1.5614,1.7449,1.4958,1.5316,1.8889,1.4114
|
||||
Alvdal,Innlandet,1.4293,1.3360,1.3171,1.5114,1.4639,1.3948,1.4019,1.4333,1.5734,1.3756,1.4286,1.6092,1.3275
|
||||
Alver,Vestland,0.9280,0.9627,1.0000,0.9544,0.9663,0.9981,0.9598,0.9807,0.8736,0.9850,0.7409,0.9617,1.0163
|
||||
Andøy,Nordland,1.0771,1.0393,1.0895,1.1975,1.1611,1.1583,1.1158,1.1684,1.1445,1.1169,0.7774,1.2529,1.1434
|
||||
Aremark,Østfold,1.0566,1.0472,1.0546,1.0532,1.0385,1.0670,1.0591,1.0702,1.0293,1.0634,0.9867,1.0613,1.0559
|
||||
Arendal,Agder,0.9537,0.9862,0.9939,0.9443,0.9351,0.9814,0.9645,0.9719,0.8984,0.9850,0.8870,0.9387,0.9953
|
||||
Asker,Akershus,1.1414,1.1179,1.1123,1.1544,1.1370,1.1378,1.1348,1.1368,1.1490,1.1386,1.1395,1.1954,1.1061
|
||||
Askvoll,Vestland,0.9332,0.9587,0.9985,0.9722,0.9687,1.0037,0.9716,0.9895,0.8691,0.9850,0.7076,0.9770,1.0233
|
||||
Askøy,Vestland,0.8740,0.9116,0.9560,0.9190,0.9062,0.9479,0.9031,0.9368,0.8262,0.9399,0.6877,0.9195,0.9848
|
||||
Aukra,Møre og Romsdal,0.9229,0.9587,1.0000,0.9797,0.9784,1.0056,0.9645,0.9877,0.8894,0.9833,0.6877,0.9923,1.0245
|
||||
Aure,Møre og Romsdal,1.0077,1.0157,1.0395,1.0608,1.0409,1.0540,1.0378,1.0439,0.9955,1.0401,0.8538,1.0920,1.0524
|
||||
Aurland,Vestland,0.8792,0.8978,0.9044,0.8684,0.8654,0.8883,0.8842,0.8947,0.8533,0.8965,0.8538,0.8352,0.9219
|
||||
Aurskog-Høland,Akershus,1.1851,1.1532,1.1442,1.2076,1.1827,1.1732,1.1773,1.1825,1.1964,1.1686,1.1794,1.2567,1.1434
|
||||
Austevoll,Vestland,0.7609,0.8232,0.8816,0.8203,0.8077,0.8566,0.7943,0.8456,0.7088,0.8464,0.5249,0.7854,0.9266
|
||||
Austrheim,Vestland,0.9049,0.9332,0.9833,0.9443,0.9567,0.9888,0.9433,0.9737,0.8533,0.9683,0.6545,0.9540,1.0152
|
||||
Averøy,Møre og Romsdal,0.9512,0.9686,1.0030,1.0228,0.9952,1.0149,0.9811,1.0088,0.9323,1.0000,0.7542,1.0536,1.0326
|
||||
Balsfjord,Troms,1.4344,1.3261,1.3141,1.5519,1.4856,1.4115,1.4326,1.4351,1.5688,1.3856,1.3787,1.7088,1.3193
|
||||
Bamble,Telemark,0.9254,0.9430,0.9590,0.9316,0.9014,0.9441,0.9291,0.9456,0.8962,0.9533,0.8605,0.9310,0.9685
|
||||
Bardu,Troms,1.4961,1.3870,1.3581,1.6101,1.5361,1.4562,1.4941,1.4825,1.6366,1.4290,1.4684,1.7816,1.3508
|
||||
Beiarn,Nordland,1.1748,1.1277,1.1366,1.2810,1.1971,1.1825,1.1891,1.2000,1.2393,1.1720,1.0698,1.3831,1.1527
|
||||
Bergen,Vestland,0.9332,0.9745,1.0091,0.9443,0.9687,1.0037,0.9669,0.9789,0.8600,0.9900,0.7409,0.9502,1.0163
|
||||
Berlevåg,Finnmark,1.5141,1.4185,1.3976,1.6405,1.5889,1.5084,1.5177,1.5421,1.6907,1.4758,1.4618,1.7854,1.4021
|
||||
Bindal,Nordland,1.1517,1.1159,1.1305,1.2051,1.1707,1.1806,1.1702,1.1807,1.1738,1.1536,1.0199,1.2720,1.1422
|
||||
Birkenes,Agder,0.9409,0.9705,0.9833,0.9342,0.9231,0.9665,0.9433,0.9614,0.8984,0.9783,0.8771,0.9042,0.9918
|
||||
Bjerkreim,Rogaland,0.8920,0.9332,0.9712,0.9241,0.9327,0.9646,0.9149,0.9561,0.8691,0.9583,0.7209,0.9195,0.9965
|
||||
Bjørnafjorden,Vestland,0.8509,0.8939,0.9393,0.8987,0.8894,0.9255,0.8794,0.9105,0.8036,0.9165,0.6545,0.8966,0.9685
|
||||
Bodø,Nordland,1.1799,1.1336,1.1563,1.2759,1.2332,1.2253,1.2175,1.2228,1.2122,1.1886,0.9601,1.3678,1.1772
|
||||
Bokn,Rogaland,0.8149,0.8703,0.9150,0.8354,0.8558,0.8901,0.8440,0.8754,0.7585,0.8815,0.6213,0.8276,0.9441
|
||||
Bremanger,Vestland,0.9846,1.0157,1.0440,1.0127,1.0312,1.0540,1.0260,1.0404,0.9503,1.0367,0.7874,1.0153,1.0594
|
||||
Brønnøy,Nordland,1.1028,1.0707,1.0956,1.1797,1.1322,1.1453,1.1277,1.1474,1.1287,1.1185,0.9336,1.2529,1.1235
|
||||
Bygland,Agder,0.9409,0.9528,0.9772,0.9899,0.9567,0.9814,0.9504,0.9912,0.9594,0.9850,0.8339,0.9579,1.0163
|
||||
Bykle,Agder,1.3522,1.2652,1.2701,1.4937,1.4351,1.3538,1.3333,1.4070,1.5621,1.3356,1.2658,1.5632,1.3147
|
||||
Bærum,Akershus,0.9820,0.9804,0.9879,0.9924,0.9832,0.9907,0.9835,0.9930,0.9910,0.9900,0.9701,0.9885,0.9965
|
||||
Bø i Vesterålen,Nordland,1.3085,1.2240,1.2382,1.4101,1.3798,1.3408,1.3428,1.3491,1.3702,1.2955,1.0831,1.5172,1.2622
|
||||
Bømlo,Vestland,0.8509,0.8939,0.9484,0.8962,0.8918,0.9404,0.8865,0.9298,0.7856,0.9249,0.6080,0.8851,0.9860
|
||||
Båtsfjord,Finnmark,1.4756,1.3792,1.3596,1.6456,1.5385,1.4600,1.4681,1.5105,1.6953,1.4407,1.4618,1.8084,1.3776
|
||||
Dovre,Innlandet,1.2828,1.2063,1.2049,1.3747,1.3413,1.2737,1.2624,1.3088,1.4447,1.2588,1.2425,1.4521,1.2389
|
||||
Drammen,Buskerud,1.0746,1.0727,1.0592,1.0532,1.0577,1.0633,1.0662,1.0614,1.0700,1.0651,1.1296,1.0805,1.0420
|
||||
Drangedal,Telemark,1.1183,1.1159,1.1002,1.1089,1.0938,1.1061,1.1135,1.1088,1.1151,1.1152,1.1429,1.1418,1.0828
|
||||
Dyrøy,Troms,1.3548,1.2711,1.2656,1.4608,1.4014,1.3520,1.3664,1.3684,1.4424,1.3222,1.2392,1.5824,1.2751
|
||||
Dønna,Nordland,1.0746,1.0570,1.0895,1.1620,1.1274,1.1341,1.1040,1.1351,1.0948,1.1085,0.8738,1.2337,1.1200
|
||||
Eidfjord,Vestland,0.9460,0.9587,0.9697,0.9468,0.9375,0.9665,0.9551,0.9649,0.9142,0.9649,0.9003,0.9310,0.9790
|
||||
Eidskog,Innlandet,1.1928,1.1572,1.1457,1.2228,1.1899,1.1844,1.1868,1.1895,1.2099,1.1753,1.1993,1.2912,1.1457
|
||||
Eidsvoll,Akershus,1.2416,1.1984,1.1791,1.2759,1.2428,1.2123,1.2270,1.2263,1.3047,1.2120,1.2924,1.3525,1.1713
|
||||
Eigersund,Rogaland,0.8278,0.8900,0.9317,0.8684,0.8702,0.9069,0.8558,0.8930,0.7901,0.9082,0.6445,0.8659,0.9580
|
||||
Elverum,Innlandet,1.4139,1.3674,1.3187,1.4228,1.4207,1.3631,1.3877,1.3737,1.4831,1.3573,1.5150,1.5326,1.2762
|
||||
Enebakk,Akershus,1.1542,1.1218,1.1123,1.1772,1.1418,1.1378,1.1418,1.1544,1.1783,1.1369,1.1694,1.2069,1.1166
|
||||
Engerdal,Innlandet,1.4216,1.3399,1.3187,1.5342,1.4712,1.3985,1.4019,1.4439,1.5847,1.3856,1.3987,1.6322,1.3357
|
||||
Etne,Vestland,0.9743,1.0138,1.0319,0.9671,1.0048,1.0261,0.9929,1.0105,0.9233,1.0234,0.8173,0.9502,1.0373
|
||||
Etnedal,Innlandet,1.3882,1.3222,1.2959,1.4430,1.4038,1.3538,1.3688,1.3772,1.4695,1.3456,1.3953,1.5479,1.2832
|
||||
Evenes,Nordland,1.2699,1.1847,1.1882,1.4000,1.3005,1.2682,1.2766,1.2947,1.3702,1.2487,1.2027,1.5326,1.2121
|
||||
Evje og Hornnes,Agder,1.0000,1.0098,1.0212,1.0329,1.0000,1.0261,1.0095,1.0298,0.9842,1.0284,0.9336,1.0345,1.0385
|
||||
Farsund,Agder,0.7249,0.7898,0.8422,0.7747,0.7476,0.8063,0.7518,0.8000,0.6727,0.8080,0.5382,0.7356,0.8869
|
||||
Fauske,Nordland,1.2853,1.2083,1.2109,1.3722,1.3197,1.2905,1.3002,1.2982,1.3386,1.2588,1.1728,1.4828,1.2238
|
||||
Fedje,Vestland,0.7584,0.8212,0.8892,0.8253,0.8173,0.8696,0.7991,0.8561,0.6998,0.8514,0.4751,0.8276,0.9394
|
||||
Fitjar,Vestland,0.7892,0.8468,0.8983,0.8456,0.8341,0.8808,0.8180,0.8719,0.7449,0.8748,0.5847,0.8123,0.9441
|
||||
Fjaler,Vestland,0.9666,0.9804,1.0137,1.0101,1.0000,1.0242,0.9953,1.0105,0.9210,1.0083,0.7807,1.0421,1.0350
|
||||
Fjord,Møre og Romsdal,0.9614,0.9705,0.9909,0.9772,0.9784,0.9963,0.9811,0.9860,0.9345,0.9850,0.8472,0.9962,1.0082
|
||||
Flakstad,Nordland,1.1722,1.1238,1.1517,1.2861,1.2380,1.2253,1.2057,1.2316,1.2212,1.1920,0.9302,1.3755,1.1853
|
||||
Flatanger,Trøndelag,1.0874,1.0766,1.1047,1.1570,1.1394,1.1415,1.1135,1.1386,1.0971,1.1185,0.8937,1.2107,1.1212
|
||||
Flekkefjord,Agder,0.8740,0.9214,0.9499,0.8810,0.8990,0.9292,0.8936,0.9175,0.8262,0.9299,0.7442,0.8621,0.9627
|
||||
Flesberg,Buskerud,1.1722,1.1532,1.1351,1.1899,1.1611,1.1601,1.1584,1.1684,1.1919,1.1636,1.2060,1.2222,1.1329
|
||||
Flå,Buskerud,1.0874,1.0668,1.0592,1.0962,1.0865,1.0708,1.0780,1.0807,1.1084,1.0735,1.1163,1.1111,1.0618
|
||||
Folldal,Innlandet,1.4781,1.3831,1.3596,1.6076,1.5457,1.4432,1.4492,1.4982,1.6953,1.4307,1.4751,1.7203,1.3776
|
||||
Fredrikstad,Østfold,0.9846,0.9961,1.0046,0.9797,0.9663,0.9944,0.9882,0.9965,0.9526,0.9983,0.9435,0.9770,1.0012
|
||||
Frogn,Akershus,1.0746,1.0727,1.0622,1.0633,1.0529,1.0633,1.0662,1.0667,1.0767,1.0668,1.1030,1.0920,1.0490
|
||||
Froland,Agder,0.9974,1.0216,1.0258,1.0051,0.9952,1.0223,1.0071,1.0158,0.9707,1.0250,0.9369,1.0000,1.0280
|
||||
Frosta,Trøndelag,1.0848,1.0609,1.0850,1.1392,1.1226,1.1248,1.1040,1.1228,1.0993,1.1085,0.9269,1.1954,1.1061
|
||||
Frøya,Trøndelag,0.8406,0.8802,0.9408,0.9190,0.9087,0.9441,0.8818,0.9351,0.8081,0.9215,0.5449,0.9234,0.9918
|
||||
Fyresdal,Telemark,1.0360,1.0255,1.0410,1.0987,1.0649,1.0615,1.0331,1.0772,1.0903,1.0584,0.9535,1.0881,1.0804
|
||||
Færder,Vestfold,1.0026,1.0098,1.0121,0.9899,0.9880,1.0056,1.0024,1.0088,0.9865,1.0100,0.9801,0.9885,1.0117
|
||||
Gamvik,Finnmark,1.3702,1.2849,1.2914,1.5266,1.4423,1.3892,1.3806,1.4246,1.5418,1.3623,1.2558,1.6590,1.3182
|
||||
Gausdal,Innlandet,1.4036,1.3458,1.3035,1.4304,1.3990,1.3538,1.3783,1.3737,1.4763,1.3506,1.4917,1.5364,1.2762
|
||||
Gildeskål,Nordland,1.0977,1.0648,1.0941,1.1848,1.1418,1.1453,1.1300,1.1439,1.1242,1.1152,0.8970,1.2644,1.1235
|
||||
Giske,Møre og Romsdal,0.8766,0.9116,0.9651,0.9342,0.9399,0.9702,0.9173,0.9544,0.8352,0.9482,0.6113,0.9464,1.0035
|
||||
Gjemnes,Møre og Romsdal,1.1311,1.1257,1.1381,1.1722,1.1779,1.1769,1.1631,1.1632,1.1242,1.1553,0.9601,1.2261,1.1387
|
||||
Gjerdrum,Akershus,1.2468,1.2024,1.1836,1.2684,1.2308,1.2235,1.2340,1.2404,1.2731,1.2187,1.2492,1.3257,1.1818
|
||||
Gjerstad,Agder,1.0591,1.0688,1.0607,1.0582,1.0433,1.0670,1.0638,1.0649,1.0339,1.0735,1.0332,1.0651,1.0559
|
||||
Gjesdal,Rogaland,0.9023,0.9391,0.9803,0.9418,0.9447,0.9702,0.9291,0.9667,0.8713,0.9649,0.6977,0.9310,1.0058
|
||||
Gjøvik,Innlandet,1.2494,1.2279,1.1912,1.2456,1.2188,1.2142,1.2340,1.2158,1.2686,1.2220,1.3688,1.3142,1.1585
|
||||
Gloppen,Vestland,0.9075,0.9450,0.9788,0.9241,0.9351,0.9721,0.9314,0.9526,0.8668,0.9633,0.7674,0.9272,0.9953
|
||||
Gol,Buskerud,1.2314,1.1906,1.1654,1.2557,1.2284,1.1955,1.2104,1.2123,1.2889,1.1970,1.2990,1.3027,1.1562
|
||||
Gran,Innlandet,1.1542,1.1179,1.1138,1.2278,1.1659,1.1397,1.1418,1.1649,1.2460,1.1452,1.1728,1.2720,1.1329
|
||||
Grane,Nordland,1.2468,1.1847,1.1866,1.3316,1.2668,1.2421,1.2435,1.2667,1.3251,1.2287,1.2027,1.4291,1.2040
|
||||
Gratangen,Troms,1.4190,1.3301,1.3126,1.5165,1.4615,1.4004,1.4208,1.4211,1.5350,1.3740,1.3588,1.6590,1.3100
|
||||
Grimstad,Agder,0.8329,0.8802,0.9014,0.8329,0.8149,0.8696,0.8463,0.8632,0.7810,0.8815,0.7542,0.7931,0.9184
|
||||
Grong,Trøndelag,1.2442,1.1965,1.1942,1.3089,1.2572,1.2495,1.2459,1.2579,1.3047,1.2337,1.1894,1.4061,1.1958
|
||||
Grue,Innlandet,1.3162,1.2849,1.2398,1.3215,1.3101,1.2700,1.2931,1.2789,1.3702,1.2705,1.4452,1.4100,1.2040
|
||||
Gulen,Vestland,0.9023,0.9332,0.9757,0.9342,0.9423,0.9739,0.9338,0.9614,0.8555,0.9616,0.6977,0.9310,1.0035
|
||||
Hadsel,Nordland,1.2262,1.1513,1.1760,1.3544,1.2933,1.2644,1.2459,1.2860,1.3296,1.2354,1.0399,1.4559,1.2179
|
||||
Halden,Østfold,1.0051,1.0196,1.0228,1.0000,0.9880,1.0130,1.0095,1.0123,0.9752,1.0167,0.9601,1.0230,1.0152
|
||||
Hamar,Innlandet,1.2674,1.2318,1.2003,1.2734,1.2548,1.2346,1.2530,1.2386,1.3070,1.2304,1.3488,1.3563,1.1737
|
||||
Hamarøy,Nordland,1.3316,1.2495,1.2519,1.4177,1.3894,1.3408,1.3475,1.3491,1.3973,1.3072,1.2027,1.5287,1.2587
|
||||
Hammerfest,Finnmark,1.4370,1.3340,1.3308,1.5570,1.5048,1.4451,1.4563,1.4649,1.5485,1.4007,1.2924,1.7088,1.3415
|
||||
Haram,Møre og Romsdal,0.9743,0.9921,1.0349,1.0203,1.0433,1.0577,1.0165,1.0421,0.9368,1.0284,0.7043,1.0421,1.0618
|
||||
Hareid,Møre og Romsdal,0.9177,0.9371,0.9818,0.9873,0.9712,0.9944,0.9480,0.9895,0.9029,0.9766,0.6977,1.0000,1.0221
|
||||
Harstad,Troms,1.0617,1.0216,1.0728,1.1949,1.1370,1.1359,1.0969,1.1474,1.1287,1.1035,0.8040,1.2567,1.1259
|
||||
Hasvik,Finnmark,1.3085,1.2259,1.2398,1.4532,1.3702,1.3371,1.3286,1.3544,1.4266,1.3005,1.1395,1.5977,1.2681
|
||||
Hattfjelldal,Nordland,1.4704,1.3733,1.3536,1.5772,1.5312,1.4507,1.4610,1.4754,1.6253,1.4207,1.4286,1.7241,1.3520
|
||||
Haugesund,Rogaland,0.7429,0.7976,0.8604,0.8051,0.7861,0.8324,0.7730,0.8228,0.6953,0.8264,0.5083,0.7893,0.9149
|
||||
Heim,Trøndelag,1.0437,1.0354,1.0607,1.0886,1.0769,1.0857,1.0615,1.0825,1.0519,1.0735,0.9003,1.1188,1.0804
|
||||
Hemnes,Nordland,1.2494,1.1886,1.1912,1.3367,1.2764,1.2533,1.2506,1.2737,1.3138,1.2337,1.1661,1.4215,1.2063
|
||||
Hemsedal,Buskerud,1.2416,1.1768,1.1866,1.3722,1.3173,1.2551,1.2270,1.3018,1.4041,1.2437,1.1429,1.4023,1.2413
|
||||
Herøy (Møre og Romsdal),Møre og Romsdal,0.8766,0.9136,0.9681,0.9316,0.9327,0.9702,0.9173,0.9544,0.8217,0.9499,0.6113,0.9540,1.0035
|
||||
Herøy (Nordland),Nordland,1.0411,1.0432,1.0744,1.1316,1.0913,1.1061,1.0757,1.1018,1.0542,1.0868,0.8505,1.1992,1.0979
|
||||
Hitra,Trøndelag,1.0154,1.0118,1.0486,1.0835,1.0721,1.0782,1.0449,1.0789,1.0248,1.0618,0.8073,1.0996,1.0839
|
||||
Hjartdal,Telemark,1.0848,1.0668,1.0607,1.0886,1.0697,1.0764,1.0804,1.0825,1.0767,1.0751,1.1063,1.1149,1.0629
|
||||
Hjelmeland,Rogaland,0.8920,0.9411,0.9666,0.8886,0.9062,0.9441,0.9102,0.9246,0.8217,0.9432,0.7442,0.8736,0.9709
|
||||
Hol,Buskerud,1.2828,1.2200,1.2170,1.3899,1.3510,1.2775,1.2671,1.3193,1.4312,1.2671,1.2259,1.4368,1.2529
|
||||
Hole,Buskerud,1.1620,1.1218,1.1093,1.2152,1.1683,1.1415,1.1489,1.1596,1.2415,1.1386,1.1860,1.2759,1.1154
|
||||
Holmestrand,Vestfold,1.0643,1.0550,1.0516,1.0582,1.0577,1.0596,1.0638,1.0596,1.0677,1.0601,1.0565,1.0920,1.0466
|
||||
Holtålen,Trøndelag,1.3548,1.2750,1.2731,1.4481,1.4207,1.3538,1.3475,1.3737,1.4921,1.3289,1.2857,1.5556,1.2855
|
||||
Horten,Vestfold,0.9794,0.9823,0.9803,0.9468,0.9375,0.9739,0.9787,0.9702,0.9391,0.9766,0.9934,0.9579,0.9674
|
||||
Hurdal,Akershus,1.2057,1.1690,1.1593,1.2405,1.2188,1.1993,1.1986,1.2053,1.2573,1.1920,1.2060,1.3103,1.1608
|
||||
Hustadvika,Møre og Romsdal,0.9666,0.9764,1.0076,1.0380,1.0096,1.0168,0.9905,1.0193,0.9729,1.0100,0.8040,1.0536,1.0408
|
||||
Hvaler,Østfold,0.8972,0.9194,0.9408,0.9089,0.8726,0.9255,0.9078,0.9246,0.8578,0.9316,0.8073,0.9080,0.9510
|
||||
Hyllestad,Vestland,0.9460,0.9745,1.0121,0.9722,0.9880,1.0168,0.9811,0.9965,0.8894,1.0000,0.7442,0.9923,1.0280
|
||||
Hægebostad,Agder,0.9614,0.9705,0.9985,1.0076,0.9760,1.0074,0.9764,1.0105,0.9436,1.0067,0.8306,0.9962,1.0280
|
||||
Høyanger,Vestland,0.9332,0.9646,0.9909,0.9620,0.9447,0.9888,0.9574,0.9789,0.8781,0.9800,0.7841,0.9617,1.0070
|
||||
Høylandet,Trøndelag,1.3265,1.2672,1.2504,1.3620,1.3341,1.3110,1.3262,1.3123,1.3679,1.2888,1.2990,1.4904,1.2296
|
||||
Hå,Rogaland,0.7455,0.8035,0.8680,0.8177,0.7909,0.8399,0.7778,0.8351,0.7088,0.8381,0.5216,0.7931,0.9242
|
||||
Ibestad,Troms,1.2956,1.2043,1.2155,1.4101,1.3389,1.3035,1.3026,1.3281,1.3950,1.2755,1.1728,1.5134,1.2424
|
||||
Inderøy,Trøndelag,1.1594,1.1513,1.1517,1.2025,1.1755,1.1862,1.1749,1.1754,1.1535,1.1770,1.0963,1.2720,1.1410
|
||||
Indre Fosen,Trøndelag,1.1105,1.0864,1.1062,1.1671,1.1418,1.1471,1.1277,1.1456,1.1242,1.1302,0.9668,1.2146,1.1247
|
||||
Indre Østfold,Østfold,1.1234,1.0943,1.0986,1.1620,1.1274,1.1210,1.1182,1.1368,1.1648,1.1219,1.0864,1.1954,1.1119
|
||||
Iveland,Agder,1.0231,1.0275,1.0486,1.0658,1.0385,1.0633,1.0331,1.0649,1.0271,1.0618,0.9103,1.0651,1.0676
|
||||
Jevnaker,Akershus,1.2288,1.1984,1.1715,1.2278,1.2188,1.2011,1.2104,1.2035,1.2573,1.2003,1.2924,1.2912,1.1492
|
||||
Karasjok,Finnmark,1.9949,1.8664,1.7466,2.0911,2.0433,1.8622,1.9433,1.9070,2.2619,1.8331,2.2093,2.4023,1.6597
|
||||
Karlsøy,Troms,1.3111,1.2200,1.2307,1.4456,1.3678,1.3240,1.3215,1.3526,1.4424,1.2955,1.1860,1.5517,1.2622
|
||||
Karmøy,Rogaland,0.8021,0.8625,0.9181,0.8405,0.8534,0.8939,0.8345,0.8807,0.7381,0.8865,0.5681,0.8314,0.9534
|
||||
Kautokeino,Finnmark,2.0848,1.9489,1.8179,2.2025,2.1514,1.9367,2.0260,1.9965,2.3905,1.9065,2.3156,2.5134,1.7273
|
||||
Kinn (Florø),Vestland,0.8278,0.8782,0.9272,0.8734,0.8654,0.9125,0.8652,0.8947,0.7630,0.8965,0.6047,0.8697,0.9522
|
||||
Kinn (Måløy),Vestland,0.9409,0.9725,1.0182,0.9823,1.0072,1.0298,0.9905,1.0088,0.8871,1.0033,0.6811,1.0038,1.0396
|
||||
Klepp,Rogaland,0.8329,0.8684,0.9196,0.8810,0.8750,0.9069,0.8605,0.9035,0.7901,0.8982,0.6179,0.8774,0.9627
|
||||
Kongsberg,Buskerud,1.1774,1.1670,1.1472,1.1797,1.1635,1.1695,1.1702,1.1702,1.1693,1.1736,1.2126,1.2146,1.1305
|
||||
Kongsvinger,Innlandet,1.2159,1.1847,1.1684,1.2405,1.2163,1.2011,1.2033,1.2123,1.2483,1.1987,1.2425,1.3027,1.1643
|
||||
Kragerø,Telemark,0.9923,1.0216,1.0212,0.9848,0.9687,1.0093,1.0000,1.0000,0.9481,1.0167,0.9435,1.0038,1.0082
|
||||
Kristiansand,Agder,0.7866,0.8389,0.8680,0.7873,0.7692,0.8287,0.7967,0.8246,0.7336,0.8397,0.6944,0.7318,0.8928
|
||||
Kristiansund,Møre og Romsdal,0.8175,0.8605,0.9165,0.8987,0.8774,0.9088,0.8558,0.9000,0.7810,0.8965,0.5615,0.8966,0.9685
|
||||
Krødsherad,Buskerud,1.2699,1.2397,1.2109,1.2886,1.2716,1.2477,1.2577,1.2526,1.3093,1.2504,1.3289,1.3640,1.1923
|
||||
Kvam,Vestland,0.9177,0.9568,0.9788,0.9342,0.9351,0.9739,0.9409,0.9561,0.8668,0.9666,0.7907,0.9387,0.9872
|
||||
Kvinesdal,Agder,0.9357,0.9646,0.9833,0.9241,0.9375,0.9683,0.9456,0.9614,0.8849,0.9699,0.8405,0.9157,0.9918
|
||||
Kvinnherad,Vestland,0.9640,1.0138,1.0349,0.9544,0.9976,1.0317,0.9929,1.0105,0.9052,1.0217,0.7774,0.9349,1.0338
|
||||
Kviteseid,Telemark,1.0643,1.0570,1.0531,1.0759,1.0505,1.0615,1.0591,1.0702,1.0722,1.0651,1.0797,1.0843,1.0571
|
||||
Kvitsøy,Rogaland,0.7095,0.7839,0.8407,0.7519,0.7524,0.7970,0.7400,0.7825,0.6479,0.7930,0.5050,0.7280,0.8788
|
||||
Kvæfjord,Troms,1.2853,1.2024,1.2170,1.3975,1.3389,1.3073,1.3026,1.3263,1.3837,1.2771,1.1362,1.4981,1.2436
|
||||
Kvænangen,Troms,1.4293,1.3320,1.3217,1.5595,1.4856,1.4171,1.4374,1.4386,1.5530,1.3873,1.3488,1.7241,1.3263
|
||||
Kåfjord,Troms,1.3985,1.2908,1.2914,1.5468,1.4519,1.3911,1.4019,1.4281,1.5553,1.3639,1.3189,1.6897,1.3147
|
||||
Larvik,Vestfold,1.0077,1.0118,1.0228,1.0177,0.9952,1.0223,1.0142,1.0246,0.9842,1.0250,0.9435,1.0230,1.0233
|
||||
Lavangen,Troms,1.3265,1.2456,1.2382,1.4380,1.3630,1.3128,1.3286,1.3386,1.4424,1.2938,1.2724,1.5594,1.2506
|
||||
Lebesby,Finnmark,1.5424,1.4440,1.4234,1.7038,1.6370,1.5363,1.5437,1.5772,1.7698,1.5042,1.4917,1.8927,1.4277
|
||||
Leirfjord,Nordland,1.2391,1.1886,1.1942,1.3089,1.2644,1.2588,1.2648,1.2614,1.2528,1.2304,1.0963,1.3985,1.1993
|
||||
Leka,Trøndelag,1.1131,1.0825,1.1093,1.1873,1.1538,1.1564,1.1371,1.1596,1.1400,1.1319,0.9336,1.2490,1.1329
|
||||
Lesja,Innlandet,1.2776,1.1965,1.1988,1.4076,1.3317,1.2663,1.2553,1.3158,1.4650,1.2571,1.2425,1.4789,1.2471
|
||||
Levanger,Trøndelag,1.0848,1.0727,1.0880,1.1468,1.0986,1.1173,1.0993,1.1123,1.0926,1.1102,1.0033,1.2107,1.0956
|
||||
Lier,Buskerud,1.1131,1.1002,1.0804,1.0987,1.1010,1.0968,1.1040,1.0965,1.1038,1.0968,1.1761,1.1303,1.0699
|
||||
Lierne,Trøndelag,1.5296,1.4126,1.3930,1.6354,1.5889,1.4991,1.5177,1.5316,1.6998,1.4708,1.4651,1.7893,1.3951
|
||||
Lillehammer,Innlandet,1.2339,1.1965,1.1836,1.2785,1.2428,1.2197,1.2246,1.2298,1.2844,1.2170,1.2492,1.3525,1.1725
|
||||
Lillesand,Agder,0.8329,0.8802,0.9044,0.8253,0.8125,0.8715,0.8416,0.8632,0.7788,0.8798,0.7542,0.7893,0.9196
|
||||
Lillestrøm,Akershus,1.1620,1.1415,1.1244,1.1595,1.1394,1.1397,1.1466,1.1456,1.1716,1.1419,1.2027,1.1916,1.1119
|
||||
Lindesnes,Agder,0.8175,0.8723,0.9059,0.8354,0.8221,0.8734,0.8392,0.8614,0.7562,0.8831,0.6811,0.8161,0.9277
|
||||
Lom,Innlandet,1.1337,1.0806,1.0880,1.2152,1.1418,1.1322,1.1229,1.1667,1.2032,1.1235,1.0797,1.2452,1.1340
|
||||
Loppa,Finnmark,1.4113,1.3104,1.3050,1.5873,1.4688,1.3985,1.4090,1.4474,1.5959,1.3790,1.3621,1.7471,1.3310
|
||||
Lund,Rogaland,0.9075,0.9509,0.9772,0.9190,0.9303,0.9665,0.9243,0.9544,0.8713,0.9666,0.7841,0.9157,0.9907
|
||||
Lunner,Akershus,1.2494,1.2004,1.1866,1.3038,1.2644,1.2309,1.2364,1.2491,1.3341,1.2270,1.2658,1.3870,1.1911
|
||||
Lurøy,Nordland,1.0977,1.0766,1.1077,1.1772,1.1538,1.1583,1.1348,1.1544,1.1084,1.1269,0.8704,1.2452,1.1329
|
||||
Luster,Vestland,0.9949,0.9921,1.0091,1.0456,1.0000,1.0242,1.0071,1.0211,1.0023,1.0200,0.9269,1.0805,1.0268
|
||||
Lyngdal,Agder,0.8715,0.9116,0.9408,0.8886,0.8702,0.9181,0.8889,0.9123,0.8172,0.9265,0.7442,0.8812,0.9580
|
||||
Lyngen,Troms,1.3033,1.2161,1.2200,1.4430,1.3510,1.3054,1.3144,1.3298,1.4199,1.2838,1.1993,1.5556,1.2436
|
||||
Lærdal,Vestland,0.9126,0.9411,0.9530,0.9013,0.8966,0.9311,0.9243,0.9246,0.8555,0.9366,0.8605,0.9004,0.9545
|
||||
Lødingen,Nordland,1.3059,1.2181,1.2307,1.4253,1.3606,1.3203,1.3191,1.3404,1.4063,1.2905,1.1528,1.5556,1.2564
|
||||
Lørenskog,Akershus,1.1851,1.1532,1.1472,1.2177,1.1923,1.1788,1.1797,1.1895,1.2325,1.1753,1.1661,1.2605,1.1492
|
||||
Løten,Innlandet,1.3548,1.3026,1.2671,1.3848,1.3510,1.3091,1.3310,1.3263,1.4244,1.3088,1.4385,1.4904,1.2448
|
||||
Malvik,Trøndelag,1.1388,1.1198,1.1320,1.1620,1.1731,1.1732,1.1584,1.1614,1.1422,1.1503,1.0100,1.2184,1.1329
|
||||
Marker,Østfold,1.0823,1.0629,1.0744,1.1139,1.0913,1.0950,1.0827,1.1035,1.0858,1.0935,1.0166,1.1341,1.0862
|
||||
Masfjorden,Vestland,0.9332,0.9646,1.0015,0.9519,0.9736,1.0037,0.9693,0.9842,0.8713,0.9850,0.7309,0.9579,1.0186
|
||||
Melhus,Trøndelag,1.1080,1.0864,1.0971,1.1468,1.1250,1.1304,1.1158,1.1316,1.1287,1.1202,1.0233,1.1954,1.1119
|
||||
Meløy,Nordland,1.2031,1.1631,1.1715,1.2886,1.2452,1.2328,1.2340,1.2351,1.2415,1.2070,1.0399,1.3870,1.1818
|
||||
Meråker,Trøndelag,1.1799,1.1532,1.1533,1.2304,1.2067,1.1918,1.1797,1.2000,1.2348,1.1870,1.1262,1.2950,1.1562
|
||||
Midtre Gauldal,Trøndelag,1.1825,1.1532,1.1578,1.2127,1.2115,1.1974,1.1868,1.1982,1.2167,1.1853,1.1096,1.2797,1.1550
|
||||
Midt-Telemark,Telemark,1.1388,1.1336,1.1108,1.1291,1.1154,1.1155,1.1253,1.1193,1.1535,1.1302,1.2027,1.1648,1.0897
|
||||
Modalen,Vestland,0.9460,0.9725,0.9985,0.9873,0.9760,1.0000,0.9716,0.9930,0.9233,0.9933,0.8040,1.0038,1.0221
|
||||
Modum,Buskerud,1.1362,1.1356,1.1199,1.1266,1.1322,1.1266,1.1277,1.1193,1.1512,1.1319,1.1761,1.1609,1.0944
|
||||
Molde,Møre og Romsdal,0.9383,0.9587,0.9970,0.9949,0.9832,1.0074,0.9716,0.9965,0.9097,0.9900,0.7409,1.0153,1.0245
|
||||
Moskenes,Nordland,1.0051,0.9804,1.0334,1.1570,1.0817,1.0894,1.0449,1.1018,1.0632,1.0584,0.7409,1.2184,1.0967
|
||||
Moss,Østfold,1.0360,1.0354,1.0334,1.0203,1.0144,1.0298,1.0331,1.0316,1.0248,1.0334,1.0299,1.0421,1.0245
|
||||
Målselv,Troms,1.5116,1.3949,1.3672,1.6329,1.5649,1.4693,1.5035,1.5018,1.6862,1.4441,1.4983,1.8046,1.3671
|
||||
Måsøy,Finnmark,1.4062,1.3104,1.3187,1.5316,1.4976,1.4302,1.4303,1.4579,1.5485,1.3873,1.2292,1.6590,1.3392
|
||||
Namsos,Trøndelag,1.2211,1.1768,1.1775,1.2582,1.2284,1.2309,1.2270,1.2333,1.2348,1.2120,1.1661,1.3333,1.1772
|
||||
Namsskogan,Trøndelag,1.3496,1.2613,1.2564,1.4582,1.3870,1.3408,1.3428,1.3684,1.4763,1.3155,1.2857,1.5824,1.2762
|
||||
Nannestad,Akershus,1.2159,1.1768,1.1624,1.2633,1.2236,1.1955,1.2057,1.2158,1.2844,1.1970,1.2392,1.3180,1.1678
|
||||
Narvik,Nordland,1.3136,1.2220,1.2276,1.4380,1.3486,1.3128,1.3262,1.3333,1.4312,1.2905,1.2093,1.5709,1.2448
|
||||
Nes,Akershus,1.2365,1.2004,1.1821,1.2633,1.2308,1.2123,1.2246,1.2246,1.2731,1.2104,1.2658,1.3372,1.1760
|
||||
Nesbyen,Buskerud,1.1465,1.1297,1.1123,1.1797,1.1418,1.1248,1.1324,1.1368,1.1874,1.1319,1.1993,1.2261,1.1084
|
||||
Nesna,Nordland,1.1645,1.1336,1.1457,1.2253,1.1899,1.1955,1.1868,1.1930,1.1693,1.1703,1.0166,1.3027,1.1550
|
||||
Nesodden,Akershus,1.0103,1.0039,1.0091,1.0101,1.0048,1.0186,1.0095,1.0140,1.0090,1.0150,0.9934,1.0268,1.0117
|
||||
Nesseby,Finnmark,1.8303,1.7053,1.6267,1.9241,1.8966,1.7505,1.8061,1.7877,2.0519,1.7112,1.8937,2.1724,1.5746
|
||||
Nissedal,Telemark,1.0591,1.0413,1.0607,1.1215,1.0889,1.0875,1.0662,1.1018,1.0835,1.0851,0.9601,1.1149,1.0932
|
||||
Nittedal,Akershus,1.2648,1.2122,1.2003,1.2835,1.2620,1.2477,1.2553,1.2596,1.2980,1.2371,1.2492,1.3563,1.1935
|
||||
Nome,Telemark,1.1054,1.1061,1.0910,1.1013,1.0865,1.0912,1.0969,1.0930,1.1084,1.1035,1.1495,1.1303,1.0723
|
||||
Nord-Aurdal,Innlandet,1.2725,1.2161,1.2033,1.3468,1.2885,1.2495,1.2577,1.2754,1.3589,1.2437,1.2757,1.4176,1.2145
|
||||
Nord-Fron,Innlandet,1.2159,1.1611,1.1502,1.2911,1.2332,1.1918,1.1986,1.2211,1.3228,1.1886,1.2458,1.3563,1.1690
|
||||
Nordkapp,Finnmark,1.3445,1.2593,1.2762,1.4962,1.4447,1.3855,1.3641,1.4123,1.5079,1.3472,1.1694,1.6207,1.3112
|
||||
Nord-Odal,Innlandet,1.2416,1.2122,1.1927,1.2456,1.2428,1.2216,1.2270,1.2263,1.2731,1.2187,1.2757,1.3142,1.1713
|
||||
Nordre Follo,Akershus,1.1645,1.1316,1.1275,1.2025,1.1611,1.1527,1.1584,1.1702,1.2122,1.1536,1.1595,1.2605,1.1317
|
||||
Nordre Land,Innlandet,1.3059,1.2652,1.2246,1.3114,1.2716,1.2607,1.2837,1.2737,1.3341,1.2654,1.4053,1.3908,1.1993
|
||||
Nordreisa,Troms,1.3907,1.2908,1.2868,1.5038,1.4471,1.3836,1.3948,1.4053,1.5260,1.3506,1.3090,1.6475,1.2995
|
||||
Nore og Uvdal,Buskerud,1.1799,1.1493,1.1472,1.2506,1.2067,1.1825,1.1678,1.2053,1.2686,1.1853,1.1495,1.2912,1.1667
|
||||
Notodden,Telemark,1.0977,1.0904,1.0819,1.1139,1.0938,1.0894,1.0898,1.0947,1.1016,1.0952,1.1030,1.1533,1.0781
|
||||
Nærøysund,Trøndelag,1.2545,1.2024,1.2079,1.3241,1.3005,1.2756,1.2719,1.2825,1.2889,1.2454,1.1096,1.4176,1.2179
|
||||
Oppdal,Trøndelag,1.2416,1.1650,1.1745,1.3570,1.2957,1.2421,1.2246,1.2825,1.3928,1.2254,1.1694,1.4215,1.2249
|
||||
Orkland,Trøndelag,1.1645,1.1415,1.1457,1.2025,1.1779,1.1825,1.1702,1.1825,1.1806,1.1703,1.0897,1.2759,1.1480
|
||||
Os,Innlandet,1.5039,1.3988,1.3809,1.6177,1.5769,1.4767,1.4823,1.5158,1.6930,1.4508,1.4618,1.7471,1.3881
|
||||
Osen,Trøndelag,1.0746,1.0747,1.0971,1.1392,1.1298,1.1266,1.1040,1.1211,1.1016,1.1085,0.9070,1.1839,1.1096
|
||||
Oslo,Oslo,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000
|
||||
Osterøy,Vestland,0.9769,1.0079,1.0334,0.9899,1.0072,1.0372,1.0071,1.0175,0.9165,1.0200,0.8140,1.0077,1.0396
|
||||
Overhalla,Trøndelag,1.2828,1.2377,1.2261,1.3392,1.2885,1.2812,1.2861,1.2877,1.3205,1.2705,1.2458,1.4444,1.2179
|
||||
Porsanger,Finnmark,1.6838,1.5874,1.5281,1.7949,1.7452,1.6313,1.6643,1.6702,1.8894,1.6060,1.7276,1.9923,1.4953
|
||||
Porsgrunn,Telemark,1.0977,1.0963,1.0789,1.0810,1.0649,1.0801,1.0898,1.0842,1.0835,1.0868,1.1395,1.1034,1.0571
|
||||
Rakkestad,Østfold,1.0720,1.0491,1.0592,1.1165,1.0721,1.0801,1.0709,1.0930,1.0926,1.0785,1.0133,1.1418,1.0781
|
||||
Rana,Nordland,1.3830,1.2967,1.2838,1.4835,1.4279,1.3631,1.3735,1.3895,1.4966,1.3406,1.3488,1.6169,1.2890
|
||||
Randaberg,Rogaland,0.7892,0.8468,0.8998,0.8304,0.8317,0.8752,0.8180,0.8649,0.7404,0.8715,0.5781,0.8161,0.9394
|
||||
Rauma,Møre og Romsdal,0.8920,0.9194,0.9408,0.9190,0.9014,0.9292,0.9102,0.9298,0.8646,0.9299,0.7841,0.9195,0.9662
|
||||
Rendalen,Innlandet,1.3702,1.3065,1.2822,1.4532,1.3894,1.3389,1.3499,1.3667,1.4763,1.3306,1.4020,1.5632,1.2762
|
||||
Rennebu,Trøndelag,1.2956,1.2083,1.2185,1.3797,1.3486,1.2980,1.2813,1.3228,1.4108,1.2721,1.2060,1.4636,1.2494
|
||||
Rindal,Trøndelag,1.2391,1.1788,1.1851,1.3063,1.2885,1.2514,1.2459,1.2632,1.3025,1.2287,1.1262,1.3602,1.2051
|
||||
Ringebu,Innlandet,1.2365,1.1906,1.1715,1.2734,1.2332,1.2086,1.2199,1.2246,1.2889,1.2053,1.2691,1.3333,1.1678
|
||||
Ringerike,Buskerud,1.2442,1.2083,1.1730,1.2203,1.2043,1.2011,1.2246,1.2070,1.2551,1.2003,1.3422,1.2912,1.1399
|
||||
Ringsaker,Innlandet,1.3033,1.2613,1.2261,1.3266,1.2885,1.2626,1.2837,1.2754,1.3612,1.2654,1.4086,1.4061,1.2028
|
||||
Risør,Agder,0.9023,0.9371,0.9514,0.9089,0.8846,0.9311,0.9125,0.9246,0.8646,0.9399,0.8306,0.9042,0.9569
|
||||
Rollag,Buskerud,1.1285,1.1238,1.1168,1.1620,1.1418,1.1322,1.1206,1.1404,1.1738,1.1419,1.1262,1.1916,1.1189
|
||||
Rælingen,Akershus,1.1928,1.1513,1.1457,1.2329,1.1923,1.1788,1.1820,1.1947,1.2348,1.1770,1.1794,1.2835,1.1515
|
||||
Rødøy,Nordland,1.0540,1.0314,1.0713,1.1544,1.1106,1.1192,1.0922,1.1193,1.0677,1.0902,0.8140,1.2261,1.1072
|
||||
Røros,Trøndelag,1.5193,1.4145,1.3915,1.6304,1.5913,1.4860,1.4917,1.5281,1.7201,1.4624,1.4983,1.7701,1.3974
|
||||
Røst,Nordland,0.8406,0.8861,0.9514,0.9620,0.9255,0.9628,0.8913,0.9614,0.8420,0.9332,0.5116,0.9847,1.0140
|
||||
Røyrvik,Trøndelag,1.5167,1.3988,1.3718,1.6354,1.5673,1.4804,1.5035,1.5158,1.6953,1.4474,1.4817,1.7969,1.3811
|
||||
Råde,Østfold,1.0617,1.0570,1.0592,1.0684,1.0529,1.0633,1.0591,1.0684,1.0564,1.0634,1.0332,1.0881,1.0559
|
||||
Salangen,Troms,1.3419,1.2534,1.2473,1.4608,1.3750,1.3277,1.3452,1.3544,1.4763,1.3072,1.2890,1.5939,1.2611
|
||||
Saltdal,Nordland,1.3239,1.2417,1.2382,1.4177,1.3510,1.3184,1.3310,1.3316,1.3860,1.2871,1.2425,1.5594,1.2459
|
||||
Samnanger,Vestland,0.9023,0.9371,0.9727,0.9316,0.9231,0.9646,0.9267,0.9526,0.8555,0.9566,0.7542,0.9272,0.9953
|
||||
Sande,Møre og Romsdal,0.8792,0.9136,0.9590,0.9392,0.9231,0.9590,0.9125,0.9509,0.8465,0.9482,0.6711,0.9425,0.9965
|
||||
Sandefjord,Vestfold,1.0231,1.0314,1.0288,1.0101,0.9976,1.0223,1.0236,1.0246,0.9955,1.0284,1.0033,1.0268,1.0186
|
||||
Sandnes,Rogaland,0.8278,0.8782,0.9287,0.8734,0.8726,0.9088,0.8558,0.8982,0.7856,0.9048,0.6512,0.8697,0.9627
|
||||
Sarpsborg,Østfold,1.0334,1.0334,1.0410,1.0405,1.0216,1.0428,1.0355,1.0456,1.0135,1.0451,0.9867,1.0575,1.0408
|
||||
Sauda,Rogaland,0.8663,0.8978,0.9150,0.8633,0.8582,0.8976,0.8818,0.8947,0.8081,0.8982,0.8007,0.8314,0.9324
|
||||
Sel,Innlandet,1.2674,1.2083,1.1942,1.3241,1.2837,1.2421,1.2482,1.2667,1.3589,1.2354,1.2990,1.3755,1.2016
|
||||
Selbu,Trøndelag,1.2674,1.1906,1.1988,1.3696,1.3101,1.2700,1.2624,1.2965,1.3883,1.2554,1.1894,1.4636,1.2261
|
||||
Seljord,Telemark,0.9846,1.0079,1.0137,0.9899,0.9808,1.0056,0.9811,1.0053,0.9797,1.0200,0.9568,0.9693,1.0221
|
||||
Senja,Troms,1.3985,1.3045,1.3005,1.5266,1.4663,1.3929,1.4066,1.4175,1.5350,1.3639,1.2824,1.6667,1.3124
|
||||
Sigdal,Buskerud,1.2751,1.2456,1.2124,1.2810,1.2620,1.2477,1.2600,1.2509,1.2844,1.2504,1.3289,1.3448,1.1911
|
||||
Siljan,Telemark,1.0977,1.0864,1.0850,1.1063,1.0889,1.1061,1.0993,1.1053,1.0745,1.1068,1.0498,1.1226,1.0874
|
||||
Sirdal,Agder,0.9049,0.9312,0.9545,0.9139,0.8966,0.9423,0.9173,0.9386,0.8646,0.9432,0.7907,0.8889,0.9744
|
||||
Skaun,Trøndelag,1.1491,1.1316,1.1411,1.1747,1.1779,1.1806,1.1631,1.1754,1.1490,1.1669,1.0332,1.2222,1.1434
|
||||
Skien,Telemark,1.0848,1.0806,1.0728,1.0911,1.0745,1.0782,1.0827,1.0825,1.0948,1.0835,1.0997,1.1073,1.0618
|
||||
Skiptvet,Østfold,1.0925,1.0688,1.0759,1.1316,1.0962,1.0950,1.0898,1.1105,1.1264,1.0968,1.0432,1.1609,1.0897
|
||||
Skjervøy,Troms,1.2237,1.1552,1.1791,1.4000,1.2957,1.2588,1.2435,1.2860,1.3679,1.2387,1.0764,1.5172,1.2214
|
||||
Skjåk,Innlandet,1.0925,1.0413,1.0546,1.2101,1.1178,1.0968,1.0851,1.1333,1.2077,1.0918,1.0465,1.2375,1.1131
|
||||
Smøla,Møre og Romsdal,0.9152,0.9391,0.9833,0.9873,0.9712,0.9907,0.9504,0.9860,0.8894,0.9750,0.6844,1.0000,1.0233
|
||||
Snåsa,Trøndelag,1.2648,1.2181,1.2155,1.3291,1.3053,1.2737,1.2695,1.2807,1.3363,1.2521,1.1927,1.4215,1.2145
|
||||
Sogndal,Vestland,0.9897,1.0000,1.0030,0.9924,0.9808,1.0019,0.9953,0.9982,0.9752,1.0067,0.9701,0.9962,1.0058
|
||||
Sokndal,Rogaland,0.8355,0.8861,0.9272,0.8709,0.8702,0.9088,0.8629,0.9018,0.7946,0.9098,0.6478,0.8544,0.9604
|
||||
Sola,Rogaland,0.7969,0.8507,0.9044,0.8430,0.8389,0.8790,0.8251,0.8737,0.7540,0.8765,0.5947,0.8391,0.9441
|
||||
Solund,Vestland,0.8817,0.9214,0.9712,0.9266,0.9327,0.9721,0.9243,0.9509,0.8126,0.9516,0.6179,0.9464,1.0035
|
||||
Sortland,Nordland,1.2828,1.2043,1.2200,1.3873,1.3486,1.3091,1.3002,1.3246,1.3837,1.2755,1.1130,1.4981,1.2459
|
||||
Stad,Vestland,0.8689,0.9018,0.9347,0.8987,0.8678,0.9218,0.8913,0.9193,0.8194,0.9232,0.7542,0.9004,0.9604
|
||||
Stange,Innlandet,1.3959,1.3340,1.2929,1.4329,1.3870,1.3426,1.3712,1.3649,1.4808,1.3356,1.4751,1.5517,1.2681
|
||||
Stavanger,Rogaland,0.7866,0.8585,0.8998,0.8051,0.8173,0.8641,0.8156,0.8421,0.7111,0.8631,0.5980,0.7931,0.9231
|
||||
Steigen,Nordland,1.2828,1.2004,1.2109,1.3747,1.3365,1.2980,1.2979,1.3088,1.3454,1.2621,1.1296,1.4828,1.2343
|
||||
Steinkjer,Trøndelag,1.2442,1.2122,1.2033,1.2911,1.2620,1.2533,1.2506,1.2509,1.2709,1.2371,1.1993,1.3831,1.1935
|
||||
Stjørdal,Trøndelag,1.1542,1.1238,1.1275,1.1899,1.1731,1.1676,1.1678,1.1649,1.1625,1.1519,1.0532,1.2644,1.1329
|
||||
Stord,Vestland,0.8149,0.8664,0.9090,0.8405,0.8341,0.8827,0.8392,0.8702,0.7562,0.8781,0.6578,0.8161,0.9359
|
||||
Stor-Elvdal,Innlandet,1.3933,1.3222,1.2929,1.4608,1.4038,1.3501,1.3641,1.3825,1.5011,1.3422,1.4551,1.5632,1.2855
|
||||
Storfjord,Troms,1.3805,1.2770,1.2625,1.5291,1.4111,1.3408,1.3759,1.3842,1.5440,1.3239,1.3754,1.6820,1.2762
|
||||
Strand,Rogaland,0.8329,0.8959,0.9332,0.8608,0.8630,0.9050,0.8582,0.8912,0.7788,0.9082,0.6678,0.8544,0.9510
|
||||
Stranda,Møre og Romsdal,0.9049,0.8978,0.9363,0.9924,0.9327,0.9441,0.9220,0.9596,0.9413,0.9382,0.7741,1.0077,0.9907
|
||||
Stryn,Vestland,1.0000,1.0039,1.0212,1.0430,1.0024,1.0317,1.0165,1.0316,0.9752,1.0284,0.8970,1.0613,1.0408
|
||||
Sula,Møre og Romsdal,0.9614,0.9882,1.0273,1.0076,1.0240,1.0410,1.0024,1.0228,0.9255,1.0184,0.7243,1.0307,1.0490
|
||||
Suldal,Rogaland,0.8560,0.9057,0.9272,0.8456,0.8606,0.8957,0.8700,0.8842,0.7991,0.8998,0.7608,0.8238,0.9371
|
||||
Sunndal,Møre og Romsdal,0.8278,0.8625,0.8877,0.8405,0.8293,0.8641,0.8463,0.8579,0.7788,0.8648,0.7276,0.8314,0.9161
|
||||
Sunnfjord,Vestland,1.0437,1.0491,1.0713,1.0684,1.0721,1.0875,1.0638,1.0737,1.0113,1.0718,0.9070,1.1073,1.0769
|
||||
Surnadal,Møre og Romsdal,1.0848,1.0727,1.0835,1.1190,1.1106,1.1136,1.0969,1.1105,1.0926,1.1002,0.9734,1.1533,1.0956
|
||||
Sveio,Vestland,0.8920,0.9253,0.9681,0.9241,0.9375,0.9590,0.9220,0.9509,0.8555,0.9516,0.6844,0.9310,0.9942
|
||||
Sykkylven,Møre og Romsdal,0.8792,0.9175,0.9575,0.9241,0.9207,0.9534,0.9078,0.9421,0.8465,0.9416,0.6977,0.9272,0.9860
|
||||
Sømna,Nordland,1.1620,1.1238,1.1396,1.2304,1.1923,1.1955,1.1844,1.1965,1.1896,1.1686,1.0066,1.3103,1.1585
|
||||
Søndre Land,Innlandet,1.2828,1.2397,1.2094,1.2987,1.2692,1.2439,1.2600,1.2561,1.3273,1.2487,1.3621,1.3793,1.1865
|
||||
Sør-Aurdal,Innlandet,1.2776,1.2299,1.2109,1.3089,1.3029,1.2533,1.2600,1.2667,1.3476,1.2487,1.2990,1.3793,1.2040
|
||||
Sørfold,Nordland,1.3470,1.2750,1.2671,1.4430,1.3966,1.3520,1.3617,1.3614,1.4244,1.3239,1.2492,1.5709,1.2681
|
||||
Sør-Fron,Innlandet,1.3008,1.2436,1.2140,1.3696,1.3173,1.2533,1.2766,1.2825,1.4153,1.2538,1.3721,1.4444,1.2145
|
||||
Sør-Odal,Innlandet,1.2365,1.2043,1.1851,1.2582,1.2356,1.2160,1.2222,1.2281,1.2754,1.2154,1.2724,1.3218,1.1772
|
||||
Sørreisa,Troms,1.3728,1.2947,1.2868,1.4810,1.4255,1.3706,1.3830,1.3860,1.4898,1.3422,1.2857,1.6169,1.2914
|
||||
Sør-Varanger,Finnmark,1.7301,1.6365,1.5660,1.8405,1.7837,1.6667,1.7069,1.7053,1.9345,1.6444,1.8040,2.0613,1.5152
|
||||
Tana,Finnmark,1.8817,1.7701,1.6753,1.9823,1.9471,1.7933,1.8534,1.8333,2.1219,1.7613,1.9934,2.2375,1.6096
|
||||
Time,Rogaland,0.8072,0.8585,0.9150,0.8684,0.8582,0.8994,0.8369,0.8912,0.7720,0.8915,0.5914,0.8544,0.9615
|
||||
Tingvoll,Møre og Romsdal,1.0925,1.0727,1.0956,1.1392,1.1346,1.1341,1.1182,1.1298,1.0903,1.1119,0.9203,1.1992,1.1154
|
||||
Tinn,Telemark,1.0566,1.0550,1.0546,1.1013,1.0721,1.0670,1.0473,1.0842,1.1174,1.0768,1.0432,1.0843,1.0769
|
||||
Tjeldsund,Troms,1.4422,1.3379,1.3263,1.5722,1.5024,1.4227,1.4374,1.4579,1.5982,1.3973,1.3787,1.7241,1.3403
|
||||
Tokke,Telemark,1.0694,1.0864,1.0698,1.0532,1.0481,1.0633,1.0591,1.0649,1.0497,1.0818,1.0997,1.0575,1.0583
|
||||
Tolga,Innlandet,1.4576,1.3674,1.3475,1.5620,1.5144,1.4283,1.4326,1.4649,1.6230,1.4124,1.4585,1.6858,1.3520
|
||||
Tromsø,Troms,1.3393,1.2613,1.2610,1.4937,1.4111,1.3464,1.3546,1.3737,1.4876,1.3239,1.2292,1.6169,1.2821
|
||||
Trondheim,Trøndelag,1.0129,1.0177,1.0410,1.0481,1.0409,1.0540,1.0331,1.0421,1.0023,1.0417,0.8771,1.0958,1.0490
|
||||
Trysil,Innlandet,1.3599,1.2986,1.2777,1.4329,1.3990,1.3371,1.3452,1.3632,1.4650,1.3255,1.3688,1.5249,1.2751
|
||||
Træna,Nordland,0.8509,0.8919,0.9575,0.9696,0.9351,0.9683,0.9031,0.9632,0.8510,0.9416,0.5515,0.9962,1.0128
|
||||
Tvedestrand,Agder,0.9897,1.0236,1.0243,0.9823,0.9663,1.0130,1.0000,1.0018,0.9368,1.0184,0.9336,0.9885,1.0152
|
||||
Tydal,Trøndelag,1.4447,1.3497,1.3338,1.5848,1.5216,1.4302,1.4303,1.4719,1.6569,1.4040,1.4086,1.6935,1.3590
|
||||
Tynset,Innlandet,1.4679,1.3851,1.3520,1.5671,1.5072,1.4209,1.4350,1.4649,1.6388,1.4107,1.5150,1.6858,1.3497
|
||||
Tysnes,Vestland,0.9563,0.9862,1.0273,0.9772,1.0120,1.0391,0.9929,1.0211,0.8916,1.0167,0.7076,0.9808,1.0478
|
||||
Tysvær,Rogaland,0.9460,0.9784,1.0091,0.9747,0.9784,1.0037,0.9740,0.9912,0.9029,0.9950,0.7741,1.0115,1.0198
|
||||
Tønsberg,Vestfold,1.0463,1.0472,1.0425,1.0278,1.0312,1.0410,1.0473,1.0386,1.0339,1.0417,1.0332,1.0498,1.0350
|
||||
Ullensaker,Akershus,1.3419,1.2770,1.2519,1.3570,1.3317,1.2980,1.3191,1.3211,1.3950,1.2905,1.3754,1.4406,1.2378
|
||||
Ullensvang,Vestland,0.7918,0.8271,0.8467,0.7949,0.7740,0.8138,0.8014,0.8175,0.7494,0.8197,0.7475,0.7586,0.8741
|
||||
Ulstein,Møre og Romsdal,0.8638,0.8998,0.9530,0.9241,0.9183,0.9497,0.9007,0.9386,0.8217,0.9366,0.6312,0.9349,0.9895
|
||||
Ulvik,Vestland,0.9409,0.9411,0.9636,1.0101,0.9399,0.9702,0.9527,0.9789,0.9549,0.9750,0.8472,0.9962,0.9977
|
||||
Utsira,Rogaland,0.6530,0.7446,0.8149,0.7215,0.7067,0.7672,0.6927,0.7509,0.5801,0.7629,0.3920,0.6897,0.8671
|
||||
Vadsø,Finnmark,1.5090,1.4086,1.3915,1.6532,1.5889,1.5028,1.5083,1.5386,1.7043,1.4725,1.4485,1.8276,1.3986
|
||||
Vaksdal,Vestland,0.9871,1.0079,1.0364,1.0203,1.0312,1.0484,1.0165,1.0351,0.9639,1.0351,0.8073,1.0345,1.0524
|
||||
Valle,Agder,0.9897,0.9784,0.9985,1.0608,1.0168,1.0112,0.9858,1.0368,1.0609,1.0150,0.9336,1.0307,1.0490
|
||||
Vang,Innlandet,1.1774,1.1238,1.1275,1.2911,1.2091,1.1732,1.1655,1.2035,1.2957,1.1686,1.1229,1.3410,1.1702
|
||||
Vanylven,Møre og Romsdal,0.9769,0.9705,1.0152,1.0557,1.0240,1.0428,1.0071,1.0474,0.9752,1.0217,0.7575,1.0843,1.0618
|
||||
Vardø,Finnmark,1.4190,1.3242,1.3293,1.5570,1.5144,1.4488,1.4303,1.4772,1.6005,1.4090,1.2591,1.7050,1.3543
|
||||
Vefsn,Nordland,1.2365,1.1827,1.1866,1.2962,1.2668,1.2458,1.2411,1.2579,1.2754,1.2237,1.1462,1.3870,1.1958
|
||||
Vega,Nordland,1.0925,1.0727,1.1032,1.1848,1.1418,1.1527,1.1277,1.1544,1.1038,1.1252,0.8771,1.2529,1.1352
|
||||
Vegårshei,Agder,1.0463,1.0413,1.0516,1.0861,1.0553,1.0689,1.0520,1.0737,1.0609,1.0718,0.9801,1.0843,1.0688
|
||||
Vennesla,Agder,0.9434,0.9784,0.9924,0.9468,0.9423,0.9795,0.9551,0.9737,0.9029,0.9866,0.8605,0.9272,1.0000
|
||||
Verdal,Trøndelag,1.1851,1.1513,1.1517,1.2228,1.1971,1.1955,1.1986,1.1895,1.1986,1.1770,1.1130,1.2989,1.1469
|
||||
Vestby,Akershus,1.1491,1.1257,1.1168,1.1544,1.1346,1.1304,1.1418,1.1404,1.1693,1.1336,1.1595,1.2146,1.1072
|
||||
Vestnes,Møre og Romsdal,0.9049,0.9352,0.9742,0.9494,0.9543,0.9795,0.9385,0.9667,0.8758,0.9633,0.7043,0.9579,1.0035
|
||||
Vestre Slidre,Innlandet,1.3316,1.2711,1.2519,1.4025,1.3582,1.2998,1.3121,1.3211,1.4424,1.2922,1.3522,1.5057,1.2471
|
||||
Vestre Toten,Innlandet,1.3316,1.2770,1.2504,1.3772,1.3462,1.3035,1.3121,1.3193,1.4131,1.2988,1.3721,1.4598,1.2401
|
||||
Vestvågøy,Nordland,1.1671,1.1100,1.1396,1.2911,1.2332,1.2160,1.1962,1.2281,1.2302,1.1786,0.9601,1.3870,1.1795
|
||||
Vevelstad,Nordland,1.2237,1.1866,1.1958,1.2937,1.2668,1.2644,1.2577,1.2596,1.2325,1.2287,1.0399,1.3678,1.2040
|
||||
Vik,Vestland,0.8123,0.8487,0.8801,0.8506,0.8221,0.8566,0.8298,0.8561,0.7856,0.8614,0.7010,0.8161,0.9149
|
||||
Vindafjord,Rogaland,0.9512,0.9823,1.0076,0.9544,0.9784,1.0000,0.9693,0.9877,0.9120,0.9950,0.7973,0.9540,1.0163
|
||||
Vinje,Telemark,1.2082,1.1591,1.1684,1.3165,1.2620,1.2216,1.2009,1.2509,1.3341,1.2154,1.1362,1.3640,1.2016
|
||||
Volda,Møre og Romsdal,0.9486,0.9548,0.9939,1.0127,0.9952,1.0074,0.9740,1.0053,0.9458,0.9933,0.7641,1.0345,1.0303
|
||||
Voss,Vestland,0.9589,0.9686,0.9863,1.0000,0.9712,0.9870,0.9669,0.9912,0.9684,0.9883,0.8937,1.0115,1.0058
|
||||
Værøy,Nordland,0.9434,0.9371,0.9954,1.0734,1.0120,1.0372,0.9882,1.0421,0.9661,1.0050,0.6412,1.1149,1.0606
|
||||
Vågan,Nordland,1.1234,1.0609,1.0986,1.2658,1.1803,1.1750,1.1560,1.1930,1.1919,1.1402,0.9070,1.3563,1.1527
|
||||
Vågå,Innlandet,1.2159,1.1670,1.1608,1.3114,1.2500,1.2067,1.1986,1.2351,1.3318,1.2037,1.2193,1.3870,1.1830
|
||||
Våler (Innlandet),Innlandet,1.2956,1.2652,1.2261,1.2886,1.2764,1.2533,1.2719,1.2632,1.3273,1.2588,1.4086,1.3678,1.1923
|
||||
Våler (Østfold),Østfold,1.0797,1.0688,1.0683,1.0835,1.0697,1.0745,1.0733,1.0807,1.0858,1.0768,1.0664,1.1149,1.0641
|
||||
Øksnes,Nordland,1.2725,1.1906,1.2155,1.3975,1.3534,1.3073,1.2955,1.3246,1.3770,1.2688,1.0565,1.5134,1.2483
|
||||
Ørland,Trøndelag,0.9692,0.9804,1.0167,1.0304,1.0120,1.0354,1.0000,1.0281,0.9571,1.0184,0.7774,1.0498,1.0490
|
||||
Ørsta,Møre og Romsdal,1.0180,1.0059,1.0395,1.0861,1.0601,1.0689,1.0426,1.0719,1.0384,1.0534,0.8439,1.1034,1.0781
|
||||
Østre Toten,Innlandet,1.3239,1.2692,1.2413,1.3696,1.3245,1.2942,1.3097,1.3140,1.3973,1.2855,1.3654,1.4521,1.2331
|
||||
Øvre Eiker,Buskerud,1.1234,1.1336,1.1017,1.0861,1.0745,1.0931,1.1087,1.0947,1.1151,1.1085,1.2292,1.1111,1.0688
|
||||
Øyer,Innlandet,1.2828,1.2318,1.2109,1.3139,1.2788,1.2551,1.2671,1.2702,1.3341,1.2487,1.3289,1.3908,1.2005
|
||||
Øygarden,Vestland,0.8972,0.9332,0.9803,0.9392,0.9423,0.9795,0.9362,0.9614,0.8375,0.9599,0.6678,0.9425,1.0070
|
||||
Øystre Slidre,Innlandet,1.3470,1.2770,1.2656,1.4481,1.4014,1.3296,1.3310,1.3632,1.5102,1.3189,1.3355,1.5441,1.2786
|
||||
Åfjord,Trøndelag,1.1003,1.0825,1.1017,1.1570,1.1394,1.1397,1.1206,1.1351,1.1219,1.1202,0.9502,1.2146,1.1142
|
||||
Ål,Buskerud,1.1954,1.1552,1.1548,1.2911,1.2428,1.1937,1.1844,1.2211,1.3115,1.1937,1.1495,1.3218,1.1818
|
||||
Ålesund,Møre og Romsdal,0.8766,0.9136,0.9666,0.9342,0.9423,0.9721,0.9173,0.9544,0.8375,0.9499,0.6113,0.9502,1.0047
|
||||
Åmli,Agder,1.0746,1.0766,1.0774,1.0886,1.0673,1.0931,1.0804,1.0912,1.0587,1.0935,1.0133,1.0996,1.0804
|
||||
Åmot,Innlandet,1.3805,1.3202,1.2883,1.4380,1.3990,1.3426,1.3546,1.3649,1.4786,1.3339,1.4352,1.5402,1.2739
|
||||
Årdal,Vestland,0.9126,0.9312,0.9454,0.9291,0.8966,0.9348,0.9267,0.9351,0.8826,0.9382,0.8638,0.9157,0.9615
|
||||
Ås,Akershus,1.1465,1.1218,1.1153,1.1696,1.1394,1.1341,1.1371,1.1491,1.1806,1.1369,1.1362,1.2146,1.1166
|
||||
Åseral,Agder,0.9820,0.9843,1.0091,1.0481,1.0024,1.0242,0.9953,1.0316,0.9819,1.0234,0.8538,1.0383,1.0455
|
||||
Åsnes,Innlandet,1.3625,1.3261,1.2807,1.3671,1.3606,1.3166,1.3357,1.3298,1.4221,1.3139,1.4718,1.4636,1.2436
|
||||
|
@@ -0,0 +1,141 @@
|
||||
# Materialdata (λ W/(mK), ρ kg/m³, c J/(kgK)) og typiske U-verdier per byggeperiode.
|
||||
# Kilder: SINTEF Byggforsk 471.010 (typiske verdier), TEK-krav 1969/1987/1997/2007/2010/2017.
|
||||
|
||||
[[materiale]]
|
||||
navn = "tre"
|
||||
lambda = 0.13
|
||||
rho = 500.0
|
||||
c = 1600.0
|
||||
[[materiale]]
|
||||
navn = "mineralull"
|
||||
lambda = 0.037
|
||||
rho = 30.0
|
||||
c = 1030.0
|
||||
[[materiale]]
|
||||
navn = "mineralull_gammel"
|
||||
lambda = 0.045
|
||||
rho = 30.0
|
||||
c = 1030.0
|
||||
[[materiale]]
|
||||
navn = "sagflis"
|
||||
lambda = 0.08
|
||||
rho = 150.0
|
||||
c = 1600.0
|
||||
[[materiale]]
|
||||
navn = "betong"
|
||||
lambda = 1.7
|
||||
rho = 2300.0
|
||||
c = 1000.0
|
||||
[[materiale]]
|
||||
navn = "lettklinker"
|
||||
lambda = 0.25
|
||||
rho = 700.0
|
||||
c = 1000.0
|
||||
[[materiale]]
|
||||
navn = "tegl"
|
||||
lambda = 0.7
|
||||
rho = 1700.0
|
||||
c = 900.0
|
||||
[[materiale]]
|
||||
navn = "gips"
|
||||
lambda = 0.25
|
||||
rho = 900.0
|
||||
c = 1000.0
|
||||
[[materiale]]
|
||||
navn = "eps"
|
||||
lambda = 0.038
|
||||
rho = 20.0
|
||||
c = 1450.0
|
||||
[[materiale]]
|
||||
navn = "xps"
|
||||
lambda = 0.034
|
||||
rho = 35.0
|
||||
c = 1450.0
|
||||
[[materiale]]
|
||||
navn = "treullsement"
|
||||
lambda = 0.09
|
||||
rho = 400.0
|
||||
c = 1600.0
|
||||
[[materiale]]
|
||||
navn = "luftspalte"
|
||||
lambda = 0.18
|
||||
rho = 1.2
|
||||
c = 1000.0 # ekvivalent for ~20 mm luftsjikt
|
||||
[[materiale]]
|
||||
navn = "papp"
|
||||
lambda = 0.2
|
||||
rho = 1000.0
|
||||
c = 1500.0
|
||||
|
||||
# Typiske U-verdier (W/m²K) for bygg uten senere etterisolering, brukt som forslag når
|
||||
# konstruksjonen ikke er kjent. [vegg, tak, gulv, vindu, kuldebro W/(m²K BRA), n50]
|
||||
[[periode]]
|
||||
navn = "før 1955"
|
||||
fra = 0
|
||||
til = 1955
|
||||
vegg = 1.0
|
||||
tak = 0.8
|
||||
gulv = 0.8
|
||||
vindu = 2.8
|
||||
kuldebro = 0.15
|
||||
n50 = 8.0
|
||||
[[periode]]
|
||||
navn = "1956–1970"
|
||||
fra = 1956
|
||||
til = 1970
|
||||
vegg = 0.6
|
||||
tak = 0.5
|
||||
gulv = 0.5
|
||||
vindu = 2.6
|
||||
kuldebro = 0.12
|
||||
n50 = 6.0
|
||||
[[periode]]
|
||||
navn = "TEK69 1971–1986"
|
||||
fra = 1971
|
||||
til = 1986
|
||||
vegg = 0.45
|
||||
tak = 0.3
|
||||
gulv = 0.4
|
||||
vindu = 2.6
|
||||
kuldebro = 0.10
|
||||
n50 = 5.0
|
||||
[[periode]]
|
||||
navn = "TEK87 1987–1997"
|
||||
fra = 1987
|
||||
til = 1997
|
||||
vegg = 0.30
|
||||
tak = 0.20
|
||||
gulv = 0.30
|
||||
vindu = 2.4
|
||||
kuldebro = 0.08
|
||||
n50 = 4.0
|
||||
[[periode]]
|
||||
navn = "TEK97 1998–2010"
|
||||
fra = 1998
|
||||
til = 2010
|
||||
vegg = 0.22
|
||||
tak = 0.15
|
||||
gulv = 0.15
|
||||
vindu = 1.6
|
||||
kuldebro = 0.05
|
||||
n50 = 3.0
|
||||
[[periode]]
|
||||
navn = "TEK10 2011–2017"
|
||||
fra = 2011
|
||||
til = 2017
|
||||
vegg = 0.18
|
||||
tak = 0.13
|
||||
gulv = 0.15
|
||||
vindu = 1.2
|
||||
kuldebro = 0.03
|
||||
n50 = 2.5
|
||||
[[periode]]
|
||||
navn = "TEK17 2018–"
|
||||
fra = 2018
|
||||
til = 9999
|
||||
vegg = 0.18
|
||||
tak = 0.13
|
||||
gulv = 0.10
|
||||
vindu = 0.8
|
||||
kuldebro = 0.05
|
||||
n50 = 0.6
|
||||
@@ -0,0 +1,56 @@
|
||||
# Normerte inndata for boligblokk.
|
||||
# Standardverdier fra NS 3031:2014 Tillegg A (Tabell A.3/A.5) slik de også fremgår av Enovas
|
||||
# XML-eksempel for energimerkesystemet. NS 3031:2025 Tillegg A gir timesprofiler over døgnet;
|
||||
# eieren av standarden kan legge inn 24-verdiers profiler i feltene `profil_*` nedenfor, som da
|
||||
# overstyrer driftstidene. Alle effekter i W per m² oppvarmet BRA.
|
||||
|
||||
kategori = "boligblokk"
|
||||
kilde = "NS 3031:2014 Tillegg A (standardverdier); NS 3031:2025-profiler kan legges inn"
|
||||
|
||||
[settpunkt]
|
||||
oppvarming = 21.0 # °C i driftstiden
|
||||
oppvarming_natt = 19.0 # °C utenfor driftstiden (NS 3031:2014: 19). Referansebyggene for
|
||||
# karakterskalaen 2026 er regnet uten nattsenking; sett lik 21.0 for det.
|
||||
kjoling = 22.0 # °C (brukes bare når kjøling er aktivert)
|
||||
ventilasjon_tilluft = 19.0 # °C; tilluft varmes til dette ved balansert ventilasjon med varmebatteri
|
||||
|
||||
[driftstid]
|
||||
# Timer per døgn og start/slutt (klokketime 0–23, slutt eksklusiv).
|
||||
oppvarming = { start = 7, slutt = 23 } # 16 h
|
||||
belysning = { start = 7, slutt = 23 } # 16 h
|
||||
utstyr = { start = 7, slutt = 23 } # 16 h
|
||||
varmtvann = { start = 7, slutt = 23 } # 16 h
|
||||
personer = { start = 0, slutt = 24 } # 24 h
|
||||
ventilasjon = { start = 0, slutt = 24 } # 24 h
|
||||
dager_per_uke = 7
|
||||
|
||||
[belysning]
|
||||
effekt = 1.95 # W/m² i driftstiden → 11.4 kWh/m²·år
|
||||
varmetilskudd = 1.95 # W/m²
|
||||
|
||||
[utstyr]
|
||||
effekt = 3.0 # W/m² i driftstiden → 17.5 kWh/m²·år
|
||||
varmetilskudd = 1.8 # W/m²
|
||||
|
||||
[varmtvann]
|
||||
# NS 3031:2025 gir 25,1 kWh/(m²·år) netto for småhus (Standard Norges lanseringseksempel,
|
||||
# Multiconsult 2025). NS 3031:2014 hadde 29,8. 4,30 W/m² i 16 h × 365 d gir 25,1 kWh/(m²·år).
|
||||
effekt = 4.30
|
||||
varmetilskudd = 0.0
|
||||
|
||||
[personer]
|
||||
varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden
|
||||
|
||||
[ventilasjon]
|
||||
luftmengde_driftstid = 1.2 # m³/(h·m²)
|
||||
luftmengde_utenfor_driftstid = 1.2 # m³/(h·m²)
|
||||
sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon
|
||||
temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon
|
||||
frostsikring_virkningsgrad = 0.80
|
||||
|
||||
[termisk]
|
||||
varmekapasitet_normalisert = 72.6 # Wh/(m²K), NS 3031:2014 «lett» bygning (bare informativt)
|
||||
kuldebroer_normalisert = 0.07 # W/(m²K) av BRA, TEK17-nivå (0.03 for passivhus, 0.10–0.15 eldre)
|
||||
|
||||
[pumper]
|
||||
effekt_vannbaaren = 0.5 # W/m² i driftstid for varmeanlegg med vannbåren distribusjon
|
||||
@@ -0,0 +1,56 @@
|
||||
# Normerte inndata for småhus.
|
||||
# Standardverdier fra NS 3031:2014 Tillegg A (Tabell A.3/A.5) slik de også fremgår av Enovas
|
||||
# XML-eksempel for energimerkesystemet. NS 3031:2025 Tillegg A gir timesprofiler over døgnet;
|
||||
# eieren av standarden kan legge inn 24-verdiers profiler i feltene `profil_*` nedenfor, som da
|
||||
# overstyrer driftstidene. Alle effekter i W per m² oppvarmet BRA.
|
||||
|
||||
kategori = "smaahus"
|
||||
kilde = "NS 3031:2014 Tillegg A (standardverdier); NS 3031:2025-profiler kan legges inn"
|
||||
|
||||
[settpunkt]
|
||||
oppvarming = 21.0 # °C i driftstiden
|
||||
oppvarming_natt = 19.0 # °C utenfor driftstiden (NS 3031:2014: 19). Referansebyggene for
|
||||
# karakterskalaen 2026 er regnet uten nattsenking; sett lik 21.0 for det.
|
||||
kjoling = 22.0 # °C (brukes bare når kjøling er aktivert)
|
||||
ventilasjon_tilluft = 19.0 # °C; tilluft varmes til dette ved balansert ventilasjon med varmebatteri
|
||||
|
||||
[driftstid]
|
||||
# Timer per døgn og start/slutt (klokketime 0–23, slutt eksklusiv).
|
||||
oppvarming = { start = 7, slutt = 23 } # 16 h
|
||||
belysning = { start = 7, slutt = 23 } # 16 h
|
||||
utstyr = { start = 7, slutt = 23 } # 16 h
|
||||
varmtvann = { start = 7, slutt = 23 } # 16 h
|
||||
personer = { start = 0, slutt = 24 } # 24 h
|
||||
ventilasjon = { start = 0, slutt = 24 } # 24 h
|
||||
dager_per_uke = 7
|
||||
|
||||
[belysning]
|
||||
effekt = 1.95 # W/m² i driftstiden → 11.4 kWh/m²·år
|
||||
varmetilskudd = 1.95 # W/m²
|
||||
|
||||
[utstyr]
|
||||
effekt = 3.0 # W/m² i driftstiden → 17.5 kWh/m²·år
|
||||
varmetilskudd = 1.8 # W/m²
|
||||
|
||||
[varmtvann]
|
||||
# NS 3031:2025 gir 25,1 kWh/(m²·år) netto for småhus (Standard Norges lanseringseksempel,
|
||||
# Multiconsult 2025). NS 3031:2014 hadde 29,8. 4,30 W/m² i 16 h × 365 d gir 25,1 kWh/(m²·år).
|
||||
effekt = 4.30
|
||||
varmetilskudd = 0.0
|
||||
|
||||
[personer]
|
||||
varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden
|
||||
|
||||
[ventilasjon]
|
||||
luftmengde_driftstid = 1.2 # m³/(h·m²)
|
||||
luftmengde_utenfor_driftstid = 1.2 # m³/(h·m²)
|
||||
sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon
|
||||
temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon
|
||||
frostsikring_virkningsgrad = 0.80
|
||||
|
||||
[termisk]
|
||||
varmekapasitet_normalisert = 72.6 # Wh/(m²K), NS 3031:2014 «lett» bygning (bare informativt)
|
||||
kuldebroer_normalisert = 0.07 # W/(m²K) av BRA, TEK17-nivå (0.03 for passivhus, 0.10–0.15 eldre)
|
||||
|
||||
[pumper]
|
||||
effekt_vannbaaren = 0.5 # W/m² i driftstid for varmeanlegg med vannbåren distribusjon
|
||||
@@ -0,0 +1,61 @@
|
||||
# Energikarakterskala gjeldende fra 1.1.2026 (Enova SF, jf. energimerkeforskriften § 10b).
|
||||
# Kilde: Multiconsult-notat "Grunnlag for ny energikarakterskala", 10257088-01-RIEn-NOT-001_REV01, 19.12.2025.
|
||||
# Verdiene er øvre grense ("lavere enn eller lik") for spesifikk vektet levert energi [kWh/m² oppvarmet BRA].
|
||||
# Arealkorreksjon legges til hver grense: grense + k/A, der A er oppvarmet BRA etter reglene nedenfor.
|
||||
# småhus: A = samlet oppvarmet BRA for hele bygningen (også ved merking av én boenhet i flerbolighus)
|
||||
# boligblokk: A = samlet oppvarmet BRA for blokken delt på antall boenheter (hel blokk),
|
||||
# eller oppvarmet BRA for boenheten (enkelt leilighet)
|
||||
|
||||
[smaahus]
|
||||
navn = "Småhus"
|
||||
grenser = [85.0, 95.0, 155.0, 210.0, 265.0, 315.0]
|
||||
arealkorreksjon = [800.0, 1600.0, 2500.0, 4100.0, 5800.0, 8000.0]
|
||||
|
||||
[boligblokk]
|
||||
navn = "Boligblokk / leilighet"
|
||||
grenser = [80.0, 95.0, 140.0, 185.0, 230.0, 270.0]
|
||||
arealkorreksjon = [600.0, 700.0, 900.0, 1100.0, 1300.0, 1500.0]
|
||||
|
||||
[barnehage]
|
||||
navn = "Barnehage"
|
||||
grenser = [105.0, 120.0, 180.0, 240.0, 300.0, 360.0]
|
||||
|
||||
[kontorbygg]
|
||||
navn = "Kontorbygning"
|
||||
grenser = [75.0, 90.0, 140.0, 190.0, 235.0, 285.0]
|
||||
|
||||
[skolebygg]
|
||||
navn = "Skolebygning"
|
||||
grenser = [70.0, 85.0, 150.0, 210.0, 275.0, 340.0]
|
||||
|
||||
[universitet]
|
||||
navn = "Universitets- og høgskolebygning"
|
||||
grenser = [75.0, 90.0, 140.0, 190.0, 245.0, 295.0]
|
||||
|
||||
[sykehus]
|
||||
navn = "Sykehus"
|
||||
grenser = [125.0, 145.0, 220.0, 300.0, 375.0, 455.0]
|
||||
|
||||
[sykehjem]
|
||||
navn = "Sykehjem"
|
||||
grenser = [95.0, 115.0, 190.0, 265.0, 340.0, 415.0]
|
||||
|
||||
[hotell]
|
||||
navn = "Hotellbygning"
|
||||
grenser = [100.0, 115.0, 195.0, 275.0, 355.0, 435.0]
|
||||
|
||||
[idrettsbygg]
|
||||
navn = "Idrettsbygning"
|
||||
grenser = [85.0, 100.0, 170.0, 235.0, 305.0, 375.0]
|
||||
|
||||
[forretningsbygg]
|
||||
navn = "Forretningsbygning"
|
||||
grenser = [110.0, 130.0, 200.0, 265.0, 330.0, 395.0]
|
||||
|
||||
[kulturbygg]
|
||||
navn = "Kulturbygning"
|
||||
grenser = [80.0, 95.0, 165.0, 230.0, 300.0, 370.0]
|
||||
|
||||
[industribygg]
|
||||
navn = "Lett industribygning, verksted"
|
||||
grenser = [95.0, 110.0, 190.0, 270.0, 345.0, 425.0]
|
||||
@@ -0,0 +1,75 @@
|
||||
# Veiledende årsgjennomsnittlig produksjonsvarmefaktor (SPF/SCOP) for varmepumper.
|
||||
# Kilde: NS 3031:2014 Tabell B.9-3 (slik gjengitt i Standard Norges lanseringsfoiler for NS 3031:2025).
|
||||
# Kolonner: småhus, boligblokk/yrkesbygg. Turtemperatur/returtemperatur angitt der det er relevant.
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "luft_luft_vp"
|
||||
beskrivelse = "Varmepumpe luft til luft, varme fra uteluft"
|
||||
smaahus = 2.5
|
||||
boligblokk = 2.6
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "avtrekk_vp"
|
||||
beskrivelse = "Varmepumpe luft til luft eller vann, varme fra avtrekksluft"
|
||||
smaahus = 2.0
|
||||
boligblokk = 2.1
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "avtrekk_vp_tappevann"
|
||||
beskrivelse = "Varmepumpe luft til vann bare for tappevannsberedning til 60 °C, varme fra avtrekksluft"
|
||||
smaahus = 1.9
|
||||
boligblokk = 1.9
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "luft_vann_vp"
|
||||
turtemperatur = 35
|
||||
beskrivelse = "Varmepumpe luft til vann 35/28 °C, varme fra uteluft"
|
||||
smaahus = 2.5
|
||||
boligblokk = 2.6
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "luft_vann_vp"
|
||||
turtemperatur = 45
|
||||
beskrivelse = "Varmepumpe luft til vann 45/37 °C, varme fra uteluft"
|
||||
smaahus = 2.3
|
||||
boligblokk = 2.4
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "luft_vann_vp"
|
||||
turtemperatur = 55
|
||||
beskrivelse = "Varmepumpe luft til vann 55/45 °C, varme fra uteluft"
|
||||
smaahus = 2.1
|
||||
boligblokk = 2.2
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "vaeske_vann_vp_tappevann"
|
||||
beskrivelse = "Varmepumpe væske til vann bare for tappevannsberedning til 60 °C, varme fra jord/berg/vann"
|
||||
smaahus = 2.3
|
||||
boligblokk = 2.4
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "vaeske_luft_vp"
|
||||
beskrivelse = "Varmepumpe væske til luft, varme fra jord/berg/vann"
|
||||
smaahus = 3.2
|
||||
boligblokk = 3.3
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "vaeske_vann_vp"
|
||||
turtemperatur = 35
|
||||
beskrivelse = "Varmepumpe væske til vann 35/28 °C, varme fra jord/berg/vann"
|
||||
smaahus = 3.2
|
||||
boligblokk = 3.3
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "vaeske_vann_vp"
|
||||
turtemperatur = 45
|
||||
beskrivelse = "Varmepumpe væske til vann 45/37 °C, varme fra jord/berg/vann"
|
||||
smaahus = 2.9
|
||||
boligblokk = 3.0
|
||||
|
||||
[[varmepumpe]]
|
||||
type = "vaeske_vann_vp"
|
||||
turtemperatur = 55
|
||||
beskrivelse = "Varmepumpe væske til vann 55/45 °C, varme fra jord/berg/vann"
|
||||
smaahus = 2.6
|
||||
boligblokk = 2.7
|
||||
@@ -0,0 +1,17 @@
|
||||
# Vektingsfaktorer for energibærere ved beregning av vektet levert energi (beregningspunkt E).
|
||||
# Kilde: energimerkeforskriften § 10a med NVEs veileder (gjeldende fra 1.1.2026).
|
||||
|
||||
[faktor]
|
||||
elektrisitet = 1.0
|
||||
fjernvarme = 0.45
|
||||
fjernkjoling = 0.45
|
||||
biobrensel_lukket = 0.45 # lukket ildsted med partikkelutslipp <= 5 g/kg (katalysator) eller <= 10 g/kg (annen teknologi)
|
||||
biobrensel_aapen = 1.0 # øvrige ildsteder
|
||||
olje = 1.0
|
||||
gass = 1.0
|
||||
annet = 1.0
|
||||
eksportert = 0.0
|
||||
|
||||
[regler]
|
||||
# Vedovn regnes å dekke inntil denne andelen av romoppvarmingsbehovet i energimerkeberegningen.
|
||||
vedovn_maks_andel_rom = 0.10
|
||||
@@ -0,0 +1,237 @@
|
||||
# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur.
|
||||
# Bygningsdata er hentet fra termografirapporten av 10.3.2016 og originale byggetegninger.
|
||||
# Kjelleren er uoppvarmet, men varmes indirekte av uisolerte varmerør.
|
||||
|
||||
[bygning]
|
||||
navn = "Nordhusstykket 5"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 1948
|
||||
oppvarmet_bra = 140.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"
|
||||
|
||||
[klima]
|
||||
kommune = "Indre Østfold"
|
||||
|
||||
# ── klimaskjerm ────────────────────────────────────────────────────────
|
||||
# Ytterveggene er stenderverk med to lag papp og rupanel. Termografien viser at det finnes
|
||||
# noe isolasjon i veggen, men mindre enn dagens krav. U-verdi anslått til 1,0 W/(m²K).
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg nord"
|
||||
type = "vegg"
|
||||
areal = 26.0
|
||||
orientering = 0
|
||||
u_verdi = 1.0
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg øst"
|
||||
type = "vegg"
|
||||
areal = 30.0
|
||||
orientering = 90
|
||||
u_verdi = 1.0
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg sør"
|
||||
type = "vegg"
|
||||
areal = 26.0
|
||||
orientering = 180
|
||||
u_verdi = 1.0
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg vest"
|
||||
type = "vegg"
|
||||
areal = 30.0
|
||||
orientering = 270
|
||||
u_verdi = 1.0
|
||||
masseklasse = "lett"
|
||||
|
||||
# Badet ble bygget om i 1985 med 10 cm isolasjon i ytterveggene.
|
||||
[[flate]]
|
||||
navn = "Yttervegg bad (1985)"
|
||||
type = "vegg"
|
||||
areal = 12.0
|
||||
orientering = 90
|
||||
u_verdi = 0.40
|
||||
masseklasse = "lett"
|
||||
kommentar = "Takopplett med bad, 10 cm isolasjon"
|
||||
|
||||
[[flate]]
|
||||
navn = "Skråtak og knevegg mot loft"
|
||||
type = "tak"
|
||||
areal = 62.0
|
||||
helning = 40
|
||||
u_verdi = 0.55
|
||||
masseklasse = "lett"
|
||||
kommentar = "Termografien viser luftlekkasje til loft og gjennom pipen"
|
||||
|
||||
[[flate]]
|
||||
navn = "Himling mot kaldt loft"
|
||||
type = "tak"
|
||||
areal = 38.0
|
||||
helning = 0
|
||||
u_verdi = 0.45
|
||||
mot = "uoppvarmet"
|
||||
b_faktor = 0.9
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Etasjeskiller mot uoppvarmet kjeller"
|
||||
type = "gulv"
|
||||
areal = 98.0
|
||||
u_verdi = 0.80
|
||||
mot = "uoppvarmet"
|
||||
b_faktor = 0.7
|
||||
masseklasse = "middels"
|
||||
kommentar = "Kjelleren varmes av uisolerte varmerør, så temperaturfaktoren er høy"
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer nord"
|
||||
areal = 3.0
|
||||
orientering = 0
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer øst"
|
||||
areal = 4.5
|
||||
orientering = 90
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer sør"
|
||||
areal = 6.0
|
||||
orientering = 180
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer vest"
|
||||
areal = 4.5
|
||||
orientering = 270
|
||||
u_verdi = 2.80
|
||||
g_verdi = 0.76
|
||||
|
||||
[[vindu]]
|
||||
navn = "Inngangsdør med vindfang"
|
||||
areal = 2.0
|
||||
orientering = 270
|
||||
u_verdi = 2.00
|
||||
dor = true
|
||||
|
||||
[[vindu]]
|
||||
navn = "Balkongdør sør"
|
||||
areal = 2.0
|
||||
orientering = 180
|
||||
u_verdi = 2.60
|
||||
g_verdi = 0.76
|
||||
|
||||
# Utkragede bjelker som bærer balkongen er en tydelig kuldebro i termografien.
|
||||
[kuldebroer]
|
||||
normalisert = 0.15
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 8.0
|
||||
skjerming = "moderat"
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig"
|
||||
luftmengde = 1.2
|
||||
|
||||
[tappevann]
|
||||
system = "aapent"
|
||||
|
||||
# Vannbårent radiatoranlegg med elektrokjel, supplert med vedovn.
|
||||
[[oppvarming.kilde]]
|
||||
type = "vedovn"
|
||||
andel_rom = 0.25
|
||||
lukket = false
|
||||
kommentar = "Åpen peis/eldre ovn; energimerkeforskriften regner inntil 10 % av romoppvarmingen"
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel"
|
||||
andel_rom = 0.75
|
||||
andel_tappevann = 1.0
|
||||
|
||||
[distribusjon]
|
||||
type = "radiator"
|
||||
turtemperatur = 60
|
||||
roer_uoppvarmet = 0.45
|
||||
kommentar = "Uisolerte varmerør gjennom uoppvarmet kjeller"
|
||||
|
||||
# ── tiltak ─────────────────────────────────────────────────────────────
|
||||
|
||||
[[scenario]]
|
||||
navn = "Isolerte varmerør og kjellervegger"
|
||||
beskrivelse = "Isolering av varmerørene gjennom kjelleren og av kjellerveggene, som termografirapporten anbefaler."
|
||||
endringer.distribusjon = { roer_uoppvarmet = 0.10 }
|
||||
endringer.flate = [{ navn = "Etasjeskiller mot uoppvarmet kjeller", b_faktor = 0.5 }]
|
||||
|
||||
[[scenario]]
|
||||
navn = "Nye vinduer"
|
||||
beskrivelse = "Utskifting til trelags energiglass med U-verdi 0,8 W/(m²K)."
|
||||
endringer.vindu = [
|
||||
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Balkongdør sør", u_verdi = 1.20, g_verdi = 0.50 },
|
||||
]
|
||||
|
||||
[[scenario]]
|
||||
navn = "Etterisolert klimaskjerm"
|
||||
beskrivelse = "Etterisolering av yttervegger til 20 cm og loft til 30 cm, med tetting av luftlekkasjer."
|
||||
endringer.flate = [
|
||||
{ navn = "Yttervegg nord", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg øst", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg sør", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg vest", u_verdi = 0.22 },
|
||||
{ navn = "Skråtak og knevegg mot loft", u_verdi = 0.18 },
|
||||
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
|
||||
]
|
||||
endringer.infiltrasjon = { n50 = 4.0 }
|
||||
endringer.kuldebroer = { normalisert = 0.10 }
|
||||
|
||||
[[scenario]]
|
||||
navn = "Væske-vann-varmepumpe"
|
||||
beskrivelse = "Bergvarmepumpe på det vannbårne anlegget, med elektrokjel som spisslast."
|
||||
endringer.oppvarming.kilde = [
|
||||
{ type = "vedovn", slett = true },
|
||||
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 55 },
|
||||
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
|
||||
]
|
||||
|
||||
[[scenario]]
|
||||
navn = "Full pakke"
|
||||
beskrivelse = "Alle tiltakene over samlet: isolerte rør og kjeller, nye vinduer, etterisolert klimaskjerm, balansert ventilasjon med varmegjenvinning og bergvarmepumpe."
|
||||
endringer.distribusjon = { roer_uoppvarmet = 0.10, turtemperatur = 45 }
|
||||
endringer.infiltrasjon = { n50 = 2.5 }
|
||||
endringer.kuldebroer = { normalisert = 0.09 }
|
||||
endringer.ventilasjon = { type = "balansert", gjenvinning = 0.80, sfp = 1.5 }
|
||||
endringer.flate = [
|
||||
{ navn = "Yttervegg nord", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg øst", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg sør", u_verdi = 0.22 },
|
||||
{ navn = "Yttervegg vest", u_verdi = 0.22 },
|
||||
{ navn = "Skråtak og knevegg mot loft", u_verdi = 0.18 },
|
||||
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
|
||||
{ navn = "Etasjeskiller mot uoppvarmet kjeller", u_verdi = 0.30, b_faktor = 0.5 },
|
||||
]
|
||||
endringer.vindu = [
|
||||
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
|
||||
{ navn = "Balkongdør sør", u_verdi = 1.20, g_verdi = 0.50 },
|
||||
]
|
||||
endringer.oppvarming.kilde = [
|
||||
{ type = "vedovn", slett = true },
|
||||
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 45 },
|
||||
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
# Referansebygget for småhus med TEK17-standard, brukt til å kontrollere beregningen mot
|
||||
# Standard Norges lanseringseksempel for NS 3031:2025 (Multiconsult, mai 2025).
|
||||
#
|
||||
# Forventet resultat i Oslo-klima, direkte elektrisk oppvarming:
|
||||
# varmetapstall totalt 0,69 W/(m²K)
|
||||
# netto energibehov 106,4 kWh/(m²·år), hvorav romoppvarming 46,2 og ventilasjonsvarme 2,3
|
||||
# energikarakter B
|
||||
|
||||
[bygning]
|
||||
navn = "TEK17-referansehus"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 2020
|
||||
oppvarmet_bra = 160.0
|
||||
oppvarmet_volum = 432.0
|
||||
beskrivelse = "Modellbygg med TEK17-standard på bygningskropp og tekniske anlegg"
|
||||
|
||||
[klima]
|
||||
kommune = "Oslo"
|
||||
|
||||
# ── klimaskjerm ────────────────────────────────────────────────────────
|
||||
# Arealene er valgt slik at varmetapstallene treffer TEK17-energirammen:
|
||||
# vegger 0,22 · tak 0,07 · gulv 0,05 · vinduer og dører 0,20 · kuldebroer 0,05 W/(m²K).
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg nord"
|
||||
type = "vegg"
|
||||
areal = 48.9
|
||||
orientering = 0
|
||||
u_verdi = 0.18
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg øst"
|
||||
type = "vegg"
|
||||
areal = 48.9
|
||||
orientering = 90
|
||||
u_verdi = 0.18
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg sør"
|
||||
type = "vegg"
|
||||
areal = 48.9
|
||||
orientering = 180
|
||||
u_verdi = 0.18
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg vest"
|
||||
type = "vegg"
|
||||
areal = 48.9
|
||||
orientering = 270
|
||||
u_verdi = 0.18
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 86.2
|
||||
helning = 0
|
||||
u_verdi = 0.13
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Gulv mot grunn"
|
||||
type = "gulv"
|
||||
areal = 80.0
|
||||
u_verdi = 0.10
|
||||
mot = "grunn"
|
||||
masseklasse = "tung"
|
||||
|
||||
# Vindusarealet er 25 % av oppvarmet BRA, som er TEK17-rammen, fordelt likt på fire fasader.
|
||||
[[vindu]]
|
||||
navn = "Vinduer nord"
|
||||
areal = 10.0
|
||||
orientering = 0
|
||||
u_verdi = 0.80
|
||||
g_verdi = 0.50
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer øst"
|
||||
areal = 10.0
|
||||
orientering = 90
|
||||
u_verdi = 0.80
|
||||
g_verdi = 0.50
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer sør"
|
||||
areal = 10.0
|
||||
orientering = 180
|
||||
u_verdi = 0.80
|
||||
g_verdi = 0.50
|
||||
|
||||
[[vindu]]
|
||||
navn = "Vinduer vest"
|
||||
areal = 10.0
|
||||
orientering = 270
|
||||
u_verdi = 0.80
|
||||
g_verdi = 0.50
|
||||
|
||||
[kuldebroer]
|
||||
normalisert = 0.05
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 0.6
|
||||
skjerming = "moderat"
|
||||
|
||||
[ventilasjon]
|
||||
type = "balansert"
|
||||
luftmengde = 1.2
|
||||
gjenvinning = 0.85
|
||||
sfp = 1.5
|
||||
|
||||
[tappevann]
|
||||
system = "aapent"
|
||||
|
||||
# TEK17-referansen for småhus forutsetter direkte elektrisk oppvarming.
|
||||
[[oppvarming.kilde]]
|
||||
type = "direkte_el"
|
||||
andel_rom = 1.0
|
||||
andel_ventilasjon = 1.0
|
||||
andel_tappevann = 1.0
|
||||
|
||||
[distribusjon]
|
||||
type = "punkt"
|
||||
|
||||
[drift]
|
||||
# Referansebyggene for karakterskalaen er regnet uten nattsenking.
|
||||
nattsenking = 21.0
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
//! Innebygde datafiler (normerte verdier, karakterskala, vekting, SPF, materialer, klimakorreksjon).
|
||||
//! Filene ligger i `data/` og bygges inn i binæren; de kan overstyres med `--data DIR`.
|
||||
|
||||
use crate::modell::Kategori;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const SKALA: &str = include_str!("../data/skala.toml");
|
||||
const VEKTING: &str = include_str!("../data/vekting.toml");
|
||||
const SPF: &str = include_str!("../data/varmepumpe_spf.toml");
|
||||
const MATERIALER: &str = include_str!("../data/materialer.toml");
|
||||
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
|
||||
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
|
||||
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
|
||||
|
||||
/// Katalog som kan overstyre de innebygde datafilene.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Datakilde {
|
||||
pub katalog: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Datakilde {
|
||||
pub fn ny(katalog: Option<PathBuf>) -> Self {
|
||||
Self { katalog }
|
||||
}
|
||||
|
||||
fn les(&self, relativ: &str, innebygd: &str) -> Result<String> {
|
||||
if let Some(k) = &self.katalog {
|
||||
let p = k.join(relativ);
|
||||
if p.exists() {
|
||||
return std::fs::read_to_string(&p)
|
||||
.with_context(|| format!("leser {}", p.display()));
|
||||
}
|
||||
}
|
||||
Ok(innebygd.to_string())
|
||||
}
|
||||
|
||||
pub fn skala(&self) -> Result<Skala> {
|
||||
let s = self.les("skala.toml", SKALA)?;
|
||||
let m: HashMap<String, SkalaKategori> = toml::from_str(&s).context("skala.toml")?;
|
||||
Ok(Skala(m))
|
||||
}
|
||||
|
||||
pub fn vekting(&self) -> Result<Vekting> {
|
||||
let s = self.les("vekting.toml", VEKTING)?;
|
||||
toml::from_str(&s).context("vekting.toml")
|
||||
}
|
||||
|
||||
pub fn varmepumper(&self) -> Result<Vec<VarmepumpeSpf>> {
|
||||
let s = self.les("varmepumpe_spf.toml", SPF)?;
|
||||
let t: SpfFil = toml::from_str(&s).context("varmepumpe_spf.toml")?;
|
||||
Ok(t.varmepumpe)
|
||||
}
|
||||
|
||||
pub fn materialer(&self) -> Result<Materialer> {
|
||||
let s = self.les("materialer.toml", MATERIALER)?;
|
||||
toml::from_str(&s).context("materialer.toml")
|
||||
}
|
||||
|
||||
pub fn normerte(&self, kategori: Kategori) -> Result<Normerte> {
|
||||
let (fil, innebygd) = match kategori {
|
||||
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
|
||||
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
|
||||
annen => {
|
||||
// Andre kategorier krever egen datafil i data-katalogen.
|
||||
let fil = format!("normerte/{}.toml", annen.nokkel());
|
||||
let p = self
|
||||
.katalog
|
||||
.as_ref()
|
||||
.map(|k| k.join(&fil))
|
||||
.filter(|p| p.exists())
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"normerte inndata for kategori «{}» mangler: legg {} i data-katalogen",
|
||||
annen.nokkel(),
|
||||
fil
|
||||
)
|
||||
})?;
|
||||
let s = std::fs::read_to_string(&p)?;
|
||||
return toml::from_str(&s).with_context(|| p.display().to_string());
|
||||
}
|
||||
};
|
||||
let s = self.les(fil, innebygd)?;
|
||||
toml::from_str(&s).context(fil)
|
||||
}
|
||||
|
||||
pub fn klimakorreksjon(&self) -> Result<Klimakorreksjon> {
|
||||
let s = self.les("klimakorreksjon.csv", KLIMAKORREKSJON)?;
|
||||
Klimakorreksjon::fra_csv(&s)
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── karakterskala ─────────────────────────────
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct SkalaKategori {
|
||||
pub navn: String,
|
||||
/// Øvre grense for A, B, C, D, E, F (kWh/m²). G har ingen grense.
|
||||
pub grenser: [f64; 6],
|
||||
/// Arealkorreksjon k for hver grense: grense + k / A.
|
||||
#[serde(default)]
|
||||
pub arealkorreksjon: Option<[f64; 6]>,
|
||||
}
|
||||
|
||||
pub struct Skala(pub HashMap<String, SkalaKategori>);
|
||||
|
||||
impl Skala {
|
||||
pub fn for_kategori(&self, k: Kategori) -> Result<&SkalaKategori> {
|
||||
self.0
|
||||
.get(k.nokkel())
|
||||
.ok_or_else(|| anyhow!("karakterskala mangler for {}", k.nokkel()))
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── vekting ─────────────────────────────
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Vekting {
|
||||
pub faktor: Vektingsfaktorer,
|
||||
pub regler: Vektingsregler,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Vektingsfaktorer {
|
||||
pub elektrisitet: f64,
|
||||
pub fjernvarme: f64,
|
||||
pub fjernkjoling: f64,
|
||||
pub biobrensel_lukket: f64,
|
||||
pub biobrensel_aapen: f64,
|
||||
pub olje: f64,
|
||||
pub gass: f64,
|
||||
pub annet: f64,
|
||||
pub eksportert: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Vektingsregler {
|
||||
pub vedovn_maks_andel_rom: f64,
|
||||
}
|
||||
|
||||
// ───────────────────────────── varmepumper ─────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SpfFil {
|
||||
varmepumpe: Vec<VarmepumpeSpf>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct VarmepumpeSpf {
|
||||
#[serde(rename = "type")]
|
||||
pub typ: String,
|
||||
#[serde(default)]
|
||||
pub turtemperatur: Option<f64>,
|
||||
pub beskrivelse: String,
|
||||
pub smaahus: f64,
|
||||
pub boligblokk: f64,
|
||||
}
|
||||
|
||||
/// Slår opp veiledende SPF for en varmepumpetype. Ved turtemperatur velges nærmeste tabellverdi.
|
||||
pub fn spf_oppslag(
|
||||
tabell: &[VarmepumpeSpf],
|
||||
typ: &str,
|
||||
turtemp: Option<f64>,
|
||||
kategori: Kategori,
|
||||
) -> Option<f64> {
|
||||
let mut best: Option<(f64, &VarmepumpeSpf)> = None;
|
||||
for rad in tabell.iter().filter(|r| r.typ == typ) {
|
||||
let avstand = match (rad.turtemperatur, turtemp) {
|
||||
(Some(t), Some(v)) => (t - v).abs(),
|
||||
(Some(t), None) => (t - 45.0).abs(), // uten oppgitt turtemperatur: radiatoranlegg 45/37
|
||||
_ => 0.0,
|
||||
};
|
||||
if best.is_none_or(|(a, _)| avstand < a) {
|
||||
best = Some((avstand, rad));
|
||||
}
|
||||
}
|
||||
best.map(|(_, r)| match kategori {
|
||||
Kategori::Smaahus => r.smaahus,
|
||||
_ => r.boligblokk,
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────── materialer ─────────────────────────────
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Materialer {
|
||||
pub materiale: Vec<Materiale>,
|
||||
pub periode: Vec<Periode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Materiale {
|
||||
pub navn: String,
|
||||
pub lambda: f64,
|
||||
pub rho: f64,
|
||||
pub c: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Periode {
|
||||
pub navn: String,
|
||||
pub fra: i32,
|
||||
pub til: i32,
|
||||
pub vegg: f64,
|
||||
pub tak: f64,
|
||||
pub gulv: f64,
|
||||
pub vindu: f64,
|
||||
pub kuldebro: f64,
|
||||
pub n50: f64,
|
||||
}
|
||||
|
||||
impl Materialer {
|
||||
pub fn materiale(&self, navn: &str) -> Option<&Materiale> {
|
||||
self.materiale
|
||||
.iter()
|
||||
.find(|m| m.navn.eq_ignore_ascii_case(navn))
|
||||
}
|
||||
pub fn periode_for(&self, byggeaar: i32) -> Option<&Periode> {
|
||||
self.periode
|
||||
.iter()
|
||||
.find(|p| byggeaar >= p.fra && byggeaar <= p.til)
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────── normerte inndata ─────────────────────────────
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Normerte {
|
||||
pub kategori: String,
|
||||
#[serde(default)]
|
||||
pub kilde: String,
|
||||
pub settpunkt: Settpunkt,
|
||||
pub driftstid: Driftstider,
|
||||
pub belysning: EffektPost,
|
||||
pub utstyr: EffektPost,
|
||||
pub varmtvann: EffektPost,
|
||||
pub personer: PersonPost,
|
||||
pub ventilasjon: VentilasjonNormert,
|
||||
pub termisk: TermiskNormert,
|
||||
pub pumper: PumperNormert,
|
||||
/// Valgfrie 24-verdiers profiler (W/m² per klokketime) som overstyrer driftstid × effekt.
|
||||
#[serde(default)]
|
||||
pub profil_belysning: Option<[f64; 24]>,
|
||||
#[serde(default)]
|
||||
pub profil_utstyr: Option<[f64; 24]>,
|
||||
#[serde(default)]
|
||||
pub profil_varmtvann: Option<[f64; 24]>,
|
||||
#[serde(default)]
|
||||
pub profil_personer: Option<[f64; 24]>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Settpunkt {
|
||||
pub oppvarming: f64,
|
||||
pub oppvarming_natt: f64,
|
||||
pub kjoling: f64,
|
||||
pub ventilasjon_tilluft: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct Driftstider {
|
||||
pub oppvarming: Tidsrom,
|
||||
pub belysning: Tidsrom,
|
||||
pub utstyr: Tidsrom,
|
||||
pub varmtvann: Tidsrom,
|
||||
pub personer: Tidsrom,
|
||||
pub ventilasjon: Tidsrom,
|
||||
#[serde(default = "sju")]
|
||||
pub dager_per_uke: u8,
|
||||
}
|
||||
|
||||
fn sju() -> u8 {
|
||||
7
|
||||
}
|
||||
|
||||
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
|
||||
pub struct Tidsrom {
|
||||
pub start: u8,
|
||||
pub slutt: u8,
|
||||
}
|
||||
|
||||
impl Tidsrom {
|
||||
pub fn aktiv(&self, klokketime: usize) -> bool {
|
||||
let h = klokketime as u8;
|
||||
if self.start <= self.slutt {
|
||||
h >= self.start && h < self.slutt
|
||||
} else {
|
||||
h >= self.start || h < self.slutt
|
||||
}
|
||||
}
|
||||
pub fn timer(&self) -> f64 {
|
||||
if self.start <= self.slutt {
|
||||
(self.slutt - self.start) as f64
|
||||
} else {
|
||||
(24 + self.slutt - self.start) as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct EffektPost {
|
||||
pub effekt: f64,
|
||||
pub varmetilskudd: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct PersonPost {
|
||||
pub varmetilskudd: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct VentilasjonNormert {
|
||||
pub luftmengde_driftstid: f64,
|
||||
pub luftmengde_utenfor_driftstid: f64,
|
||||
pub sfp: f64,
|
||||
pub temperaturvirkningsgrad: f64,
|
||||
pub frostsikring_virkningsgrad: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct TermiskNormert {
|
||||
pub varmekapasitet_normalisert: f64,
|
||||
pub kuldebroer_normalisert: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct PumperNormert {
|
||||
pub effekt_vannbaaren: f64,
|
||||
}
|
||||
|
||||
// ───────────────────────────── klimakorreksjon ─────────────────────────────
|
||||
|
||||
/// Klimakorrigeringsfaktorer KF per klimasted og bygningskategori (NS 3031:2025, tillegg).
|
||||
pub struct Klimakorreksjon {
|
||||
pub kategorier: Vec<String>,
|
||||
pub steder: Vec<(String, String, Vec<f64>)>,
|
||||
}
|
||||
|
||||
impl Klimakorreksjon {
|
||||
fn fra_csv(s: &str) -> Result<Self> {
|
||||
let mut rdr = csv::ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.from_reader(s.as_bytes());
|
||||
let kategorier: Vec<String> = rdr
|
||||
.headers()?
|
||||
.iter()
|
||||
.skip(2)
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let mut steder = Vec::new();
|
||||
for rad in rdr.records() {
|
||||
let rad = rad?;
|
||||
let sted = rad.get(0).unwrap_or("").to_string();
|
||||
let fylke = rad.get(1).unwrap_or("").to_string();
|
||||
let verdier: Result<Vec<f64>, _> =
|
||||
rad.iter().skip(2).map(|v| v.parse::<f64>()).collect();
|
||||
steder.push((
|
||||
sted,
|
||||
fylke,
|
||||
verdier.context("klimakorreksjon.csv: tallverdi")?,
|
||||
));
|
||||
}
|
||||
Ok(Self { kategorier, steder })
|
||||
}
|
||||
|
||||
/// KF for et klimasted og en kategori. Navnet sammenlignes normalisert (æøå → ae/o/aa).
|
||||
pub fn faktor(&self, sted: &str, kategori: Kategori) -> Option<f64> {
|
||||
let kol = self
|
||||
.kategorier
|
||||
.iter()
|
||||
.position(|k| k == kategori.nokkel())?;
|
||||
let mål = normaliser_stedsnavn(sted);
|
||||
self.steder
|
||||
.iter()
|
||||
.find(|(s, _, _)| normaliser_stedsnavn(s) == mål)
|
||||
.map(|(_, _, v)| v[kol])
|
||||
}
|
||||
}
|
||||
|
||||
/// Normaliserer stedsnavn for sammenligning: små bokstaver, æ→ae, ø→o, å→aa, uten parentes-fylke.
|
||||
pub fn normaliser_stedsnavn(s: &str) -> String {
|
||||
let s = s.split('(').next().unwrap_or(s).trim();
|
||||
s.chars()
|
||||
.flat_map(|c| match c.to_ascii_lowercase() {
|
||||
'æ' => "ae".chars().collect::<Vec<_>>(),
|
||||
'ø' => "o".chars().collect(),
|
||||
'å' => "aa".chars().collect(),
|
||||
'Æ' => "ae".chars().collect(),
|
||||
'Ø' => "o".chars().collect(),
|
||||
'Å' => "aa".chars().collect(),
|
||||
'-' | ' ' => vec![],
|
||||
c => vec![c],
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn finnes(p: &Path) -> bool {
|
||||
p.exists()
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
//! Import av gbXML fra BIM-verktøy (Vectorworks, Archicad, Revit m.fl.).
|
||||
//!
|
||||
//! gbXML er formatet Vectorworks Architect eksporterer for energianalyse, og inneholder rom,
|
||||
//! flater med himmelretning og helning, åpninger, konstruksjoner og vindustyper. Importen
|
||||
//! oversetter dette til en `bygning.toml` som kan fylles ut med det gbXML ikke beskriver
|
||||
//! (varmekilder, ventilasjon, lekkasjetall, kommune).
|
||||
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::{anyhow, Context};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Material {
|
||||
tykkelse: f64,
|
||||
ledningsevne: f64,
|
||||
r_verdi: f64,
|
||||
densitet: f64,
|
||||
varmekapasitet: f64,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Konstruksjon {
|
||||
navn: String,
|
||||
u_verdi: Option<f64>,
|
||||
lag: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Vindustype {
|
||||
navn: String,
|
||||
u_verdi: Option<f64>,
|
||||
shgc: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Aapning {
|
||||
navn: String,
|
||||
typ: String,
|
||||
vindustype: String,
|
||||
bredde: f64,
|
||||
hoyde: f64,
|
||||
areal: Option<f64>,
|
||||
polygon: Vec<[f64; 3]>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Overflate {
|
||||
navn: String,
|
||||
typ: String,
|
||||
konstruksjon: String,
|
||||
asimut: Option<f64>,
|
||||
helning: Option<f64>,
|
||||
bredde: f64,
|
||||
hoyde: f64,
|
||||
areal: Option<f64>,
|
||||
polygon: Vec<[f64; 3]>,
|
||||
naborom: Vec<String>,
|
||||
aapninger: Vec<Aapning>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Rom {
|
||||
areal: f64,
|
||||
volum: f64,
|
||||
}
|
||||
|
||||
/// Rapport fra importen: hva som ble lest og hva som må fylles ut manuelt.
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
fra_tekst(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut leser = Reader::from_str(tekst);
|
||||
leser.config_mut().trim_text(true);
|
||||
|
||||
let mut materialer: HashMap<String, Material> = HashMap::new();
|
||||
let mut lag: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut konstruksjoner: HashMap<String, Konstruksjon> = HashMap::new();
|
||||
let mut vindustyper: HashMap<String, Vindustype> = HashMap::new();
|
||||
let mut overflater: Vec<Overflate> = Vec::new();
|
||||
let mut rom: Vec<Rom> = Vec::new();
|
||||
|
||||
// Gjeldende kontekst under parsingen.
|
||||
let mut sti_elementer: Vec<String> = Vec::new();
|
||||
let mut gjeldende_material: Option<(String, Material)> = None;
|
||||
let mut gjeldende_lag: Option<(String, Vec<String>)> = None;
|
||||
let mut gjeldende_konstruksjon: Option<(String, Konstruksjon)> = None;
|
||||
let mut gjeldende_vindustype: Option<(String, Vindustype)> = None;
|
||||
let mut gjeldende_overflate: Option<Overflate> = None;
|
||||
let mut gjeldende_aapning: Option<Aapning> = None;
|
||||
let mut gjeldende_rom: Option<Rom> = None;
|
||||
let mut gjeldende_punkt: Vec<f64> = Vec::new();
|
||||
let mut i_planar = false;
|
||||
let mut bygningsnavn: Option<String> = None;
|
||||
let mut bygningstype: Option<String> = None;
|
||||
let mut tekstbuffer = String::new();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match leser.read_event_into(&mut buf) {
|
||||
Err(e) => return Err(anyhow!("gbXML er ikke gyldig XML: {e}")),
|
||||
Ok(Event::Eof) => break,
|
||||
Ok(Event::Start(e)) => {
|
||||
let navn = lokalt_navn(e.name().as_ref());
|
||||
let attr = |n: &str| -> Option<String> {
|
||||
e.attributes().flatten().find_map(|a| {
|
||||
if lokalt_navn(a.key.as_ref()) == n {
|
||||
Some(String::from_utf8_lossy(&a.value).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
match navn.as_str() {
|
||||
"Material" => {
|
||||
gjeldende_material =
|
||||
Some((attr("id").unwrap_or_default(), Material::default()))
|
||||
}
|
||||
"Layer" => gjeldende_lag = Some((attr("id").unwrap_or_default(), Vec::new())),
|
||||
"Construction" => {
|
||||
gjeldende_konstruksjon =
|
||||
Some((attr("id").unwrap_or_default(), Konstruksjon::default()))
|
||||
}
|
||||
"WindowType" => {
|
||||
gjeldende_vindustype =
|
||||
Some((attr("id").unwrap_or_default(), Vindustype::default()))
|
||||
}
|
||||
"Building" => {
|
||||
bygningstype = attr("buildingType");
|
||||
}
|
||||
"Space" => gjeldende_rom = Some(Rom::default()),
|
||||
"Surface" => {
|
||||
gjeldende_overflate = Some(Overflate {
|
||||
navn: attr("id").unwrap_or_default(),
|
||||
typ: attr("surfaceType").unwrap_or_default(),
|
||||
konstruksjon: attr("constructionIdRef").unwrap_or_default(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
"Opening" => {
|
||||
gjeldende_aapning = Some(Aapning {
|
||||
navn: attr("id").unwrap_or_default(),
|
||||
typ: attr("openingType").unwrap_or_default(),
|
||||
vindustype: attr("windowTypeIdRef").unwrap_or_default(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
"PlanarGeometry" => i_planar = true,
|
||||
"CartesianPoint" => gjeldende_punkt.clear(),
|
||||
_ => {}
|
||||
}
|
||||
sti_elementer.push(navn);
|
||||
tekstbuffer.clear();
|
||||
}
|
||||
Ok(Event::Empty(e)) => {
|
||||
let navn = lokalt_navn(e.name().as_ref());
|
||||
let attr = |n: &str| -> Option<String> {
|
||||
e.attributes().flatten().find_map(|a| {
|
||||
if lokalt_navn(a.key.as_ref()) == n {
|
||||
Some(String::from_utf8_lossy(&a.value).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
match navn.as_str() {
|
||||
"MaterialId" => {
|
||||
if let (Some((_, l)), Some(r)) =
|
||||
(gjeldende_lag.as_mut(), attr("materialIdRef"))
|
||||
{
|
||||
l.push(r);
|
||||
}
|
||||
}
|
||||
"LayerId" => {
|
||||
if let (Some((_, k)), Some(r)) =
|
||||
(gjeldende_konstruksjon.as_mut(), attr("layerIdRef"))
|
||||
{
|
||||
k.lag.push(r);
|
||||
}
|
||||
}
|
||||
"AdjacentSpaceId" => {
|
||||
if let (Some(o), Some(r)) =
|
||||
(gjeldende_overflate.as_mut(), attr("spaceIdRef"))
|
||||
{
|
||||
o.naborom.push(r);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Event::Text(t)) => {
|
||||
tekstbuffer = t.unescape().unwrap_or_default().to_string();
|
||||
}
|
||||
Ok(Event::End(e)) => {
|
||||
let navn = lokalt_navn(e.name().as_ref());
|
||||
let tall = tekstbuffer.trim().parse::<f64>().ok();
|
||||
let i_aapning = gjeldende_aapning.is_some();
|
||||
match navn.as_str() {
|
||||
"Thickness" => sett(&mut gjeldende_material, |m| {
|
||||
m.tykkelse = tall.unwrap_or(0.0)
|
||||
}),
|
||||
"Conductivity" => sett(&mut gjeldende_material, |m| {
|
||||
m.ledningsevne = tall.unwrap_or(0.0)
|
||||
}),
|
||||
"Density" => sett(&mut gjeldende_material, |m| {
|
||||
m.densitet = tall.unwrap_or(0.0)
|
||||
}),
|
||||
"SpecificHeat" => sett(&mut gjeldende_material, |m| {
|
||||
m.varmekapasitet = tall.unwrap_or(0.0)
|
||||
}),
|
||||
"R-value" => sett(&mut gjeldende_material, |m| m.r_verdi = tall.unwrap_or(0.0)),
|
||||
"U-value" => {
|
||||
if let Some((_, v)) = gjeldende_vindustype.as_mut() {
|
||||
v.u_verdi = tall;
|
||||
} else if let Some((_, k)) = gjeldende_konstruksjon.as_mut() {
|
||||
k.u_verdi = tall;
|
||||
}
|
||||
}
|
||||
"SolarHeatGainCoeff" => {
|
||||
if let Some((_, v)) = gjeldende_vindustype.as_mut() {
|
||||
v.shgc = tall;
|
||||
}
|
||||
}
|
||||
"Name" => {
|
||||
if let Some((_, v)) = gjeldende_vindustype.as_mut() {
|
||||
v.navn = tekstbuffer.clone();
|
||||
} else if let Some((_, k)) = gjeldende_konstruksjon.as_mut() {
|
||||
k.navn = tekstbuffer.clone();
|
||||
} else if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
if !tekstbuffer.is_empty() {
|
||||
o.navn = tekstbuffer.clone();
|
||||
}
|
||||
} else if sti_elementer.iter().rev().nth(1).map(|s| s.as_str())
|
||||
== Some("Building")
|
||||
{
|
||||
bygningsnavn = Some(tekstbuffer.clone());
|
||||
}
|
||||
}
|
||||
"Azimuth" => {
|
||||
if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.asimut = tall;
|
||||
}
|
||||
}
|
||||
"Tilt" => {
|
||||
if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.helning = tall;
|
||||
}
|
||||
}
|
||||
"Width" => {
|
||||
if let Some(a) = gjeldende_aapning.as_mut() {
|
||||
a.bredde = tall.unwrap_or(0.0);
|
||||
} else if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.bredde = tall.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
"Height" => {
|
||||
if let Some(a) = gjeldende_aapning.as_mut() {
|
||||
a.hoyde = tall.unwrap_or(0.0);
|
||||
} else if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.hoyde = tall.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
"Area" => {
|
||||
if let Some(a) = gjeldende_aapning.as_mut() {
|
||||
a.areal = tall;
|
||||
} else if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.areal = tall;
|
||||
} else if let Some(r) = gjeldende_rom.as_mut() {
|
||||
r.areal = tall.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
"Volume" => {
|
||||
if let Some(r) = gjeldende_rom.as_mut() {
|
||||
r.volum = tall.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
"Coordinate" => {
|
||||
if let Some(v) = tall {
|
||||
gjeldende_punkt.push(v);
|
||||
}
|
||||
}
|
||||
"CartesianPoint" => {
|
||||
if i_planar && gjeldende_punkt.len() >= 3 {
|
||||
let p = [gjeldende_punkt[0], gjeldende_punkt[1], gjeldende_punkt[2]];
|
||||
if i_aapning {
|
||||
if let Some(a) = gjeldende_aapning.as_mut() {
|
||||
a.polygon.push(p);
|
||||
}
|
||||
} else if let Some(o) = gjeldende_overflate.as_mut() {
|
||||
o.polygon.push(p);
|
||||
}
|
||||
}
|
||||
gjeldende_punkt.clear();
|
||||
}
|
||||
"PlanarGeometry" => i_planar = false,
|
||||
"Material" => {
|
||||
if let Some((id, m)) = gjeldende_material.take() {
|
||||
materialer.insert(id, m);
|
||||
}
|
||||
}
|
||||
"Layer" => {
|
||||
if let Some((id, l)) = gjeldende_lag.take() {
|
||||
lag.insert(id, l);
|
||||
}
|
||||
}
|
||||
"Construction" => {
|
||||
if let Some((id, k)) = gjeldende_konstruksjon.take() {
|
||||
konstruksjoner.insert(id, k);
|
||||
}
|
||||
}
|
||||
"WindowType" => {
|
||||
if let Some((id, v)) = gjeldende_vindustype.take() {
|
||||
vindustyper.insert(id, v);
|
||||
}
|
||||
}
|
||||
"Opening" => {
|
||||
if let (Some(a), Some(o)) =
|
||||
(gjeldende_aapning.take(), gjeldende_overflate.as_mut())
|
||||
{
|
||||
o.aapninger.push(a);
|
||||
}
|
||||
}
|
||||
"Surface" => {
|
||||
if let Some(o) = gjeldende_overflate.take() {
|
||||
overflater.push(o);
|
||||
}
|
||||
}
|
||||
"Space" => {
|
||||
if let Some(r) = gjeldende_rom.take() {
|
||||
rom.push(r);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
sti_elementer.pop();
|
||||
tekstbuffer.clear();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
bygg(
|
||||
standardnavn,
|
||||
bygningsnavn,
|
||||
bygningstype,
|
||||
rom,
|
||||
overflater,
|
||||
konstruksjoner,
|
||||
lag,
|
||||
materialer,
|
||||
vindustyper,
|
||||
)
|
||||
}
|
||||
|
||||
fn sett<T>(mål: &mut Option<(String, T)>, f: impl FnOnce(&mut T)) {
|
||||
if let Some((_, v)) = mål.as_mut() {
|
||||
f(v);
|
||||
}
|
||||
}
|
||||
|
||||
fn lokalt_navn(b: &[u8]) -> String {
|
||||
let s = String::from_utf8_lossy(b);
|
||||
s.rsplit(':').next().unwrap_or(&s).to_string()
|
||||
}
|
||||
|
||||
/// Arealet av et plant polygon i rommet.
|
||||
fn polygonareal(p: &[[f64; 3]]) -> f64 {
|
||||
if p.len() < 3 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut n = [0.0f64; 3];
|
||||
for i in 0..p.len() {
|
||||
let a = p[i];
|
||||
let b = p[(i + 1) % p.len()];
|
||||
n[0] += a[1] * b[2] - a[2] * b[1];
|
||||
n[1] += a[2] * b[0] - a[0] * b[2];
|
||||
n[2] += a[0] * b[1] - a[1] * b[0];
|
||||
}
|
||||
0.5 * (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt()
|
||||
}
|
||||
|
||||
fn areal_av(areal: Option<f64>, bredde: f64, hoyde: f64, polygon: &[[f64; 3]]) -> f64 {
|
||||
if let Some(a) = areal.filter(|a| *a > 0.0) {
|
||||
return a;
|
||||
}
|
||||
if bredde > 0.0 && hoyde > 0.0 {
|
||||
return bredde * hoyde;
|
||||
}
|
||||
polygonareal(polygon)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn bygg(
|
||||
standardnavn: &str,
|
||||
bygningsnavn: Option<String>,
|
||||
bygningstype: Option<String>,
|
||||
rom: Vec<Rom>,
|
||||
overflater: Vec<Overflate>,
|
||||
konstruksjoner: HashMap<String, Konstruksjon>,
|
||||
lag: HashMap<String, Vec<String>>,
|
||||
materialer: HashMap<String, Material>,
|
||||
vindustyper: HashMap<String, Vindustype>,
|
||||
) -> Result<Importrapport> {
|
||||
let mut merknader = Vec::new();
|
||||
let bra: f64 = rom.iter().map(|r| r.areal).sum();
|
||||
let volum: f64 = rom.iter().map(|r| r.volum).sum();
|
||||
if bra <= 0.0 {
|
||||
merknader.push(
|
||||
"gbXML-filen oppgir ikke romareal. Oppvarmet BRA er satt til 0 og må fylles inn."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let u_av_konstruksjon = |id: &str| -> Option<f64> {
|
||||
let k = konstruksjoner.get(id)?;
|
||||
if let Some(u) = k.u_verdi.filter(|u| *u > 0.0) {
|
||||
return Some(u);
|
||||
}
|
||||
// Summer lagvis varmemotstand og legg til overgangsmotstander.
|
||||
let mut r = 0.17;
|
||||
let mut fant = false;
|
||||
for lid in &k.lag {
|
||||
for mid in lag.get(lid).into_iter().flatten() {
|
||||
if let Some(m) = materialer.get(mid) {
|
||||
if m.r_verdi > 0.0 {
|
||||
r += m.r_verdi;
|
||||
fant = true;
|
||||
} else if m.ledningsevne > 0.0 && m.tykkelse > 0.0 {
|
||||
r += m.tykkelse / m.ledningsevne;
|
||||
fant = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fant.then(|| 1.0 / r)
|
||||
};
|
||||
|
||||
let kappa_av_konstruksjon = |id: &str| -> Option<f64> {
|
||||
let k = konstruksjoner.get(id)?;
|
||||
let mut kappa = 0.0;
|
||||
for lid in &k.lag {
|
||||
for mid in lag.get(lid).into_iter().flatten() {
|
||||
if let Some(m) = materialer.get(mid) {
|
||||
if m.densitet > 0.0 && m.varmekapasitet > 0.0 && m.tykkelse > 0.0 {
|
||||
kappa += m.tykkelse * m.densitet * m.varmekapasitet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(kappa > 0.0).then_some(kappa)
|
||||
};
|
||||
|
||||
let mut flater: Vec<Flate> = Vec::new();
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut hoppet_over = 0;
|
||||
|
||||
for o in &overflater {
|
||||
let Some((typ, mot)) = klassifiser(&o.typ) else {
|
||||
hoppet_over += 1;
|
||||
continue;
|
||||
};
|
||||
let brutto = areal_av(o.areal, o.bredde, o.hoyde, &o.polygon);
|
||||
let mut aapningsareal = 0.0;
|
||||
for a in &o.aapninger {
|
||||
let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon);
|
||||
aapningsareal += areal;
|
||||
let vt = vindustyper.get(&a.vindustype);
|
||||
let u = vt.and_then(|v| v.u_verdi).unwrap_or_else(|| {
|
||||
merknader.push(format!(
|
||||
"Åpningen «{}» mangler U-verdi i gbXML; satt til 2,4 W/(m²K).",
|
||||
a.navn
|
||||
));
|
||||
2.4
|
||||
});
|
||||
let dor = a.typ.to_lowercase().contains("door");
|
||||
vinduer.push(Vindu {
|
||||
navn: vt
|
||||
.map(|v| v.navn.clone())
|
||||
.filter(|n| !n.is_empty())
|
||||
.unwrap_or_else(|| a.navn.clone()),
|
||||
areal,
|
||||
antall: 1.0,
|
||||
orientering: o.asimut.unwrap_or(0.0),
|
||||
helning: o.helning.unwrap_or(90.0),
|
||||
u_verdi: u,
|
||||
g_verdi: vt.and_then(|v| v.shgc),
|
||||
karmandel: None,
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor,
|
||||
kommentar: Some(format!("fra gbXML, flate {}", o.navn)),
|
||||
});
|
||||
}
|
||||
let netto = (brutto - aapningsareal).max(0.0);
|
||||
if netto <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let u = u_av_konstruksjon(&o.konstruksjon);
|
||||
if u.is_none() {
|
||||
merknader.push(format!(
|
||||
"Flaten «{}» mangler U-verdi og lagoppbygging i gbXML; verdien utledes av byggeår.",
|
||||
o.navn
|
||||
));
|
||||
}
|
||||
let helning = o.helning.unwrap_or(match typ {
|
||||
FlateType::Vegg => 90.0,
|
||||
FlateType::Tak => 0.0,
|
||||
FlateType::Gulv => 180.0,
|
||||
});
|
||||
flater.push(Flate {
|
||||
navn: o.navn.clone(),
|
||||
typ,
|
||||
areal: netto,
|
||||
orientering: o.asimut.unwrap_or(0.0),
|
||||
helning: Some(helning),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: kappa_av_konstruksjon(&o.konstruksjon).and_then(masseklasse_av_kappa),
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: konstruksjoner
|
||||
.get(&o.konstruksjon)
|
||||
.map(|k| k.navn.clone())
|
||||
.filter(|n| !n.is_empty()),
|
||||
});
|
||||
}
|
||||
|
||||
if hoppet_over > 0 {
|
||||
merknader.push(format!(
|
||||
"{hoppet_over} innvendige flater og skyggeflater er utelatt; bare klimaskjermen regnes med."
|
||||
));
|
||||
}
|
||||
merknader.push(
|
||||
"gbXML beskriver ikke varmekilder, ventilasjon, lekkasjetall eller kommune. \
|
||||
Fyll inn [ventilasjon], [infiltrasjon], [oppvarming] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let kategori = match bygningstype.as_deref() {
|
||||
Some(t) if t.contains("MultiFamily") || t.contains("Apartment") => Kategori::Boligblokk,
|
||||
Some(t) if t.contains("Office") => Kategori::Kontorbygg,
|
||||
Some(t) if t.contains("School") => Kategori::Skolebygg,
|
||||
_ => Kategori::Smaahus,
|
||||
};
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn: bygningsnavn.unwrap_or_else(|| standardnavn.to_string()),
|
||||
kategori,
|
||||
byggeaar: 2020,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: (volum > 0.0).then_some(volum),
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: None,
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra gbXML".into()),
|
||||
energiraadgiver: None,
|
||||
oppdragsgiver: None,
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
ventilasjon: Ventilasjon::default(),
|
||||
tappevann: Tappevann::default(),
|
||||
oppvarming: Oppvarming::default(),
|
||||
distribusjon: Distribusjon::default(),
|
||||
solcelle: None,
|
||||
kjoling: Kjoling::default(),
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
fn klassifiser(typ: &str) -> Option<(FlateType, Mot)> {
|
||||
match typ {
|
||||
"ExteriorWall" => Some((FlateType::Vegg, Mot::Ute)),
|
||||
"UndergroundWall" => Some((FlateType::Vegg, Mot::Grunn)),
|
||||
"Roof" => Some((FlateType::Tak, Mot::Ute)),
|
||||
"Ceiling" | "UndergroundCeiling" => Some((FlateType::Tak, Mot::Uoppvarmet)),
|
||||
"SlabOnGrade" | "UndergroundSlab" => Some((FlateType::Gulv, Mot::Grunn)),
|
||||
"RaisedFloor" => Some((FlateType::Gulv, Mot::Ute)),
|
||||
"ExposedFloor" => Some((FlateType::Gulv, Mot::Ute)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn masseklasse_av_kappa(kappa: f64) -> Option<Masseklasse> {
|
||||
Some(match kappa {
|
||||
k if k < 62_500.0 => Masseklasse::SvaertLett,
|
||||
k if k < 92_500.0 => Masseklasse::Lett,
|
||||
k if k < 142_500.0 => Masseklasse::Middels,
|
||||
k if k < 212_500.0 => Masseklasse::Tung,
|
||||
_ => Masseklasse::SvaertTung,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const PROVE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gbXML xmlns="http://www.gbxml.org/schema" lengthUnit="Meters" areaUnit="SquareMeters" temperatureUnit="C">
|
||||
<Campus id="c1">
|
||||
<Building id="b1" buildingType="SingleFamily">
|
||||
<Name>Prøvehus</Name>
|
||||
<Space id="s1"><Area>100.0</Area><Volume>250.0</Volume></Space>
|
||||
</Building>
|
||||
<Surface id="v-sor" surfaceType="ExteriorWall" constructionIdRef="k-vegg">
|
||||
<Name>Yttervegg sør</Name>
|
||||
<AdjacentSpaceId spaceIdRef="s1"/>
|
||||
<RectangularGeometry><Azimuth>180</Azimuth><Tilt>90</Tilt><Width>10</Width><Height>2.5</Height></RectangularGeometry>
|
||||
<Opening id="vindu-1" openingType="FixedWindow" windowTypeIdRef="w1">
|
||||
<RectangularGeometry><Width>2.0</Width><Height>1.5</Height></RectangularGeometry>
|
||||
</Opening>
|
||||
</Surface>
|
||||
<Surface id="tak" surfaceType="Roof" constructionIdRef="k-tak">
|
||||
<AdjacentSpaceId spaceIdRef="s1"/>
|
||||
<PlanarGeometry><PolyLoop>
|
||||
<CartesianPoint><Coordinate>0</Coordinate><Coordinate>0</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
|
||||
<CartesianPoint><Coordinate>10</Coordinate><Coordinate>0</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
|
||||
<CartesianPoint><Coordinate>10</Coordinate><Coordinate>10</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
|
||||
<CartesianPoint><Coordinate>0</Coordinate><Coordinate>10</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
|
||||
</PolyLoop></PlanarGeometry>
|
||||
</Surface>
|
||||
<Surface id="innervegg" surfaceType="InteriorWall" constructionIdRef="k-vegg">
|
||||
<RectangularGeometry><Width>5</Width><Height>2.5</Height></RectangularGeometry>
|
||||
</Surface>
|
||||
</Campus>
|
||||
<Construction id="k-vegg"><Name>Bindingsverk 200</Name><U-value unit="WPerSquareMeterK">0.18</U-value></Construction>
|
||||
<Construction id="k-tak">
|
||||
<Name>Takkonstruksjon</Name>
|
||||
<LayerId layerIdRef="l1"/>
|
||||
</Construction>
|
||||
<Layer id="l1"><MaterialId materialIdRef="m1"/></Layer>
|
||||
<Material id="m1"><Thickness>0.3</Thickness><Conductivity>0.037</Conductivity><Density>30</Density><SpecificHeat>1030</SpecificHeat></Material>
|
||||
<WindowType id="w1"><Name>2-lags</Name><U-value unit="WPerSquareMeterK">1.2</U-value><SolarHeatGainCoeff>0.5</SolarHeatGainCoeff></WindowType>
|
||||
</gbXML>"#;
|
||||
|
||||
#[test]
|
||||
fn leser_flater_vinduer_og_arealer() {
|
||||
let r = fra_tekst(PROVE, "prove").unwrap();
|
||||
let b = r.bygning;
|
||||
assert_eq!(b.bygning.navn, "Prøvehus");
|
||||
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
||||
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
||||
// Ytterveggen: 25 m² minus 3 m² vindu.
|
||||
let vegg = b.flate.iter().find(|f| f.navn == "Yttervegg sør").unwrap();
|
||||
assert!((vegg.areal - 22.0).abs() < 1e-9);
|
||||
assert_eq!(vegg.u_verdi, Some(0.18));
|
||||
assert_eq!(vegg.orientering, 180.0);
|
||||
// Taket: polygonareal 100 m², U-verdi utledet av lag.
|
||||
let tak = b.flate.iter().find(|f| f.navn == "tak").unwrap();
|
||||
assert!((tak.areal - 100.0).abs() < 1e-6);
|
||||
let u = tak.u_verdi.unwrap();
|
||||
assert!((u - 1.0 / (0.17 + 0.3 / 0.037)).abs() < 1e-9, "u = {u}");
|
||||
// Innvendig vegg skal ikke bli med.
|
||||
assert!(b.flate.iter().all(|f| f.navn != "innervegg"));
|
||||
// Vinduet.
|
||||
assert_eq!(b.vindu.len(), 1);
|
||||
assert_eq!(b.vindu[0].areal, 3.0);
|
||||
assert_eq!(b.vindu[0].u_verdi, 1.2);
|
||||
assert_eq!(b.vindu[0].g_verdi, Some(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygonareal_av_kvadrat() {
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[2.0, 0.0, 0.0],
|
||||
[2.0, 3.0, 0.0],
|
||||
[0.0, 3.0, 0.0],
|
||||
];
|
||||
assert!((polygonareal(&p) - 6.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
//! Import fra IFC (IFC2x3 og IFC4), som er formatet Vectorworks eksporterer med
|
||||
//! «Export IFC Project».
|
||||
//!
|
||||
//! Importen henter klimaskjermen: yttervegger, tak, gulv, vinduer og dører, med areal,
|
||||
//! U-verdi og himmelretning. Areal og U-verdi hentes fra mengder (`IfcElementQuantity`) og
|
||||
//! egenskapssett (`Pset_WallCommon.ThermalTransmittance` m.fl.) når modellen har dem, ellers
|
||||
//! av geometriske attributter. Himmelretningen utledes av elementets plassering: veggens
|
||||
//! lokale x-akse gir planet, og ytre normal velges som den som peker bort fra bygningens
|
||||
//! tyngdepunkt. Prosjektets sanne nord (`IfcGeometricRepresentationContext.TrueNorth`)
|
||||
//! legges til.
|
||||
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&raa);
|
||||
fra_tekst(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
let lengdeskala = lengdeenhet(&fil);
|
||||
let sant_nord = sant_nord(&fil);
|
||||
let egenskaper = samle_egenskaper(&fil);
|
||||
let mengder = samle_mengder(&fil);
|
||||
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
||||
|
||||
// Areal og volum fra rom.
|
||||
let mut bra = 0.0;
|
||||
let mut volum = 0.0;
|
||||
for (r, _) in fil.av_type("IFCSPACE") {
|
||||
if let Some(m) = mengder.get(&r) {
|
||||
bra += m
|
||||
.get("NetFloorArea")
|
||||
.or_else(|| m.get("GrossFloorArea"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
volum += m
|
||||
.get("NetVolume")
|
||||
.or_else(|| m.get("GrossVolume"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Plasseringer for alle bygningsdeler.
|
||||
let mut plassering: HashMap<u64, (f64, f64, f64, f64, f64)> = HashMap::new();
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER).chain(fil.av_typer(&PLATETYPER)) {
|
||||
if let Some(p) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(v) = global_plassering(&fil, p, lengdeskala) {
|
||||
plassering.insert(r, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tyngdepunkt av ytterveggene, for å bestemme hvilken vei normalen peker.
|
||||
let ytre: Vec<u64> = fil
|
||||
.av_typer(&VEGGTYPER)
|
||||
.filter(|(r, _)| er_ytre(&egenskaper, *r).unwrap_or(true))
|
||||
.map(|(r, _)| r)
|
||||
.collect();
|
||||
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
||||
for r in &ytre {
|
||||
if let Some((x, y, _, _, _)) = plassering.get(r) {
|
||||
cx += x;
|
||||
cy += y;
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if n > 0.0 {
|
||||
cx /= n;
|
||||
cy /= n;
|
||||
}
|
||||
|
||||
let mut flater: Vec<Flate> = Vec::new();
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut uten_uverdi = 0usize;
|
||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
|
||||
// ── vegger ──────────────────────────────────────────────────────
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER) {
|
||||
let ytre_vegg = er_ytre(&egenskaper, r).unwrap_or(true);
|
||||
let asimut = plassering
|
||||
.get(&r)
|
||||
.map(|(x, y, _, dx, dy)| asimut_av(*x, *y, *dx, *dy, cx, cy, sant_nord))
|
||||
.unwrap_or(0.0);
|
||||
asimut_for_vegg.insert(r, asimut);
|
||||
if !ytre_vegg {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let brutto = m
|
||||
.and_then(|m| {
|
||||
m.get("NetSideArea")
|
||||
.or_else(|| m.get("GrossSideArea"))
|
||||
.copied()
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if brutto <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, "Vegg"),
|
||||
typ: FlateType::Vegg,
|
||||
areal: brutto,
|
||||
orientering: asimut,
|
||||
helning: Some(90.0),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot: Mot::Ute,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── plater: tak, gulv, dekke mot grunn ──────────────────────────
|
||||
for (r, e) in fil.av_typer(&PLATETYPER) {
|
||||
if !er_ytre(&egenskaper, r).unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let a = m
|
||||
.and_then(|m| m.get("NetArea").or_else(|| m.get("GrossArea")).copied())
|
||||
.unwrap_or(0.0);
|
||||
if a <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let type_tekst = e
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tekst())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_uppercase();
|
||||
// Plater uten ROOF-merking regnes som dekke mot grunn; kontroller `mot` etter importen
|
||||
// dersom modellen har dekker mot det fri eller mot uoppvarmet kjeller.
|
||||
let (typ, mot, helning) = if e.typ == "IFCROOF" || type_tekst.contains("ROOF") {
|
||||
(FlateType::Tak, Mot::Ute, 0.0)
|
||||
} else {
|
||||
(FlateType::Gulv, Mot::Grunn, 180.0)
|
||||
};
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, if typ == FlateType::Tak { "Tak" } else { "Gulv" }),
|
||||
typ,
|
||||
areal: a,
|
||||
orientering: 0.0,
|
||||
helning: Some(helning),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── vinduer og dører ────────────────────────────────────────────
|
||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||
let dor = e.typ == "IFCDOOR";
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let areal = mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("Area").or_else(|| m.get("GrossArea")).copied())
|
||||
.filter(|a| *a > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
if areal <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let vert = if dor {
|
||||
dor_vert.get(&r)
|
||||
} else {
|
||||
vindu_vert.get(&r)
|
||||
};
|
||||
let asimut = vert
|
||||
.and_then(|v| asimut_for_vegg.get(v))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
|
||||
uten_uverdi += 1;
|
||||
if dor {
|
||||
2.0
|
||||
} else {
|
||||
2.4
|
||||
}
|
||||
});
|
||||
vinduer.push(Vindu {
|
||||
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
|
||||
areal,
|
||||
antall: 1.0,
|
||||
orientering: asimut,
|
||||
helning: 90.0,
|
||||
u_verdi: u,
|
||||
g_verdi: egenskap(&egenskaper, r, "SolarHeatGainTransmittance")
|
||||
.or_else(|| egenskap(&egenskaper, r, "SolarTransmittance")),
|
||||
karmandel: egenskap(&egenskaper, r, "GlazingAreaFraction").map(|g| 1.0 - g),
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// Trekk vindus- og dørareal fra veggene de sitter i.
|
||||
for (v, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let areal = vinduer
|
||||
.iter()
|
||||
.find(|x| x.kommentar.is_some() && x.navn.ends_with(&format!("#{v}")))
|
||||
.map(|x| x.areal);
|
||||
let _ = (areal, vegg);
|
||||
}
|
||||
let apningsareal_per_vegg = {
|
||||
let mut m: HashMap<u64, f64> = HashMap::new();
|
||||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let e = fil.hent(*a);
|
||||
let hoyde = e
|
||||
.and_then(|e| e.arg(8))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let bredde = e
|
||||
.and_then(|e| e.arg(9))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let areal = mengder
|
||||
.get(a)
|
||||
.and_then(|q| q.get("Area").or_else(|| q.get("GrossArea")).copied())
|
||||
.filter(|v| *v > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
*m.entry(*vegg).or_default() += areal;
|
||||
}
|
||||
m
|
||||
};
|
||||
for (r, _) in fil.av_typer(&VEGGTYPER) {
|
||||
if let Some(ap) = apningsareal_per_vegg.get(&r) {
|
||||
let navn = fil.hent(r).map(|e| navn_av(e, r, "Vegg"));
|
||||
if let Some(f) = navn.and_then(|n| flater.iter_mut().find(|f| f.navn == n)) {
|
||||
f.areal = (f.areal - ap).max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
flater.retain(|f| f.areal > 0.01);
|
||||
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum::<f64>();
|
||||
if bra > 0.0 {
|
||||
merknader.push(format!(
|
||||
"IFC-modellen har ingen romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvarealet. Kontroller verdien."
|
||||
));
|
||||
} else {
|
||||
merknader.push(
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if uten_uverdi > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok \
|
||||
bruke typiske verdier for byggeåret."
|
||||
));
|
||||
}
|
||||
if n == 0.0 {
|
||||
merknader.push(
|
||||
"Fant ingen plasseringer for ytterveggene; alle himmelretninger er satt til nord."
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
merknader.push(
|
||||
"Himmelretningene er utledet av elementenes plassering og bygningens tyngdepunkt. Kontroller dem mot tegningen."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
merknader.push(
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], \
|
||||
[infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let navn = fil
|
||||
.av_type("IFCBUILDING")
|
||||
.next()
|
||||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| standardnavn.to_string());
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori: Kategori::Smaahus,
|
||||
byggeaar: 2020,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: (volum > 0.0).then_some(volum),
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: adresse(&fil),
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra IFC".into()),
|
||||
energiraadgiver: None,
|
||||
oppdragsgiver: None,
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
ventilasjon: Ventilasjon::default(),
|
||||
tappevann: Tappevann::default(),
|
||||
oppvarming: Oppvarming::default(),
|
||||
distribusjon: Distribusjon::default(),
|
||||
solcelle: None,
|
||||
kjoling: Kjoling::default(),
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
fn navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
||||
match e.navn() {
|
||||
Some(n) if !n.trim().is_empty() => format!("{n} #{r}"),
|
||||
_ => format!("{standard} #{r}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn adresse(fil: &Fil) -> Option<String> {
|
||||
let (_, e) = fil.av_type("IFCPOSTALADDRESS").next()?;
|
||||
let linjer: Vec<String> = e
|
||||
.arg(4)?
|
||||
.som_liste()
|
||||
.iter()
|
||||
.filter_map(|a| a.som_tekst().map(|s| s.to_string()))
|
||||
.collect();
|
||||
let by = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
let mut ut = linjer.join(", ");
|
||||
if !by.is_empty() {
|
||||
if !ut.is_empty() {
|
||||
ut.push_str(", ");
|
||||
}
|
||||
ut.push_str(by);
|
||||
}
|
||||
(!ut.is_empty()).then_some(ut)
|
||||
}
|
||||
|
||||
/// Faktor som gjør lengder om til meter.
|
||||
fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCSIUNIT") {
|
||||
let er_lengde = e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT");
|
||||
if !er_lengde {
|
||||
continue;
|
||||
}
|
||||
let prefiks = e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
return match prefiks {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
}
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
||||
fn sant_nord(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCGEOMETRICREPRESENTATIONCONTEXT") {
|
||||
if let Some(r) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(d) = fil.hent(r) {
|
||||
let v = d.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
if v.len() >= 2 {
|
||||
let (x, y) = (
|
||||
v[0].som_tall().unwrap_or(0.0),
|
||||
v[1].som_tall().unwrap_or(1.0),
|
||||
);
|
||||
// TrueNorth peker mot nord i prosjektets koordinater.
|
||||
return x.atan2(y).to_degrees();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0.0
|
||||
}
|
||||
|
||||
/// Sammensatt global plassering: (x, y, z, dx, dy) der (dx, dy) er lokal x-akse i planet.
|
||||
fn global_plassering(fil: &Fil, r: u64, skala: f64) -> Option<(f64, f64, f64, f64, f64)> {
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
let mut z = 0.0;
|
||||
let mut dx = 1.0;
|
||||
let mut dy = 0.0;
|
||||
let mut kjede = Vec::new();
|
||||
let mut gjeldende = Some(r);
|
||||
let mut vakt = 0;
|
||||
while let Some(p) = gjeldende {
|
||||
vakt += 1;
|
||||
if vakt > 64 {
|
||||
break;
|
||||
}
|
||||
let e = fil.hent(p)?;
|
||||
if e.typ != "IFCLOCALPLACEMENT" {
|
||||
break;
|
||||
}
|
||||
kjede.push(p);
|
||||
gjeldende = e.arg(0).and_then(|a| a.som_ref());
|
||||
}
|
||||
for p in kjede.into_iter().rev() {
|
||||
let e = fil.hent(p)?;
|
||||
let ax = e
|
||||
.arg(1)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|a| fil.hent(a))?;
|
||||
let (lx, ly, lz) = punkt(fil, ax.arg(0).and_then(|a| a.som_ref()), skala);
|
||||
let (rx, ry) = retning(fil, ax.arg(2).and_then(|a| a.som_ref())).unwrap_or((1.0, 0.0));
|
||||
// Roter det lokale punktet med foreldrenes akse og legg det til.
|
||||
let nx = x + lx * dx - ly * dy;
|
||||
let ny = y + lx * dy + ly * dx;
|
||||
x = nx;
|
||||
y = ny;
|
||||
z += lz;
|
||||
let ndx = dx * rx - dy * ry;
|
||||
let ndy = dy * rx + dx * ry;
|
||||
let lengde = (ndx * ndx + ndy * ndy).sqrt().max(1e-12);
|
||||
dx = ndx / lengde;
|
||||
dy = ndy / lengde;
|
||||
}
|
||||
Some((x, y, z, dx, dy))
|
||||
}
|
||||
|
||||
fn punkt(fil: &Fil, r: Option<u64>, skala: f64) -> (f64, f64, f64) {
|
||||
let Some(e) = r.and_then(|r| fil.hent(r)) else {
|
||||
return (0.0, 0.0, 0.0);
|
||||
};
|
||||
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| v.get(i).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
(g(0), g(1), g(2))
|
||||
}
|
||||
|
||||
fn retning(fil: &Fil, r: Option<u64>) -> Option<(f64, f64)> {
|
||||
let e = r.and_then(|r| fil.hent(r))?;
|
||||
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
if v.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
v[0].som_tall().unwrap_or(1.0),
|
||||
v[1].som_tall().unwrap_or(0.0),
|
||||
))
|
||||
}
|
||||
|
||||
/// Himmelretning for en vegg: normalen står vinkelrett på veggens akse, og velges slik at
|
||||
/// den peker bort fra bygningens tyngdepunkt.
|
||||
fn asimut_av(x: f64, y: f64, dx: f64, dy: f64, cx: f64, cy: f64, sant_nord: f64) -> f64 {
|
||||
let kandidater = [(dy, -dx), (-dy, dx)];
|
||||
let ut = (x - cx, y - cy);
|
||||
let valgt = if kandidater[0].0 * ut.0 + kandidater[0].1 * ut.1
|
||||
>= kandidater[1].0 * ut.0 + kandidater[1].1 * ut.1
|
||||
{
|
||||
kandidater[0]
|
||||
} else {
|
||||
kandidater[1]
|
||||
};
|
||||
// Asimut fra nord med klokka; prosjektets y-akse peker mot nord etter TrueNorth-dreining.
|
||||
(valgt.0.atan2(valgt.1).to_degrees() + sant_nord).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
type Egenskaper = HashMap<u64, HashMap<String, Arg>>;
|
||||
|
||||
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
|
||||
fn samle_egenskaper(fil: &Fil) -> Egenskaper {
|
||||
let mut ut: Egenskaper = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
||||
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let Some(def) = rel
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|r| fil.hent(r))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if def.typ != "IFCPROPERTYSET" {
|
||||
continue;
|
||||
}
|
||||
let mut verdier = HashMap::new();
|
||||
for p in def.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(pe) = p.som_ref().and_then(|r| fil.hent(r)) else {
|
||||
continue;
|
||||
};
|
||||
if pe.typ != "IFCPROPERTYSINGLEVALUE" {
|
||||
continue;
|
||||
}
|
||||
let (Some(navn), Some(v)) = (pe.arg(0).and_then(|a| a.som_tekst()), pe.arg(2)) else {
|
||||
continue;
|
||||
};
|
||||
verdier.insert(navn.to_string(), v.clone());
|
||||
}
|
||||
for o in objekter {
|
||||
if let Some(r) = o.som_ref() {
|
||||
ut.entry(r).or_default().extend(verdier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Typeobjektenes egenskaper arves av forekomstene.
|
||||
let mut arv: Vec<(u64, HashMap<String, Arg>)> = Vec::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYTYPE") {
|
||||
let Some(typ) = rel.arg(5).and_then(|a| a.som_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(verdier) = ut.get(&typ).cloned() else {
|
||||
continue;
|
||||
};
|
||||
for o in rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(r) = o.som_ref() {
|
||||
arv.push((r, verdier.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (r, verdier) in arv {
|
||||
let e = ut.entry(r).or_default();
|
||||
for (k, v) in verdier {
|
||||
e.entry(k).or_insert(v);
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Samler alle mengder fra `IfcElementQuantity` per objekt.
|
||||
fn samle_mengder(fil: &Fil) -> HashMap<u64, HashMap<String, f64>> {
|
||||
let mut ut: HashMap<u64, HashMap<String, f64>> = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
||||
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let Some(def) = rel
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|r| fil.hent(r))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if def.typ != "IFCELEMENTQUANTITY" {
|
||||
continue;
|
||||
}
|
||||
let mut verdier = HashMap::new();
|
||||
for q in def.arg(5).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(qe) = q.som_ref().and_then(|r| fil.hent(r)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(navn) = qe.arg(0).and_then(|a| a.som_tekst()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(v) = qe.arg(3).and_then(|a| a.som_tall()) {
|
||||
verdier.insert(navn.to_string(), v);
|
||||
}
|
||||
}
|
||||
for o in objekter {
|
||||
if let Some(r) = o.som_ref() {
|
||||
ut.entry(r).or_default().extend(verdier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Kobler vinduer og dører til veggen de sitter i, via åpningene.
|
||||
fn samle_apninger(fil: &Fil) -> (HashMap<u64, u64>, HashMap<u64, u64>) {
|
||||
// åpning → vertselement
|
||||
let mut vert_for_apning: HashMap<u64, u64> = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELVOIDSELEMENT") {
|
||||
if let (Some(vert), Some(apning)) = (
|
||||
rel.arg(4).and_then(|a| a.som_ref()),
|
||||
rel.arg(5).and_then(|a| a.som_ref()),
|
||||
) {
|
||||
vert_for_apning.insert(apning, vert);
|
||||
}
|
||||
}
|
||||
let mut vindu = HashMap::new();
|
||||
let mut dor = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELFILLSELEMENT") {
|
||||
let (Some(apning), Some(element)) = (
|
||||
rel.arg(4).and_then(|a| a.som_ref()),
|
||||
rel.arg(5).and_then(|a| a.som_ref()),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Some(vert) = vert_for_apning.get(&apning) else {
|
||||
continue;
|
||||
};
|
||||
match fil.hent(element).map(|e| e.typ.as_str()) {
|
||||
Some("IFCDOOR") => {
|
||||
dor.insert(element, *vert);
|
||||
}
|
||||
Some("IFCWINDOW") => {
|
||||
vindu.insert(element, *vert);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(vindu, dor)
|
||||
}
|
||||
|
||||
fn egenskap(e: &Egenskaper, r: u64, navn: &str) -> Option<f64> {
|
||||
e.get(&r)?.get(navn)?.som_tall()
|
||||
}
|
||||
|
||||
fn u_verdi(e: &Egenskaper, r: u64) -> Option<f64> {
|
||||
[
|
||||
"ThermalTransmittance",
|
||||
"UValue",
|
||||
"U-Value",
|
||||
"Varmegjennomgangskoeffisient",
|
||||
]
|
||||
.iter()
|
||||
.find_map(|n| egenskap(e, r, n))
|
||||
.filter(|u| *u > 0.0 && *u < 20.0)
|
||||
}
|
||||
|
||||
fn er_ytre(e: &Egenskaper, r: u64) -> Option<bool> {
|
||||
let v = e.get(&r)?.get("IsExternal")?;
|
||||
match v {
|
||||
Arg::Enum(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
||||
Arg::Tall(n) => Some(*n != 0.0),
|
||||
Arg::Tekst(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const PROVE: &str = r#"ISO-10303-21;
|
||||
HEADER;ENDSEC;
|
||||
DATA;
|
||||
#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2= IFCDIRECTION((0.,1.));
|
||||
#3= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#2);
|
||||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);
|
||||
#20= IFCLOCALPLACEMENT($,#21);
|
||||
#21= IFCAXIS2PLACEMENT3D(#22,$,$);
|
||||
#22= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#100= IFCWALLSTANDARDCASE('w1',$,'Sorvegg',$,$,#110,$,$);
|
||||
#110= IFCLOCALPLACEMENT(#20,#111);
|
||||
#111= IFCAXIS2PLACEMENT3D(#112,$,#113);
|
||||
#112= IFCCARTESIANPOINT((0.,-5.,0.));
|
||||
#113= IFCDIRECTION((1.,0.,0.));
|
||||
#120= IFCWALLSTANDARDCASE('w2',$,'Nordvegg',$,$,#130,$,$);
|
||||
#130= IFCLOCALPLACEMENT(#20,#131);
|
||||
#131= IFCAXIS2PLACEMENT3D(#132,$,#133);
|
||||
#132= IFCCARTESIANPOINT((0.,5.,0.));
|
||||
#133= IFCDIRECTION((1.,0.,0.));
|
||||
#200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);
|
||||
#201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
|
||||
#202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));
|
||||
#203= IFCRELDEFINESBYPROPERTIES('r',$,$,$,(#100,#120),#202);
|
||||
#210= IFCQUANTITYAREA('NetSideArea',$,$,25.0,$);
|
||||
#211= IFCELEMENTQUANTITY('q',$,'BaseQuantities',$,$,(#210));
|
||||
#212= IFCRELDEFINESBYPROPERTIES('r2',$,$,$,(#100,#120),#211);
|
||||
#300= IFCSPACE('s',$,'Stue',$,$,#20,$,$,.ELEMENT.,.INTERNAL.,$);
|
||||
#310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);
|
||||
#311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);
|
||||
#312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));
|
||||
#313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;"#;
|
||||
|
||||
#[test]
|
||||
fn leser_vegger_areal_uverdi_og_himmelretning() {
|
||||
let r = fra_tekst(PROVE, "prove").unwrap();
|
||||
let b = &r.bygning;
|
||||
assert_eq!(b.bygning.navn, "Testhus");
|
||||
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
||||
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
||||
assert_eq!(b.flate.len(), 2);
|
||||
for f in &b.flate {
|
||||
assert_eq!(f.areal, 25.0);
|
||||
assert_eq!(f.u_verdi, Some(0.18));
|
||||
}
|
||||
// Veggen ved y = −5 vender sør (180°), den ved y = +5 vender nord (0°).
|
||||
let sor = b
|
||||
.flate
|
||||
.iter()
|
||||
.find(|f| f.navn.starts_with("Sorvegg"))
|
||||
.unwrap();
|
||||
let nord = b
|
||||
.flate
|
||||
.iter()
|
||||
.find(|f| f.navn.starts_with("Nordvegg"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
(sor.orientering - 180.0).abs() < 1.0,
|
||||
"sør = {}",
|
||||
sor.orientering
|
||||
);
|
||||
assert!(
|
||||
nord.orientering < 1.0 || nord.orientering > 359.0,
|
||||
"nord = {}",
|
||||
nord.orientering
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn millimeterenhet_skaleres() {
|
||||
let t = PROVE.replace(
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);",
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
|
||||
);
|
||||
let r = fra_tekst(&t, "prove").unwrap();
|
||||
// Arealene kommer fra mengder og er alltid i m², så de endres ikke.
|
||||
assert_eq!(r.bygning.flate[0].areal, 25.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Innlesing av bygningsbeskrivelser: `bygning.toml` og gbXML fra BIM-verktøy.
|
||||
|
||||
pub mod gbxml;
|
||||
pub mod ifc;
|
||||
pub mod regneark;
|
||||
pub mod step;
|
||||
pub mod toml_inn;
|
||||
|
||||
pub use toml_inn::{les_bygning, slaa_sammen_scenario};
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Import fra regneark (CSV), for eksempel en Vectorworks-arbeidstegning eksportert med
|
||||
//! «Export Worksheet». Formatet er bevisst enkelt, slik at det kan settes opp som en
|
||||
//! rapport i BIM-verktøyet og eksporteres direkte.
|
||||
//!
|
||||
//! Hver rad begynner med en type:
|
||||
//!
|
||||
//! ```text
|
||||
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||
//! bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
||||
//! vegg,Yttervegg sør,32,1.0,180,90,ute,,
|
||||
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
|
||||
//! ```
|
||||
//!
|
||||
//! For `bygning`-raden brukes kolonnene: navn, oppvarmet BRA, (tom), (tom), (tom),
|
||||
//! kategori, byggeår.
|
||||
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
fra_tekst(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
)
|
||||
}
|
||||
|
||||
fn skilletegn(tekst: &str) -> u8 {
|
||||
let forste = tekst.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
|
||||
if forste.matches(';').count() > forste.matches(',').count() {
|
||||
b';'
|
||||
} else {
|
||||
b','
|
||||
}
|
||||
}
|
||||
|
||||
fn tall(s: Option<&str>) -> Option<f64> {
|
||||
let s = s?.trim().replace(',', ".");
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
s.parse().ok()
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut leser = csv::ReaderBuilder::new()
|
||||
.delimiter(skilletegn(tekst))
|
||||
.flexible(true)
|
||||
.has_headers(true)
|
||||
.from_reader(tekst.as_bytes());
|
||||
|
||||
let mut merknader = Vec::new();
|
||||
let mut flater = Vec::new();
|
||||
let mut vinduer = Vec::new();
|
||||
let mut navn = standardnavn.to_string();
|
||||
let mut kategori = Kategori::Smaahus;
|
||||
let mut byggeaar = 2020;
|
||||
let mut bra = 0.0;
|
||||
|
||||
for (nr, rad) in leser.records().enumerate() {
|
||||
let rad = rad.with_context(|| format!("rad {}", nr + 2))?;
|
||||
let felt = |i: usize| rad.get(i).map(|s| s.trim()).filter(|s| !s.is_empty());
|
||||
let Some(typ) = felt(0).map(|s| s.to_lowercase()) else {
|
||||
continue;
|
||||
};
|
||||
let radnavn = felt(1).unwrap_or("uten navn").to_string();
|
||||
match typ.as_str() {
|
||||
"bygning" => {
|
||||
navn = radnavn;
|
||||
bra = tall(felt(2)).unwrap_or(0.0);
|
||||
if let Some(k) = felt(6) {
|
||||
kategori = match k.to_lowercase().as_str() {
|
||||
"smaahus" | "småhus" | "enebolig" => Kategori::Smaahus,
|
||||
"boligblokk" | "leilighet" => Kategori::Boligblokk,
|
||||
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
|
||||
"skolebygg" | "skole" => Kategori::Skolebygg,
|
||||
"barnehage" => Kategori::Barnehage,
|
||||
annen => {
|
||||
merknader
|
||||
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
|
||||
Kategori::Smaahus
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(a) = tall(felt(7)) {
|
||||
byggeaar = a as i32;
|
||||
}
|
||||
}
|
||||
"vegg" | "tak" | "gulv" => {
|
||||
let ft = match typ.as_str() {
|
||||
"vegg" => FlateType::Vegg,
|
||||
"tak" => FlateType::Tak,
|
||||
_ => FlateType::Gulv,
|
||||
};
|
||||
let mot = match felt(6).map(|s| s.to_lowercase()).as_deref() {
|
||||
Some("grunn") | Some("grunnen") => Mot::Grunn,
|
||||
Some("uoppvarmet") | Some("kjeller") | Some("loft") => Mot::Uoppvarmet,
|
||||
_ => Mot::Ute,
|
||||
};
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
flater.push(Flate {
|
||||
navn: radnavn,
|
||||
typ: ft,
|
||||
areal,
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)),
|
||||
u_verdi: tall(felt(3)),
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
"vindu" | "dør" | "dor" => {
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let Some(u) = tall(felt(3)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler U-verdi og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
vinduer.push(Vindu {
|
||||
navn: radnavn,
|
||||
areal,
|
||||
antall: tall(felt(8)).unwrap_or(1.0),
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)).unwrap_or(90.0),
|
||||
u_verdi: u,
|
||||
g_verdi: tall(felt(7)),
|
||||
karmandel: None,
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor: typ != "vindu",
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
annen => merknader.push(format!("Ukjent radtype «{annen}» på rad {}.", nr + 2)),
|
||||
}
|
||||
}
|
||||
|
||||
if flater.is_empty() {
|
||||
bail!("regnearket inneholder ingen flater (rader som begynner med vegg, tak eller gulv)");
|
||||
}
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum();
|
||||
merknader.push(format!(
|
||||
"Regnearket mangler en bygning-rad med oppvarmet BRA; satt til {bra:.0} m² av gulvarealet."
|
||||
));
|
||||
}
|
||||
merknader.push(
|
||||
"Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori,
|
||||
byggeaar,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: None,
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: None,
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra regneark".into()),
|
||||
energiraadgiver: None,
|
||||
oppdragsgiver: None,
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
ventilasjon: Ventilasjon::default(),
|
||||
tappevann: Tappevann::default(),
|
||||
oppvarming: Oppvarming::default(),
|
||||
distribusjon: Distribusjon::default(),
|
||||
solcelle: None,
|
||||
kjoling: Kjoling::default(),
|
||||
lufting: Lufting::default(),
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn leser_enkelt_regneark() {
|
||||
let csv = "type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall\n\
|
||||
bygning,Testhus,140,,,,smaahus,1948,\n\
|
||||
vegg,Yttervegg sør,32,1.0,180,90,ute,,\n\
|
||||
gulv,Kjellergulv,70,0.8,,180,grunn,,\n\
|
||||
vindu,Stue sør,2.1,2.8,180,90,,0.75,2\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.bygning.navn, "Testhus");
|
||||
assert_eq!(r.bygning.bygning.oppvarmet_bra, 140.0);
|
||||
assert_eq!(r.bygning.bygning.byggeaar, 1948);
|
||||
assert_eq!(r.bygning.flate.len(), 2);
|
||||
assert_eq!(r.bygning.flate[1].mot, Mot::Grunn);
|
||||
assert_eq!(r.bygning.vindu.len(), 1);
|
||||
assert_eq!(r.bygning.vindu[0].antall, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn godtar_semikolon_og_desimalkomma() {
|
||||
let csv = "type;navn;areal;u_verdi\nvegg;Vegg;32,5;1,0\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.flate[0].areal, 32.5);
|
||||
assert_eq!(r.bygning.flate[0].u_verdi, Some(1.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Minimal leser for STEP-filer etter ISO 10303-21, som er formatet IFC bruker.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Arg {
|
||||
Ref(u64),
|
||||
Tekst(String),
|
||||
Tall(f64),
|
||||
Enum(String),
|
||||
Liste(Vec<Arg>),
|
||||
Tom,
|
||||
}
|
||||
|
||||
impl Arg {
|
||||
pub fn som_ref(&self) -> Option<u64> {
|
||||
match self {
|
||||
Arg::Ref(r) => Some(*r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_tall(&self) -> Option<f64> {
|
||||
match self {
|
||||
Arg::Tall(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_tekst(&self) -> Option<&str> {
|
||||
match self {
|
||||
Arg::Tekst(s) | Arg::Enum(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_liste(&self) -> &[Arg] {
|
||||
match self {
|
||||
Arg::Liste(l) => l,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entitet {
|
||||
pub typ: String,
|
||||
pub args: Vec<Arg>,
|
||||
}
|
||||
|
||||
impl Entitet {
|
||||
pub fn arg(&self, i: usize) -> Option<&Arg> {
|
||||
self.args.get(i)
|
||||
}
|
||||
pub fn navn(&self) -> Option<&str> {
|
||||
self.arg(2).and_then(|a| a.som_tekst())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Fil {
|
||||
pub entiteter: HashMap<u64, Entitet>,
|
||||
}
|
||||
|
||||
impl Fil {
|
||||
pub fn hent(&self, r: u64) -> Option<&Entitet> {
|
||||
self.entiteter.get(&r)
|
||||
}
|
||||
/// Alle entiteter av en gitt type (uten hensyn til store/små bokstaver).
|
||||
pub fn av_type<'a>(&'a self, typ: &'a str) -> impl Iterator<Item = (u64, &'a Entitet)> + 'a {
|
||||
self.entiteter
|
||||
.iter()
|
||||
.filter_map(move |(k, e)| (e.typ == typ).then_some((*k, e)))
|
||||
}
|
||||
pub fn av_typer<'a>(
|
||||
&'a self,
|
||||
typer: &'a [&'a str],
|
||||
) -> impl Iterator<Item = (u64, &'a Entitet)> + 'a {
|
||||
self.entiteter
|
||||
.iter()
|
||||
.filter_map(move |(k, e)| typer.contains(&e.typ.as_str()).then_some((*k, e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deler en STEP-fil i entiteter.
|
||||
pub fn les(tekst: &str) -> Fil {
|
||||
let mut entiteter = HashMap::new();
|
||||
let mut setning = String::new();
|
||||
let mut i_streng = false;
|
||||
let mut i_data = false;
|
||||
|
||||
for ch in tekst.chars() {
|
||||
if !i_data {
|
||||
setning.push(ch);
|
||||
if setning.len() > 4096 {
|
||||
setning.drain(..2048);
|
||||
}
|
||||
if setning.to_uppercase().ends_with("DATA;") {
|
||||
i_data = true;
|
||||
setning.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match ch {
|
||||
'\'' => {
|
||||
i_streng = !i_streng;
|
||||
setning.push(ch);
|
||||
}
|
||||
';' if !i_streng => {
|
||||
if let Some((nr, e)) = tolk_setning(&setning) {
|
||||
entiteter.insert(nr, e);
|
||||
}
|
||||
setning.clear();
|
||||
}
|
||||
'\n' | '\r' if !i_streng => setning.push(' '),
|
||||
_ => setning.push(ch),
|
||||
}
|
||||
}
|
||||
Fil { entiteter }
|
||||
}
|
||||
|
||||
fn tolk_setning(s: &str) -> Option<(u64, Entitet)> {
|
||||
let s = s.trim();
|
||||
let s = s.strip_prefix('#')?;
|
||||
let (nr, rest) = s.split_once('=')?;
|
||||
let nr: u64 = nr.trim().parse().ok()?;
|
||||
let rest = rest.trim();
|
||||
let paren = rest.find('(')?;
|
||||
let typ = rest[..paren].trim().to_uppercase();
|
||||
if typ.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let innhold = &rest[paren + 1..rest.rfind(')')?];
|
||||
Some((
|
||||
nr,
|
||||
Entitet {
|
||||
typ,
|
||||
args: del_argumenter(innhold),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Deler argumentlisten på toppnivå-kommaer.
|
||||
fn del_argumenter(s: &str) -> Vec<Arg> {
|
||||
let mut ut = Vec::new();
|
||||
let mut dybde = 0usize;
|
||||
let mut i_streng = false;
|
||||
let mut start = 0usize;
|
||||
let b: Vec<char> = s.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
let c = b[i];
|
||||
match c {
|
||||
'\'' => {
|
||||
if i_streng && i + 1 < b.len() && b[i + 1] == '\'' {
|
||||
i += 1;
|
||||
} else {
|
||||
i_streng = !i_streng;
|
||||
}
|
||||
}
|
||||
'(' if !i_streng => dybde += 1,
|
||||
')' if !i_streng => dybde = dybde.saturating_sub(1),
|
||||
',' if !i_streng && dybde == 0 => {
|
||||
ut.push(tolk_arg(&b[start..i].iter().collect::<String>()));
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if start <= b.len() {
|
||||
let siste: String = b[start..].iter().collect();
|
||||
if !(siste.trim().is_empty() && ut.is_empty()) {
|
||||
ut.push(tolk_arg(&siste));
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn tolk_arg(s: &str) -> Arg {
|
||||
let t = s.trim();
|
||||
if t.is_empty() || t == "$" || t == "*" {
|
||||
return Arg::Tom;
|
||||
}
|
||||
if let Some(r) = t.strip_prefix('#') {
|
||||
if let Ok(n) = r.trim().parse::<u64>() {
|
||||
return Arg::Ref(n);
|
||||
}
|
||||
}
|
||||
if t.starts_with('\'') && t.ends_with('\'') && t.len() >= 2 {
|
||||
return Arg::Tekst(avkod_tekst(&t[1..t.len() - 1]));
|
||||
}
|
||||
if t.starts_with('.') && t.ends_with('.') && t.len() >= 2 {
|
||||
return Arg::Enum(t[1..t.len() - 1].to_string());
|
||||
}
|
||||
if t.starts_with('(') && t.ends_with(')') {
|
||||
return Arg::Liste(del_argumenter(&t[1..t.len() - 1]));
|
||||
}
|
||||
if let Ok(v) = t.parse::<f64>() {
|
||||
return Arg::Tall(v);
|
||||
}
|
||||
// Typede verdier som IFCREAL(3.0) eller IFCLABEL('x').
|
||||
if let Some(p) = t.find('(') {
|
||||
if t.ends_with(')') {
|
||||
let indre = &t[p + 1..t.len() - 1];
|
||||
return tolk_arg(indre);
|
||||
}
|
||||
}
|
||||
Arg::Tekst(t.to_string())
|
||||
}
|
||||
|
||||
/// Dekoder ISO 10303-21-strenger: `''` er apostrof, `\X2\…\X0\` er UTF-16, `\X\hh` er latin-1.
|
||||
fn avkod_tekst(s: &str) -> String {
|
||||
let mut ut = String::with_capacity(s.len());
|
||||
let b: Vec<char> = s.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i] == '\'' && i + 1 < b.len() && b[i + 1] == '\'' {
|
||||
ut.push('\'');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if b[i] == '\\' && i + 3 < b.len() && b[i + 1] == 'X' && b[i + 2] == '2' && b[i + 3] == '\\'
|
||||
{
|
||||
let mut j = i + 4;
|
||||
let mut enheter: Vec<u16> = Vec::new();
|
||||
while j + 3 < b.len() {
|
||||
if b[j] == '\\' {
|
||||
break;
|
||||
}
|
||||
let hex: String = b[j..(j + 4).min(b.len())].iter().collect();
|
||||
match u16::from_str_radix(&hex, 16) {
|
||||
Ok(v) => enheter.push(v),
|
||||
Err(_) => break,
|
||||
}
|
||||
j += 4;
|
||||
}
|
||||
ut.push_str(&String::from_utf16_lossy(&enheter));
|
||||
// Hopp over avslutningen \X0\.
|
||||
while j < b.len() && b[j] != '0' {
|
||||
j += 1;
|
||||
}
|
||||
i = (j + 2).min(b.len());
|
||||
continue;
|
||||
}
|
||||
if b[i] == '\\' && i + 3 < b.len() && b[i + 1] == 'X' && b[i + 2] == '\\' {
|
||||
let hex: String = b[i + 3..(i + 5).min(b.len())].iter().collect();
|
||||
if let Ok(v) = u8::from_str_radix(&hex, 16) {
|
||||
ut.push(v as char);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ut.push(b[i]);
|
||||
i += 1;
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn leser_entiteter_og_argumenter() {
|
||||
let f = les("ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n\
|
||||
#1= IFCWALL('0xABC',#2,'Yttervegg s\\X2\\00F8\\X0\\r',$,$,#10,#20,$);\n\
|
||||
#2= IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,$,$,0);\n\
|
||||
#10= IFCLOCALPLACEMENT($,#11);\n\
|
||||
ENDSEC;");
|
||||
assert_eq!(f.entiteter.len(), 3);
|
||||
let v = f.hent(1).unwrap();
|
||||
assert_eq!(v.typ, "IFCWALL");
|
||||
assert_eq!(v.navn(), Some("Yttervegg sør"));
|
||||
assert_eq!(v.arg(5).unwrap().som_ref(), Some(10));
|
||||
assert_eq!(
|
||||
f.hent(2).unwrap().arg(3).unwrap().som_tekst(),
|
||||
Some("ADDED")
|
||||
);
|
||||
assert_eq!(f.hent(10).unwrap().arg(0), Some(&Arg::Tom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn haandterer_lister_og_semikolon_i_streng() {
|
||||
let f = les("DATA;\n#5= IFCPOLYLOOP((#1,#2,#3));\n#6= IFCLABEL('a;b');\nENDSEC;");
|
||||
assert_eq!(f.hent(5).unwrap().arg(0).unwrap().som_liste().len(), 3);
|
||||
assert_eq!(f.hent(6).unwrap().arg(0).unwrap().som_tekst(), Some("a;b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leser_tall_og_typede_verdier() {
|
||||
let f = les("DATA;\n#7= IFCPROPERTYSINGLEVALUE('U',$,IFCREAL(0.18),$);\nENDSEC;");
|
||||
assert_eq!(f.hent(7).unwrap().arg(2).unwrap().som_tall(), Some(0.18));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Leser `bygning.toml` og fletter inn scenarioendringer.
|
||||
|
||||
use crate::modell::{Bygning, Scenario};
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use std::path::Path;
|
||||
use toml::Value;
|
||||
|
||||
pub fn les_bygning(sti: &Path) -> Result<Bygning> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||
}
|
||||
|
||||
/// Lager en ny bygning der scenarioets endringer er flettet inn.
|
||||
///
|
||||
/// 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
|
||||
/// til, og `slett = true` fjerner et element. Andre verdier erstattes.
|
||||
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
||||
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
||||
flett(&mut grunn, &s.endringer);
|
||||
grunn
|
||||
.try_into()
|
||||
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
||||
}
|
||||
|
||||
fn navnenokkel(v: &Value) -> Option<String> {
|
||||
let t = v.as_table()?;
|
||||
t.get("navn")
|
||||
.and_then(|x| x.as_str())
|
||||
.or_else(|| t.get("type").and_then(|x| x.as_str()))
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn skal_slettes(v: &Value) -> bool {
|
||||
v.as_table()
|
||||
.and_then(|t| t.get("slett"))
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn flett(grunn: &mut Value, over: &Value) {
|
||||
match (grunn, over) {
|
||||
(Value::Table(g), Value::Table(o)) => {
|
||||
for (k, v) in o {
|
||||
if k == "slett" {
|
||||
continue;
|
||||
}
|
||||
match g.get_mut(k) {
|
||||
Some(eksisterende) => flett(eksisterende, v),
|
||||
None => {
|
||||
g.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Value::Array(g), Value::Array(o)) => {
|
||||
for element in o {
|
||||
match navnenokkel(element) {
|
||||
Some(navn) => {
|
||||
let treff = g
|
||||
.iter()
|
||||
.position(|e| navnenokkel(e).as_deref() == Some(&navn));
|
||||
match treff {
|
||||
Some(i) if skal_slettes(element) => {
|
||||
g.remove(i);
|
||||
}
|
||||
Some(i) => flett(&mut g[i], element),
|
||||
None if skal_slettes(element) => {}
|
||||
None => g.push(element.clone()),
|
||||
}
|
||||
}
|
||||
None => g.push(element.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
(g, o) => *g = o.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const GRUNN: &str = r#"
|
||||
[bygning]
|
||||
navn = "Testhus"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 1948
|
||||
oppvarmet_bra = 100.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg"
|
||||
type = "vegg"
|
||||
areal = 80.0
|
||||
u_verdi = 1.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 100.0
|
||||
u_verdi = 0.8
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn scenario_endrer_u_verdi_for_navngitt_flate() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Etterisolert".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(r#"flate = [{ navn = "Yttervegg", u_verdi = 0.22 }]"#)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
assert_eq!(ny.flate.len(), 2);
|
||||
assert_eq!(ny.flate[0].u_verdi, Some(0.22));
|
||||
assert_eq!(ny.flate[1].u_verdi, Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_legger_til_og_sletter() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Ombygd".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(
|
||||
r#"flate = [{ navn = "Tak", slett = true }, { navn = "Nytt tak", type = "tak", areal = 100.0, u_verdi = 0.13 }]"#,
|
||||
)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
let navn: Vec<&str> = ny.flate.iter().map(|f| f.navn.as_str()).collect();
|
||||
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_endrer_skalarfelt() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Varmepumpe".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(
|
||||
r#"
|
||||
[infiltrasjon]
|
||||
n50 = 3.0
|
||||
[oppvarming]
|
||||
kilde = [{ type = "luft_luft_vp", andel_rom = 0.6 }]
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
assert_eq!(ny.infiltrasjon.n50, Some(3.0));
|
||||
assert_eq!(ny.oppvarming.kilde.len(), 1);
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
//! Kjører en fullstendig energiberegning: fra bygningsbeskrivelse til energikarakter.
|
||||
|
||||
use crate::data::Datakilde;
|
||||
use crate::klima::{korreksjon, tmy, Vaerdata, REFERANSEKLIMA};
|
||||
use crate::merking::{karakter, Energikarakter};
|
||||
use crate::modell::lost::{Forutsetning, Lost};
|
||||
use crate::modell::Bygning;
|
||||
use crate::motor::{iso52016::Systemtilskudd, simuler, Timeserie, Varmetapsbudsjett};
|
||||
use crate::systemer::{self, Effektbehov, Energibudsjett, Levert, Systemoppsett};
|
||||
use crate::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Alt som kommer ut av én beregning.
|
||||
pub struct Resultat {
|
||||
pub bygning: Bygning,
|
||||
pub lost: Lost,
|
||||
pub vaerdata: Vaerdata,
|
||||
pub klimanavn: String,
|
||||
pub varmetap: Varmetapsbudsjett,
|
||||
pub timeserie: Timeserie,
|
||||
pub systemer: Systemoppsett,
|
||||
pub budsjett: Energibudsjett,
|
||||
pub levert: Levert,
|
||||
pub karakter: Energikarakter,
|
||||
pub effektbehov: Effektbehov,
|
||||
pub solceller: Option<crate::systemer::solcelle::Produksjon>,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
/// Kompakt, serialiserbart sammendrag for `resultat.json`.
|
||||
#[derive(Serialize)]
|
||||
pub struct Sammendrag<'a> {
|
||||
pub navn: &'a str,
|
||||
pub kategori: &'a str,
|
||||
pub byggeaar: i32,
|
||||
pub oppvarmet_bra: f64,
|
||||
pub klimasted: &'a str,
|
||||
pub referanseklima: &'a str,
|
||||
pub varmetapsbudsjett: &'a Varmetapsbudsjett,
|
||||
pub energibudsjett: &'a Energibudsjett,
|
||||
pub netto_spesifikk: f64,
|
||||
pub levert: &'a Levert,
|
||||
pub energikarakter: &'a Energikarakter,
|
||||
pub effektbehov: &'a Effektbehov,
|
||||
pub forutsetninger: &'a [Forutsetning],
|
||||
pub merknader: &'a [String],
|
||||
pub maanedsverdier: Maanedsverdier,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Maanedsverdier {
|
||||
pub maaned: [&'static str; 12],
|
||||
pub romoppvarming: [f64; 12],
|
||||
pub ventilasjonsvarme: [f64; 12],
|
||||
pub varmtvann: [f64; 12],
|
||||
pub vifter_pumper: [f64; 12],
|
||||
pub belysning: [f64; 12],
|
||||
pub utstyr: [f64; 12],
|
||||
pub kjoling: [f64; 12],
|
||||
pub utetemperatur: [f64; 12],
|
||||
}
|
||||
|
||||
impl Resultat {
|
||||
pub fn sammendrag(&self) -> Sammendrag<'_> {
|
||||
let m = |s: &[f64]| Timeserie::maaned_kwh(s);
|
||||
let mut vifter_pumper = m(&self.timeserie.vifter);
|
||||
let pumper = m(&self.timeserie.pumper);
|
||||
let mut kjoling = m(&self.timeserie.romkjoling);
|
||||
let ventkjol = m(&self.timeserie.ventilasjonskjoling);
|
||||
for i in 0..12 {
|
||||
vifter_pumper[i] += pumper[i];
|
||||
kjoling[i] += ventkjol[i];
|
||||
}
|
||||
Sammendrag {
|
||||
navn: &self.lost.navn,
|
||||
kategori: self.lost.kategori.nokkel(),
|
||||
byggeaar: self.lost.byggeaar,
|
||||
oppvarmet_bra: self.lost.bra,
|
||||
klimasted: &self.klimanavn,
|
||||
referanseklima: REFERANSEKLIMA,
|
||||
varmetapsbudsjett: &self.varmetap,
|
||||
energibudsjett: &self.budsjett,
|
||||
netto_spesifikk: self.budsjett.netto_spesifikk(self.lost.bra),
|
||||
levert: &self.levert,
|
||||
energikarakter: &self.karakter,
|
||||
effektbehov: &self.effektbehov,
|
||||
forutsetninger: &self.lost.forutsetninger,
|
||||
merknader: &self.merknader,
|
||||
maanedsverdier: Maanedsverdier {
|
||||
maaned: crate::MAANEDER,
|
||||
romoppvarming: m(&self.timeserie.romoppvarming),
|
||||
ventilasjonsvarme: m(&self.timeserie.ventilasjonsvarme),
|
||||
varmtvann: m(&self.timeserie.varmtvann),
|
||||
vifter_pumper,
|
||||
belysning: m(&self.timeserie.belysning),
|
||||
utstyr: m(&self.timeserie.utstyr),
|
||||
kjoling,
|
||||
utetemperatur: self.vaerdata.maanedsmiddeltemperatur(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Henter værdata for bygningen: egen EPW-fil, oppgitt kommune, eller referanseklimaet.
|
||||
pub fn hent_klima(b: &Bygning) -> Result<(Vaerdata, String, bool)> {
|
||||
if let Some(sti) = &b.klima.epw {
|
||||
let v = Vaerdata::fra_fil(sti)?;
|
||||
let navn = v.sted.clone();
|
||||
let er_referanse = crate::data::normaliser_stedsnavn(&navn)
|
||||
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
||||
return Ok((v, navn, er_referanse));
|
||||
}
|
||||
let sted = b
|
||||
.klima
|
||||
.kommune
|
||||
.clone()
|
||||
.unwrap_or_else(|| REFERANSEKLIMA.to_string());
|
||||
let er_referanse = crate::data::normaliser_stedsnavn(&sted)
|
||||
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
||||
let v = tmy::for_kommune(&sted)?;
|
||||
Ok((v, sted, er_referanse))
|
||||
}
|
||||
|
||||
pub fn kjor(b: &Bygning, kilde: &Datakilde) -> Result<Resultat> {
|
||||
let lost = Lost::fra(b, kilde)?;
|
||||
let (vaerdata, klimanavn, er_referanse) = hent_klima(b)?;
|
||||
kjor_med_klima(b, kilde, lost, vaerdata, klimanavn, er_referanse)
|
||||
}
|
||||
|
||||
pub fn kjor_med_klima(
|
||||
b: &Bygning,
|
||||
kilde: &Datakilde,
|
||||
lost: Lost,
|
||||
vaerdata: Vaerdata,
|
||||
klimanavn: String,
|
||||
er_referanse: bool,
|
||||
) -> Result<Resultat> {
|
||||
let varmetap = Varmetapsbudsjett::beregn(&lost);
|
||||
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
|
||||
let mut merknader = systemer_oppsett.merknader.clone();
|
||||
|
||||
let tilskudd = Systemtilskudd {
|
||||
tappevann: systemer::tappevannstilskudd(&lost, &systemer_oppsett),
|
||||
};
|
||||
let timeserie = simuler(&lost, &vaerdata, tilskudd)?;
|
||||
let budsjett = systemer::energibudsjett(×erie, &systemer_oppsett);
|
||||
|
||||
let solceller = b.solcelle.as_ref().map(|s| {
|
||||
systemer::solcelle::beregn(s, &vaerdata, ×erie, &systemer_oppsett, lost.albedo)
|
||||
});
|
||||
let (egenbruk, eksportert) = solceller
|
||||
.as_ref()
|
||||
.map(|p| (p.egenbruk_kwh, p.eksportert_kwh))
|
||||
.unwrap_or((0.0, 0.0));
|
||||
|
||||
let korrigering = if er_referanse {
|
||||
None
|
||||
} else {
|
||||
let tabell = kilde.klimakorreksjon()?;
|
||||
let klimaavhengig = systemer::klimaavhengig_spesifikk(&budsjett, lost.bra);
|
||||
let k = korreksjon::korriger(&tabell, &klimanavn, lost.kategori, klimaavhengig);
|
||||
if k.is_none() {
|
||||
merknader.push(format!(
|
||||
"Fant ingen klimakorrigeringsfaktor for «{klimanavn}». Energiytelsen er oppgitt i lokalt \
|
||||
klima og er ikke sammenlignbar med energimerkeskalaen."
|
||||
));
|
||||
}
|
||||
k
|
||||
};
|
||||
|
||||
let vekting = kilde.vekting()?;
|
||||
let levert = systemer::levert(
|
||||
&budsjett,
|
||||
&vekting,
|
||||
lost.bra,
|
||||
korrigering,
|
||||
egenbruk,
|
||||
eksportert,
|
||||
);
|
||||
let skala = kilde.skala()?;
|
||||
let karakter = karakter(
|
||||
skala.for_kategori(lost.kategori)?,
|
||||
levert.spesifikk_vektet,
|
||||
lost.skala_areal,
|
||||
);
|
||||
let effektbehov = systemer::effektbehov(×erie, &systemer_oppsett);
|
||||
|
||||
Ok(Resultat {
|
||||
bygning: b.clone(),
|
||||
lost,
|
||||
vaerdata,
|
||||
klimanavn,
|
||||
varmetap,
|
||||
timeserie,
|
||||
systemer: systemer_oppsett,
|
||||
budsjett,
|
||||
levert,
|
||||
karakter,
|
||||
effektbehov,
|
||||
solceller,
|
||||
merknader,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Leser EnergyPlus Weather (EPW) og gir 8760 timesverdier.
|
||||
//!
|
||||
//! Feltnumre følger EPW-spesifikasjonen (0-basert etter de åtte headerlinjene):
|
||||
//! 6 tørrkuletemperatur °C, 7 duggpunkt °C, 8 relativ fuktighet %, 9 lufttrykk Pa,
|
||||
//! 12 horisontal infrarød stråling W/m², 13 global horisontal W/m², 14 direkte normal W/m²,
|
||||
//! 15 diffus horisontal W/m², 20 vindretning °, 21 vindhastighet m/s.
|
||||
|
||||
use crate::{Result, TIMER};
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Vaerdata {
|
||||
pub sted: String,
|
||||
pub land: String,
|
||||
pub bredde: f64,
|
||||
pub lengde: f64,
|
||||
pub tidssone: f64,
|
||||
pub hoyde: f64,
|
||||
/// Utetemperatur, °C.
|
||||
pub t_ute: Vec<f64>,
|
||||
/// Relativ fuktighet, %.
|
||||
pub rf: Vec<f64>,
|
||||
/// Global horisontal solstråling, W/m².
|
||||
pub ghi: Vec<f64>,
|
||||
/// Direkte normal solstråling, W/m².
|
||||
pub dni: Vec<f64>,
|
||||
/// Diffus horisontal solstråling, W/m².
|
||||
pub dhi: Vec<f64>,
|
||||
/// Horisontal infrarød stråling fra himmelen, W/m².
|
||||
pub ir_himmel: Vec<f64>,
|
||||
/// Vindhastighet, m/s.
|
||||
pub vind: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Vaerdata {
|
||||
pub fn fra_fil(sti: &Path) -> Result<Self> {
|
||||
let innhold = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&innhold);
|
||||
Self::fra_tekst(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str) -> Result<Self> {
|
||||
let mut linjer = tekst.lines();
|
||||
let location = linjer.next().context("EPW mangler LOCATION-linje")?;
|
||||
let f: Vec<&str> = location.split(',').collect();
|
||||
if f.len() < 10 || !f[0].eq_ignore_ascii_case("LOCATION") {
|
||||
bail!("første linje er ikke en gyldig LOCATION-linje");
|
||||
}
|
||||
let tall =
|
||||
|i: usize| -> f64 { f.get(i).and_then(|s| s.trim().parse().ok()).unwrap_or(0.0) };
|
||||
let mut v = Vaerdata {
|
||||
sted: f[1].trim().to_string(),
|
||||
land: f.get(3).unwrap_or(&"").trim().to_string(),
|
||||
bredde: tall(6),
|
||||
lengde: tall(7),
|
||||
tidssone: tall(8),
|
||||
hoyde: tall(9),
|
||||
t_ute: Vec::with_capacity(TIMER),
|
||||
rf: Vec::with_capacity(TIMER),
|
||||
ghi: Vec::with_capacity(TIMER),
|
||||
dni: Vec::with_capacity(TIMER),
|
||||
dhi: Vec::with_capacity(TIMER),
|
||||
ir_himmel: Vec::with_capacity(TIMER),
|
||||
vind: Vec::with_capacity(TIMER),
|
||||
};
|
||||
// Sju gjenstående headerlinjer.
|
||||
for _ in 0..7 {
|
||||
linjer.next();
|
||||
}
|
||||
for linje in linjer {
|
||||
if linje.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let d: Vec<&str> = linje.split(',').collect();
|
||||
if d.len() < 22 {
|
||||
continue;
|
||||
}
|
||||
let g = |i: usize| -> f64 { d[i].trim().parse::<f64>().unwrap_or(0.0) };
|
||||
// EPW bruker 9999/999999 som «mangler».
|
||||
let mangler = |x: f64, grense: f64| if x >= grense { 0.0 } else { x };
|
||||
v.t_ute.push(g(6));
|
||||
v.rf.push(g(8));
|
||||
v.ir_himmel.push(mangler(g(12), 9999.0));
|
||||
v.ghi.push(mangler(g(13), 9999.0).max(0.0));
|
||||
v.dni.push(mangler(g(14), 9999.0).max(0.0));
|
||||
v.dhi.push(mangler(g(15), 9999.0).max(0.0));
|
||||
v.vind.push(mangler(g(21), 999.0));
|
||||
if v.t_ute.len() == TIMER {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if v.t_ute.len() != TIMER {
|
||||
bail!(
|
||||
"EPW inneholder {} timer, forventet {}",
|
||||
v.t_ute.len(),
|
||||
TIMER
|
||||
);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn aarsmiddeltemperatur(&self) -> f64 {
|
||||
self.t_ute.iter().sum::<f64>() / self.t_ute.len() as f64
|
||||
}
|
||||
|
||||
pub fn maanedsmiddeltemperatur(&self) -> [f64; 12] {
|
||||
let mut sum = [0.0; 12];
|
||||
let mut antall = [0.0; 12];
|
||||
for (t, v) in self.t_ute.iter().enumerate() {
|
||||
let m = crate::maaned_for_time(t);
|
||||
sum[m] += v;
|
||||
antall[m] += 1.0;
|
||||
}
|
||||
let mut ut = [0.0; 12];
|
||||
for m in 0..12 {
|
||||
ut[m] = if antall[m] > 0.0 {
|
||||
sum[m] / antall[m]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Graddager under 17 °C, til informasjon i rapporten.
|
||||
pub fn graddager(&self, basis: f64) -> f64 {
|
||||
self.t_ute
|
||||
.chunks(24)
|
||||
.map(|d| {
|
||||
let m = d.iter().sum::<f64>() / d.len() as f64;
|
||||
(basis - m).max(0.0)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! Klimakorrigering av beregnet energiytelse til referanseklima (Oslo).
|
||||
//!
|
||||
//! NS 3031:2025 fjernet normert Oslo-klima fra selve beregningen og innførte i stedet
|
||||
//! klimakorrigeringsfaktorer: `EP_NORM = EP_LOK / KF`, der korrigeringen gjelder de
|
||||
//! klimaavhengige energipostene (romoppvarming, ventilasjonsvarme, romkjøling,
|
||||
//! ventilasjonskjøling). Øvrige poster er uendret.
|
||||
|
||||
use crate::data::Klimakorreksjon;
|
||||
use crate::modell::Kategori;
|
||||
|
||||
/// Resultat av klimakorrigering for én bygning.
|
||||
#[derive(serde::Serialize, Clone, Debug)]
|
||||
pub struct Korrigering {
|
||||
pub sted: String,
|
||||
pub faktor: f64,
|
||||
/// Klimaavhengige poster før korrigering, kWh/m².
|
||||
pub klimaavhengig_lokalt: f64,
|
||||
/// Klimaavhengige poster etter korrigering, kWh/m².
|
||||
pub klimaavhengig_normert: f64,
|
||||
}
|
||||
|
||||
impl Korrigering {
|
||||
/// Korrigerer en total energiytelse: trekker ut de klimaavhengige postene, deler på KF,
|
||||
/// og legger dem tilbake.
|
||||
pub fn korriger_total(&self, total_lokalt: f64) -> f64 {
|
||||
total_lokalt - self.klimaavhengig_lokalt + self.klimaavhengig_normert
|
||||
}
|
||||
}
|
||||
|
||||
pub fn korriger(
|
||||
tabell: &Klimakorreksjon,
|
||||
sted: &str,
|
||||
kategori: Kategori,
|
||||
klimaavhengig_lokalt: f64,
|
||||
) -> Option<Korrigering> {
|
||||
let faktor = tabell.faktor(sted, kategori)?;
|
||||
Some(Korrigering {
|
||||
sted: sted.to_string(),
|
||||
faktor,
|
||||
klimaavhengig_lokalt,
|
||||
klimaavhengig_normert: klimaavhengig_lokalt / faktor,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Klimadata: TMY-filer (EPW) for norske kommuner, solberegning og klimakorrigering.
|
||||
|
||||
pub mod epw;
|
||||
pub mod korreksjon;
|
||||
pub mod sol;
|
||||
pub mod tmy;
|
||||
|
||||
pub use epw::Vaerdata;
|
||||
|
||||
/// Referanseklima for energimerket.
|
||||
pub const REFERANSEKLIMA: &str = "Oslo";
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Solberegning etter NS-EN ISO 52010-1: solhøyde og asimut per time, og innstråling
|
||||
//! på en vilkårlig orientert og skrå flate (Perez-lignende anisotrop himmelmodell forenklet
|
||||
//! til Hay–Davies, som ISO 52010-1 tillater når sirkumsolar- og horisontbidrag ikke skilles).
|
||||
|
||||
use crate::klima::Vaerdata;
|
||||
use crate::TIMER;
|
||||
|
||||
const SOLKONSTANT: f64 = 1367.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Solposisjon {
|
||||
/// Solhøyde over horisonten, grader (negativ når sola er under horisonten).
|
||||
pub hoyde: f64,
|
||||
/// Asimut, grader fra nord med klokka (0 N, 90 Ø, 180 S, 270 V).
|
||||
pub asimut: f64,
|
||||
}
|
||||
|
||||
/// Solposisjon for time `t` (0–8759, lokal standardtid, midt i timen).
|
||||
pub fn solposisjon(v: &Vaerdata, t: usize) -> Solposisjon {
|
||||
let dag = (t / 24) as f64 + 1.0;
|
||||
let klokke = (t % 24) as f64 + 0.5;
|
||||
|
||||
// Tidsligning (ISO 52010-1 ligning 2–4).
|
||||
let rd = 360.0 / 365.0 * (dag - 81.0);
|
||||
let te = 0.1645 * (2.0 * rd.to_radians()).sin()
|
||||
- 0.1255 * rd.to_radians().cos()
|
||||
- 0.025 * rd.to_radians().sin();
|
||||
// Lokal soltid: korriger for lengdegradsavvik fra tidssonens meridian.
|
||||
let soltid = klokke + te + (v.lengde - 15.0 * v.tidssone) / 15.0;
|
||||
let timevinkel = 15.0 * (soltid - 12.0);
|
||||
|
||||
// Solens deklinasjon.
|
||||
let deklinasjon = 23.45 * (360.0 / 365.0 * (284.0 + dag)).to_radians().sin();
|
||||
|
||||
let (b, d, w) = (
|
||||
v.bredde.to_radians(),
|
||||
deklinasjon.to_radians(),
|
||||
timevinkel.to_radians(),
|
||||
);
|
||||
let sin_h = b.sin() * d.sin() + b.cos() * d.cos() * w.cos();
|
||||
let hoyde = sin_h.clamp(-1.0, 1.0).asin();
|
||||
|
||||
// Asimut regnes først fra sør (positiv mot vest) og gjøres så om til fra nord med klokka.
|
||||
let cos_h = hoyde.cos();
|
||||
let asimut = if cos_h.abs() < 1e-9 {
|
||||
180.0
|
||||
} else {
|
||||
let sin_a = w.sin() * d.cos() / cos_h;
|
||||
let cos_a = (sin_h * b.sin() - d.sin()) / (b.cos() * cos_h);
|
||||
let a = sin_a.atan2(cos_a.clamp(-1.0, 1.0)).to_degrees();
|
||||
(180.0 + a).rem_euclid(360.0)
|
||||
};
|
||||
Solposisjon {
|
||||
hoyde: hoyde.to_degrees(),
|
||||
asimut,
|
||||
}
|
||||
}
|
||||
|
||||
/// Innfallsvinkelens cosinus mot en flate med gitt helning og orientering.
|
||||
pub fn cos_innfallsvinkel(sol: Solposisjon, helning: f64, orientering: f64) -> f64 {
|
||||
let h = sol.hoyde.to_radians();
|
||||
let a = sol.asimut.to_radians();
|
||||
let t = helning.to_radians();
|
||||
let o = orientering.to_radians();
|
||||
(h.sin() * t.cos() + h.cos() * t.sin() * (a - o).cos()).max(0.0)
|
||||
}
|
||||
|
||||
/// Solinnstråling på en flate, W/m². `albedo` er bakkerefleksjonen (typisk 0,2; 0,7 med snø).
|
||||
pub fn innstraaling(v: &Vaerdata, t: usize, helning: f64, orientering: f64, albedo: f64) -> f64 {
|
||||
let sol = solposisjon(v, t);
|
||||
if sol.hoyde <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let cos_i = cos_innfallsvinkel(sol, helning, orientering);
|
||||
let sin_h = sol.hoyde.to_radians().sin().max(0.017); // begrenset for lave solhøyder
|
||||
|
||||
let dni = v.dni[t];
|
||||
let dhi = v.dhi[t];
|
||||
let ghi = v.ghi[t];
|
||||
|
||||
// Ekstraterrestriell normalstråling for anisotropi-indeksen.
|
||||
let dag = (t / 24) as f64 + 1.0;
|
||||
let e0 = SOLKONSTANT * (1.0 + 0.033 * (360.0 * dag / 365.0).to_radians().cos());
|
||||
let ai = (dni / e0).clamp(0.0, 1.0); // Hay–Davies anisotropiindeks
|
||||
|
||||
let direkte = dni * cos_i;
|
||||
let t_rad = helning.to_radians();
|
||||
let diffus = dhi * (ai * cos_i / sin_h + (1.0 - ai) * (1.0 + t_rad.cos()) / 2.0);
|
||||
let reflektert = ghi * albedo * (1.0 - t_rad.cos()) / 2.0;
|
||||
|
||||
(direkte + diffus.max(0.0) + reflektert).max(0.0)
|
||||
}
|
||||
|
||||
/// Årsserie med innstråling på én flate, W/m².
|
||||
pub fn innstraaling_aar(v: &Vaerdata, helning: f64, orientering: f64, albedo: f64) -> Vec<f64> {
|
||||
(0..TIMER)
|
||||
.map(|t| innstraaling(v, t, helning, orientering, albedo))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Himmelens ekvivalente temperatur for langbølget utstråling, °C.
|
||||
/// Brukes til å beregne ekstra varmetap fra utvendige flater mot klar himmel.
|
||||
pub fn himmeltemperatur(v: &Vaerdata, t: usize) -> f64 {
|
||||
let ir = v.ir_himmel[t];
|
||||
if ir > 1.0 {
|
||||
// Stefan–Boltzmann invertert.
|
||||
(ir / 5.67e-8).powf(0.25) - 273.15
|
||||
} else {
|
||||
// Uten måledata: forenklet sammenheng med utetemperatur (ISO 52016-1 tillegg).
|
||||
v.t_ute[t] - 11.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Henter og hurtiglagrer TMY-filer for norske kommuner.
|
||||
//!
|
||||
//! Datasettet «TMY files for Norway based on CERRA data from 1991-2020» (Zenodo,
|
||||
//! doi 10.5281/zenodo.14779899, CC-BY-4.0) er laget i tilknytning til NS 3031:2025 og dekker
|
||||
//! alle norske kommuner med timesverdier. Zip-filen lastes ned én gang til
|
||||
//! `$XDG_CACHE_HOME/enok` og pakkes ut derfra.
|
||||
|
||||
use crate::data::normaliser_stedsnavn;
|
||||
use crate::klima::Vaerdata;
|
||||
use crate::Result;
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const ZIP_URL: &str =
|
||||
"https://zenodo.org/api/records/19566253/files/EPW-TMYNO_CERRA_1991-2020.zip/content";
|
||||
const ZIP_NAVN: &str = "EPW-TMYNO_CERRA_1991-2020.zip";
|
||||
|
||||
pub fn hurtiglager() -> PathBuf {
|
||||
std::env::var_os("ENOK_CACHE")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("XDG_CACHE_HOME").map(|c| PathBuf::from(c).join("enok")))
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache/enok")))
|
||||
.unwrap_or_else(|| PathBuf::from(".enok-cache"))
|
||||
}
|
||||
|
||||
fn zip_sti() -> PathBuf {
|
||||
hurtiglager().join(ZIP_NAVN)
|
||||
}
|
||||
|
||||
/// Laster ned TMY-arkivet dersom det ikke finnes lokalt.
|
||||
pub fn sikre_arkiv() -> Result<PathBuf> {
|
||||
let sti = zip_sti();
|
||||
if sti.exists() && std::fs::metadata(&sti)?.len() > 1_000_000 {
|
||||
return Ok(sti);
|
||||
}
|
||||
std::fs::create_dir_all(sti.parent().unwrap())?;
|
||||
eprintln!(
|
||||
"Laster ned TMY-klimadata (74 MB) fra Zenodo til {} …",
|
||||
sti.display()
|
||||
);
|
||||
let svar = ureq::get(ZIP_URL)
|
||||
.timeout(std::time::Duration::from_secs(900))
|
||||
.call()
|
||||
.context("nedlasting av TMY-arkivet feilet")?;
|
||||
let mut data = Vec::new();
|
||||
svar.into_reader().read_to_end(&mut data)?;
|
||||
if data.len() < 1_000_000 {
|
||||
bail!("nedlastet arkiv er uventet lite ({} byte)", data.len());
|
||||
}
|
||||
std::fs::write(&sti, &data)?;
|
||||
Ok(sti)
|
||||
}
|
||||
|
||||
/// Navnene på alle kommuner i arkivet.
|
||||
pub fn kommuner() -> Result<Vec<String>> {
|
||||
let sti = sikre_arkiv()?;
|
||||
let fil = std::fs::File::open(&sti)?;
|
||||
let mut zip = zip::ZipArchive::new(fil)?;
|
||||
let mut ut = Vec::new();
|
||||
for i in 0..zip.len() {
|
||||
let navn = zip.by_index(i)?.name().to_string();
|
||||
if let Some(k) = kommune_av_filnavn(&navn) {
|
||||
ut.push(k);
|
||||
}
|
||||
}
|
||||
ut.sort();
|
||||
Ok(ut)
|
||||
}
|
||||
|
||||
fn kommune_av_filnavn(navn: &str) -> Option<String> {
|
||||
let base = Path::new(navn).file_name()?.to_str()?;
|
||||
let base = base.strip_suffix(".epw")?;
|
||||
let base = base.strip_prefix("TMYNO_")?;
|
||||
let base = base.strip_suffix("_CERRA_1991-2020")?;
|
||||
Some(base.to_string())
|
||||
}
|
||||
|
||||
/// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både
|
||||
/// «Indre Østfold» og «Indre Ostfold» treffer samme fil.
|
||||
pub fn for_kommune(kommune: &str) -> Result<Vaerdata> {
|
||||
let sti = sikre_arkiv()?;
|
||||
let fil = std::fs::File::open(&sti)?;
|
||||
let mut zip = zip::ZipArchive::new(fil)?;
|
||||
let mål = normaliser_stedsnavn(kommune);
|
||||
let mut treff = None;
|
||||
for i in 0..zip.len() {
|
||||
let navn = zip.by_index(i)?.name().to_string();
|
||||
if let Some(k) = kommune_av_filnavn(&navn) {
|
||||
if normaliser_stedsnavn(&k) == mål {
|
||||
treff = Some((i, k));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (indeks, navn) = treff.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"fant ingen TMY-fil for kommunen «{}» (bruk `enok klima --liste` for gyldige navn)",
|
||||
kommune
|
||||
)
|
||||
})?;
|
||||
let mut oppf = zip.by_index(indeks)?;
|
||||
let mut data = Vec::new();
|
||||
oppf.read_to_end(&mut data)?;
|
||||
let tekst = String::from_utf8_lossy(&data);
|
||||
let mut v = Vaerdata::fra_tekst(&tekst).with_context(|| format!("TMY-fil for {navn}"))?;
|
||||
v.sted = navn;
|
||||
Ok(v)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
//! enok – årlig energiberegning for bygninger etter NS 3031 med timesmetode (ISO 52016-1).
|
||||
//!
|
||||
//! Modulene følger beregningsgangen i standarden:
|
||||
//! `modell` (inndata) → `klima` (TMY + sol) → `motor` (netto energibehov, beregningspunkt A)
|
||||
//! → `systemer` (brutto, tilført, levert og vektet energi, B–E) → `merking` (energikarakter)
|
||||
//! → `rapport` (CSV, JSON, Typst/PDF, Enova-XML).
|
||||
|
||||
pub mod data;
|
||||
pub mod inndata;
|
||||
pub mod kjor;
|
||||
pub mod klima;
|
||||
pub mod merking;
|
||||
pub mod modell;
|
||||
pub mod motor;
|
||||
pub mod rapport;
|
||||
pub mod systemer;
|
||||
|
||||
pub use anyhow::{Context, Result};
|
||||
pub use modell::lost::Lost;
|
||||
|
||||
/// Antall timer i beregningsåret.
|
||||
pub const TIMER: usize = 8760;
|
||||
|
||||
/// Måneder (0-basert) for hver time i året.
|
||||
pub fn maaned_for_time(t: usize) -> usize {
|
||||
const DAGER: [usize; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut dag = t / 24;
|
||||
for (m, d) in DAGER.iter().enumerate() {
|
||||
if dag < *d {
|
||||
return m;
|
||||
}
|
||||
dag -= d;
|
||||
}
|
||||
11
|
||||
}
|
||||
|
||||
pub const MAANEDER: [&str; 12] = [
|
||||
"jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "des",
|
||||
];
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
//! enok — årlig energiberegning og energirapport for bygninger etter NS 3031.
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use enok::data::Datakilde;
|
||||
use enok::inndata;
|
||||
use enok::kjor;
|
||||
use enok::rapport;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "enok",
|
||||
version,
|
||||
about = "Årlig energiberegning og energirapport for bygninger etter NS 3031:2025",
|
||||
long_about = "Beregner timesvis energibehov for et helt år etter NS 3031:2025 (NS-EN ISO 52016-1), \
|
||||
fastsetter energikarakter etter Enovas skala fra 1.1.2026, og skriver rapport som \
|
||||
Typst/PDF med CSV- og JSON-vedlegg."
|
||||
)]
|
||||
struct Cli {
|
||||
/// Katalog som overstyrer de innebygde datafilene (skala, normerte verdier, materialer).
|
||||
#[arg(long, global = true, value_name = "KATALOG")]
|
||||
data: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
kommando: Kommando,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Kommando {
|
||||
/// Beregner energibehov og skriver rapport.
|
||||
Beregn(Beregn),
|
||||
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
|
||||
Import(Import),
|
||||
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
|
||||
Klima(Klima),
|
||||
/// Viser energikarakterskalaen for en bygningskategori.
|
||||
Skala(Skala),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct Beregn {
|
||||
/// Bygningsbeskrivelse (bygning.toml).
|
||||
fil: PathBuf,
|
||||
/// Utdatakatalog. Standard: `ut/<bygningsnavn>`.
|
||||
#[arg(short, long, value_name = "KATALOG")]
|
||||
ut: Option<PathBuf>,
|
||||
/// Hopper over PDF-kompilering (skriver bare rapport.typ).
|
||||
#[arg(long)]
|
||||
ingen_pdf: bool,
|
||||
/// Skriver også XML for Enovas energimerkesystem.
|
||||
#[arg(long)]
|
||||
enova_xml: bool,
|
||||
/// Hopper over timeserie.csv (8760 rader).
|
||||
#[arg(long)]
|
||||
ingen_timeserie: bool,
|
||||
/// Beregner bare hovedalternativet, uten scenarioene i filen.
|
||||
#[arg(long)]
|
||||
uten_scenarier: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct Import {
|
||||
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
|
||||
fil: PathBuf,
|
||||
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
||||
#[arg(short, long, value_name = "FIL")]
|
||||
ut: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct Klima {
|
||||
/// Lister alle kommuner med klimadata.
|
||||
#[arg(long)]
|
||||
liste: bool,
|
||||
/// Viser nøkkeltall for én kommune.
|
||||
#[arg(long, value_name = "KOMMUNE")]
|
||||
kommune: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct Skala {
|
||||
/// Bygningskategori, for eksempel smaahus eller boligblokk.
|
||||
#[arg(default_value = "smaahus")]
|
||||
kategori: String,
|
||||
/// Oppvarmet BRA for arealkorreksjonen, m².
|
||||
#[arg(long, default_value_t = 160.0)]
|
||||
areal: f64,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let kilde = Datakilde::ny(cli.data.clone());
|
||||
match cli.kommando {
|
||||
Kommando::Beregn(a) => beregn(a, &kilde),
|
||||
Kommando::Import(a) => import(a),
|
||||
Kommando::Klima(a) => klima(a),
|
||||
Kommando::Skala(a) => skala(a, &kilde),
|
||||
}
|
||||
}
|
||||
|
||||
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
|
||||
let bygning = inndata::les_bygning(&a.fil)?;
|
||||
let ut =
|
||||
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
|
||||
std::fs::create_dir_all(&ut)?;
|
||||
|
||||
eprintln!("Henter klimadata …");
|
||||
let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?;
|
||||
eprintln!("Beregner {} …", bygning.bygning.navn);
|
||||
let lost = enok::Lost::fra(&bygning, kilde)?;
|
||||
let resultat = kjor::kjor_med_klima(
|
||||
&bygning,
|
||||
kilde,
|
||||
lost,
|
||||
vaerdata.clone(),
|
||||
klimanavn.clone(),
|
||||
er_referanse,
|
||||
)?;
|
||||
|
||||
let mut scenarier = Vec::new();
|
||||
if !a.uten_scenarier {
|
||||
for s in &bygning.scenario {
|
||||
eprintln!("Beregner scenario «{}» …", s.navn);
|
||||
let endret = inndata::slaa_sammen_scenario(&bygning, s)?;
|
||||
let lost = enok::Lost::fra(&endret, kilde)?;
|
||||
let r = kjor::kjor_med_klima(
|
||||
&endret,
|
||||
kilde,
|
||||
lost,
|
||||
vaerdata.clone(),
|
||||
klimanavn.clone(),
|
||||
er_referanse,
|
||||
)?;
|
||||
scenarier.push((s.navn.clone(), r));
|
||||
}
|
||||
}
|
||||
|
||||
rapport::json_ut::skriv(&resultat, &ut.join("resultat.json"))?;
|
||||
rapport::csv_ut::maanedsverdier(&resultat, &ut.join("maaned.csv"))?;
|
||||
if !a.ingen_timeserie {
|
||||
rapport::csv_ut::timeserie(&resultat, &ut.join("timeserie.csv"))?;
|
||||
}
|
||||
if a.enova_xml {
|
||||
rapport::enova_xml::skriv(&resultat, &ut.join("enova-2014.xml"))?;
|
||||
}
|
||||
rapport::typst_ut::skriv(&resultat, &ut, &scenarier)?;
|
||||
if !a.ingen_pdf {
|
||||
let pdf = rapport::typst_ut::kompiler(&ut)?;
|
||||
eprintln!("Rapport: {}", pdf.display());
|
||||
}
|
||||
|
||||
skriv_sammendrag(&resultat, &scenarier);
|
||||
eprintln!("Filer skrevet til {}", ut.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)]) {
|
||||
let l = &r.lost;
|
||||
println!();
|
||||
println!(
|
||||
" {} ({}, {} m², byggeår {})",
|
||||
l.navn,
|
||||
r.karakter.kategorinavn,
|
||||
l.bra.round(),
|
||||
l.byggeaar
|
||||
);
|
||||
println!(" Klimadata: {}", r.klimanavn);
|
||||
println!();
|
||||
println!(
|
||||
" Varmetapstall {:>8.2} W/(m²K)",
|
||||
r.varmetap.totalt
|
||||
);
|
||||
println!(
|
||||
" Netto energibehov {:>8.1} kWh/(m²·år)",
|
||||
r.budsjett.netto_spesifikk(l.bra)
|
||||
);
|
||||
println!(
|
||||
" Levert energi {:>8.1} kWh/(m²·år)",
|
||||
r.levert.levert_totalt / l.bra
|
||||
);
|
||||
println!(
|
||||
" Vektet levert energi {:>8.1} kWh/(m²·år)",
|
||||
r.levert.spesifikk_vektet
|
||||
);
|
||||
println!(" Energikarakter {}", r.karakter.bokstav);
|
||||
if let Some(t) = r.karakter.til_neste {
|
||||
println!(" ({:.0} kWh/(m²·år) fra neste karakter)", t);
|
||||
}
|
||||
if !scenarier.is_empty() {
|
||||
println!();
|
||||
println!(" Tiltak:");
|
||||
for (navn, s) in scenarier {
|
||||
println!(
|
||||
" {:<34} {:>6.1} kWh/(m²·år) {}",
|
||||
navn, s.levert.spesifikk_vektet, s.karakter.bokstav
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn import(a: Import) -> Result<()> {
|
||||
let endelse = a
|
||||
.fil
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let (bygning, merknader) = match endelse.as_str() {
|
||||
"gbxml" | "xml" => {
|
||||
let r = inndata::gbxml::les(&a.fil)?;
|
||||
(r.bygning, r.merknader)
|
||||
}
|
||||
"ifc" => {
|
||||
let r = inndata::ifc::les(&a.fil)?;
|
||||
(r.bygning, r.merknader)
|
||||
}
|
||||
"csv" => {
|
||||
let r = inndata::regneark::les(&a.fil)?;
|
||||
(r.bygning, r.merknader)
|
||||
}
|
||||
annen => bail!(
|
||||
"ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
|
||||
),
|
||||
};
|
||||
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
||||
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('\n');
|
||||
innhold.push_str(&tekst);
|
||||
std::fs::write(&ut, innhold)?;
|
||||
println!("Skrev {}", ut.display());
|
||||
for m in &merknader {
|
||||
println!(" • {m}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn klima(a: Klima) -> Result<()> {
|
||||
if a.liste {
|
||||
for k in enok::klima::tmy::kommuner()? {
|
||||
println!("{k}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let kommune = a
|
||||
.kommune
|
||||
.unwrap_or_else(|| enok::klima::REFERANSEKLIMA.to_string());
|
||||
let v = enok::klima::tmy::for_kommune(&kommune)?;
|
||||
let m = v.maanedsmiddeltemperatur();
|
||||
println!(
|
||||
"{} ({:.4}° N, {:.4}° Ø, {:.0} moh.)",
|
||||
v.sted, v.bredde, v.lengde, v.hoyde
|
||||
);
|
||||
println!("Årsmiddeltemperatur {:.1} °C", v.aarsmiddeltemperatur());
|
||||
println!("Graddager under 17 °C {:.0}", v.graddager(17.0));
|
||||
println!(
|
||||
"Global solinnstråling {:.0} kWh/(m²·år)",
|
||||
v.ghi.iter().sum::<f64>() / 1000.0
|
||||
);
|
||||
println!();
|
||||
println!("Månedsmiddeltemperatur, °C");
|
||||
for (i, navn) in enok::MAANEDER.iter().enumerate() {
|
||||
println!(" {navn} {:>6.1}", m[i]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn skala(a: Skala, kilde: &Datakilde) -> Result<()> {
|
||||
let skala = kilde.skala()?;
|
||||
let k = skala
|
||||
.0
|
||||
.get(a.kategori.as_str())
|
||||
.with_context(|| format!("ukjent kategori «{}»", a.kategori))?;
|
||||
let kar = enok::merking::karakter(k, f64::NAN, a.areal);
|
||||
println!("{} (arealkorreksjon for {:.0} m²)", k.navn, a.areal);
|
||||
println!();
|
||||
for (i, bokstav) in enok::merking::skala::BOKSTAVER.iter().enumerate() {
|
||||
if i < 6 {
|
||||
println!(" {bokstav} ≤ {:>6.1} kWh/(m²·år)", kar.grenser[i]);
|
||||
} else {
|
||||
println!(" {bokstav} > {:>6.1} kWh/(m²·år)", kar.grenser[5]);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn filnavnvennlig(s: &str) -> String {
|
||||
s.chars()
|
||||
.map(|c| match c {
|
||||
'æ' => 'a',
|
||||
'ø' => 'o',
|
||||
'å' => 'a',
|
||||
'Æ' => 'A',
|
||||
'Ø' => 'O',
|
||||
'Å' => 'A',
|
||||
c if c.is_alphanumeric() => c.to_ascii_lowercase(),
|
||||
_ => '-',
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn sjekk_sti(p: &Path) -> Result<()> {
|
||||
if !p.exists() {
|
||||
bail!("finner ikke {}", p.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Energikarakter etter energimerkeforskriften § 10b og Enovas skala fra 1.1.2026.
|
||||
|
||||
pub mod skala;
|
||||
pub use skala::{karakter, Energikarakter};
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Oppslag i energikarakterskalaen med arealkorreksjon.
|
||||
//!
|
||||
//! Skalaen gir øvre grense for hver karakter i kWh/m² spesifikk vektet levert energi.
|
||||
//! For småhus og boligblokk legges en arealkorreksjon `k/A` til hver grense, der A er
|
||||
//! oppvarmet BRA etter reglene i Enovas notat: for småhus hele bygningens oppvarmede BRA,
|
||||
//! for boligblokk blokkens BRA delt på antall boenheter (eller boenhetens eget areal).
|
||||
|
||||
use crate::data::SkalaKategori;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const BOKSTAVER: [&str; 7] = ["A", "B", "C", "D", "E", "F", "G"];
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Energikarakter {
|
||||
pub bokstav: &'static str,
|
||||
pub spesifikk_vektet: f64,
|
||||
/// Grensene etter arealkorreksjon, kWh/m².
|
||||
pub grenser: Vec<f64>,
|
||||
pub kategorinavn: String,
|
||||
/// Arealet som ble brukt i arealkorreksjonen.
|
||||
pub korreksjonsareal: f64,
|
||||
/// Hvor mye energiytelsen må ned for å nå neste karakter, kWh/m² (None for A).
|
||||
pub til_neste: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn karakter(skala: &SkalaKategori, spesifikk_vektet: f64, areal: f64) -> Energikarakter {
|
||||
let mut grenser = Vec::with_capacity(6);
|
||||
for i in 0..6 {
|
||||
let korreksjon = skala
|
||||
.arealkorreksjon
|
||||
.map(|k| if areal > 0.0 { k[i] / areal } else { 0.0 })
|
||||
.unwrap_or(0.0);
|
||||
grenser.push(skala.grenser[i] + korreksjon);
|
||||
}
|
||||
let indeks = grenser
|
||||
.iter()
|
||||
.position(|g| spesifikk_vektet <= *g + 1e-9)
|
||||
.unwrap_or(6);
|
||||
let til_neste = if indeks == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(spesifikk_vektet - grenser[indeks - 1])
|
||||
};
|
||||
Energikarakter {
|
||||
bokstav: BOKSTAVER[indeks],
|
||||
spesifikk_vektet,
|
||||
grenser,
|
||||
kategorinavn: skala.navn.clone(),
|
||||
korreksjonsareal: areal,
|
||||
til_neste,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
fn smaahus() -> SkalaKategori {
|
||||
SkalaKategori {
|
||||
navn: "Småhus".into(),
|
||||
grenser: [85.0, 95.0, 155.0, 210.0, 265.0, 315.0],
|
||||
arealkorreksjon: Some([800.0, 1600.0, 2500.0, 4100.0, 5800.0, 8000.0]),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arealkorreksjon_flytter_grensene() {
|
||||
let s = smaahus();
|
||||
// 160 m²: A-grensen blir 85 + 800/160 = 90.
|
||||
let k = karakter(&s, 90.0, 160.0);
|
||||
assert_eq!(k.bokstav, "A");
|
||||
let k = karakter(&s, 90.1, 160.0);
|
||||
assert_eq!(k.bokstav, "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lite_hus_faar_romsligere_grenser() {
|
||||
let s = smaahus();
|
||||
// Et lite hus (80 m²) får A-grense 85 + 10 = 95.
|
||||
let k = karakter(&s, 94.0, 80.0);
|
||||
assert_eq!(k.bokstav, "A");
|
||||
// Samme ytelse i et stort hus (400 m²) gir A-grense 87 → karakter B.
|
||||
let k = karakter(&s, 94.0, 400.0);
|
||||
assert_eq!(k.bokstav, "B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_f_gir_g() {
|
||||
let s = smaahus();
|
||||
let k = karakter(&s, 500.0, 160.0);
|
||||
assert_eq!(k.bokstav, "G");
|
||||
assert!(k.til_neste.unwrap() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uten_arealkorreksjon() {
|
||||
let s = SkalaKategori {
|
||||
navn: "Kontorbygning".into(),
|
||||
grenser: [75.0, 90.0, 140.0, 190.0, 235.0, 285.0],
|
||||
arealkorreksjon: None,
|
||||
};
|
||||
assert_eq!(karakter(&s, 75.0, 3600.0).bokstav, "A");
|
||||
assert_eq!(karakter(&s, 75.01, 3600.0).bokstav, "B");
|
||||
assert_eq!(karakter(&s, 285.0, 3600.0).bokstav, "F");
|
||||
assert_eq!(karakter(&s, 285.1, 3600.0).bokstav, "G");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
//! Oppløsning av inndata: fyller manglende felter med normerte verdier eller verdier utledet
|
||||
//! av byggeår, og fører logg over hva som er antatt. Loggen gjengis i rapporten slik at leseren
|
||||
//! ser hvilke tall som er dokumentert og hvilke som er normerte.
|
||||
|
||||
use crate::data::{Datakilde, Materialer, Normerte};
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Én forutsetning som ikke kom fra inndatafilen.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Forutsetning {
|
||||
pub felt: String,
|
||||
pub verdi: String,
|
||||
pub kilde: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Regulering {
|
||||
/// Settpunktet gjelder operativ temperatur (middel av luft- og strålingstemperatur).
|
||||
Operativ,
|
||||
/// Settpunktet gjelder lufttemperatur.
|
||||
Luft,
|
||||
}
|
||||
|
||||
/// En ferdig oppløst flate klar for timesberegning.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct LostFlate {
|
||||
pub navn: String,
|
||||
pub typ: FlateType,
|
||||
pub areal: f64,
|
||||
pub u_verdi: f64,
|
||||
pub helning: f64,
|
||||
pub orientering: f64,
|
||||
pub kappa: f64,
|
||||
pub masseplassering: Masseplassering,
|
||||
pub mot: Mot,
|
||||
pub b_faktor: f64,
|
||||
pub absorptans: f64,
|
||||
/// Sant for vinduer og dører (to noder, ingen masse, solinnstråling gjennom glasset).
|
||||
pub glass: bool,
|
||||
/// Effektiv solfaktor inkl. karmandel og skjerming (bare vinduer).
|
||||
pub g_effektiv: f64,
|
||||
/// Skjermingsfaktor når solskjerming er i bruk (1,0 = ingen skjerming).
|
||||
pub skjermingsfaktor: f64,
|
||||
pub automatisk_skjerming: bool,
|
||||
pub horisont: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Lost {
|
||||
pub navn: String,
|
||||
pub kategori: Kategori,
|
||||
pub byggeaar: i32,
|
||||
pub bra: f64,
|
||||
pub volum: f64,
|
||||
pub antall_boenheter: u32,
|
||||
/// Areal som brukes i arealkorreksjonen på karakterskalaen.
|
||||
pub skala_areal: f64,
|
||||
pub flater: Vec<LostFlate>,
|
||||
/// W/K, summert transmisjonstap uten kuldebroer.
|
||||
pub kuldebro_w_per_k: f64,
|
||||
pub n50: f64,
|
||||
pub skjerming: Skjerming,
|
||||
pub ventilasjon: LostVentilasjon,
|
||||
pub laster: LostLaster,
|
||||
pub settpunkt_oppvarming: f64,
|
||||
pub settpunkt_natt: f64,
|
||||
pub settpunkt_kjoling: Option<f64>,
|
||||
/// Vinduslufting: (settpunkt °C, største luftmengde m³/(h·m²)).
|
||||
pub lufting: Option<(f64, f64)>,
|
||||
pub regulering: Regulering,
|
||||
pub innvendig_varmekapasitet: f64,
|
||||
pub maks_varmeeffekt: Option<f64>,
|
||||
pub maks_kjoleeffekt: Option<f64>,
|
||||
pub albedo: f64,
|
||||
pub forutsetninger: Vec<Forutsetning>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct LostVentilasjon {
|
||||
pub typ: VentilasjonType,
|
||||
/// m³/(h·m²) i driftstiden.
|
||||
pub luftmengde: f64,
|
||||
pub luftmengde_natt: f64,
|
||||
pub gjenvinning: f64,
|
||||
pub sfp: f64,
|
||||
pub varmebatteri: bool,
|
||||
pub tilluft_settpunkt: f64,
|
||||
pub driftstid: crate::data::Tidsrom,
|
||||
}
|
||||
|
||||
/// Timesprofiler for interne laster, W/m² oppvarmet BRA.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct LostLaster {
|
||||
pub belysning: [f64; 24],
|
||||
pub utstyr_effekt: [f64; 24],
|
||||
pub utstyr_varme: [f64; 24],
|
||||
pub personer: [f64; 24],
|
||||
pub varmtvann: [f64; 24],
|
||||
pub pumper: [f64; 24],
|
||||
pub oppvarming_driftstid: crate::data::Tidsrom,
|
||||
}
|
||||
|
||||
const R_SI: f64 = 0.13;
|
||||
const R_SE: f64 = 0.04;
|
||||
|
||||
impl Lost {
|
||||
pub fn fra(b: &Bygning, kilde: &Datakilde) -> Result<Self> {
|
||||
let mut f = Vec::new();
|
||||
let normerte = kilde.normerte(b.bygning.kategori)?;
|
||||
let materialer = kilde.materialer()?;
|
||||
let info = &b.bygning;
|
||||
|
||||
if info.oppvarmet_bra <= 0.0 {
|
||||
bail!("oppvarmet_bra må være større enn null");
|
||||
}
|
||||
let volum = info.oppvarmet_volum.unwrap_or_else(|| {
|
||||
f.push(Forutsetning {
|
||||
felt: "bygning.oppvarmet_volum".into(),
|
||||
verdi: format!("{:.0} m³", info.oppvarmet_bra * 2.4),
|
||||
kilde: "antatt 2,4 m romhøyde".into(),
|
||||
});
|
||||
info.oppvarmet_bra * 2.4
|
||||
});
|
||||
|
||||
let periode = materialer.periode_for(info.byggeaar);
|
||||
|
||||
let mut flater = Vec::new();
|
||||
for fl in &b.flate {
|
||||
flater.push(los_flate(fl, &materialer, periode, &mut f)?);
|
||||
}
|
||||
for v in &b.vindu {
|
||||
flater.push(los_vindu(v, &mut f));
|
||||
}
|
||||
if flater.is_empty() {
|
||||
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
|
||||
}
|
||||
|
||||
// Kuldebroer.
|
||||
let kuldebro_w_per_k = if !b.kuldebroer.liste.is_empty() {
|
||||
b.kuldebroer.liste.iter().map(|k| k.psi * k.lengde).sum()
|
||||
} else {
|
||||
let n = b.kuldebroer.normalisert.unwrap_or_else(|| {
|
||||
let v = periode
|
||||
.map(|p| p.kuldebro)
|
||||
.unwrap_or(normerte.termisk.kuldebroer_normalisert);
|
||||
f.push(Forutsetning {
|
||||
felt: "kuldebroer.normalisert".into(),
|
||||
verdi: format!("{v:.2} W/(m²K)"),
|
||||
kilde: periode
|
||||
.map(|p| format!("typisk for {}", p.navn))
|
||||
.unwrap_or_else(|| "normert verdi".into()),
|
||||
});
|
||||
v
|
||||
});
|
||||
n * info.oppvarmet_bra
|
||||
};
|
||||
|
||||
// Lekkasjetall.
|
||||
let n50 = b.infiltrasjon.n50.unwrap_or_else(|| {
|
||||
let v = periode.map(|p| p.n50).unwrap_or(3.0);
|
||||
f.push(Forutsetning {
|
||||
felt: "infiltrasjon.n50".into(),
|
||||
verdi: format!("{v:.1} h⁻¹"),
|
||||
kilde: periode
|
||||
.map(|p| format!("typisk for {}", p.navn))
|
||||
.unwrap_or_else(|| "antatt".into()),
|
||||
});
|
||||
v
|
||||
});
|
||||
|
||||
let ventilasjon = los_ventilasjon(&b.ventilasjon, &normerte, &mut f);
|
||||
let laster = los_laster(b, &normerte, &mut f);
|
||||
|
||||
let settpunkt_oppvarming = b
|
||||
.drift
|
||||
.settpunkt_oppvarming
|
||||
.unwrap_or(normerte.settpunkt.oppvarming);
|
||||
let settpunkt_natt = b
|
||||
.drift
|
||||
.nattsenking
|
||||
.unwrap_or(normerte.settpunkt.oppvarming_natt);
|
||||
let settpunkt_kjoling = if b.kjoling.ingen {
|
||||
None
|
||||
} else {
|
||||
Some(b.kjoling.settpunkt.unwrap_or(normerte.settpunkt.kjoling))
|
||||
};
|
||||
|
||||
let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0);
|
||||
|
||||
// Vinduslufting brukes når bygningen ikke har kjøleanlegg.
|
||||
let lufting = if b.lufting.ingen || settpunkt_kjoling.is_some() {
|
||||
None
|
||||
} else {
|
||||
let sett = b.lufting.settpunkt.unwrap_or_else(|| {
|
||||
f.push(Forutsetning {
|
||||
felt: "lufting.settpunkt".into(),
|
||||
verdi: "24 °C".into(),
|
||||
kilde: "antatt grense for vinduslufting i bygning uten kjøling".into(),
|
||||
});
|
||||
24.0
|
||||
});
|
||||
let maks = b.lufting.maks_luftmengde.unwrap_or_else(|| {
|
||||
f.push(Forutsetning {
|
||||
felt: "lufting.maks_luftmengde".into(),
|
||||
verdi: "8 m³/(h·m²)".into(),
|
||||
kilde: "antatt største luftmengde ved vinduslufting".into(),
|
||||
});
|
||||
8.0
|
||||
});
|
||||
Some((sett, maks))
|
||||
};
|
||||
|
||||
let skala_areal = match info.kategori {
|
||||
Kategori::Smaahus => info.bygningens_oppvarmede_bra.unwrap_or(info.oppvarmet_bra),
|
||||
Kategori::Boligblokk => info
|
||||
.bygningens_oppvarmede_bra
|
||||
.map(|a| a / info.antall_boenheter.max(1) as f64)
|
||||
.unwrap_or_else(|| {
|
||||
if info.antall_boenheter > 1 {
|
||||
info.oppvarmet_bra / info.antall_boenheter as f64
|
||||
} else {
|
||||
info.oppvarmet_bra
|
||||
}
|
||||
}),
|
||||
_ => info.oppvarmet_bra,
|
||||
};
|
||||
|
||||
Ok(Lost {
|
||||
navn: info.navn.clone(),
|
||||
kategori: info.kategori,
|
||||
byggeaar: info.byggeaar,
|
||||
bra: info.oppvarmet_bra,
|
||||
volum,
|
||||
antall_boenheter: info.antall_boenheter.max(1),
|
||||
skala_areal,
|
||||
flater,
|
||||
kuldebro_w_per_k,
|
||||
n50,
|
||||
skjerming: b.infiltrasjon.skjerming,
|
||||
ventilasjon,
|
||||
laster,
|
||||
settpunkt_oppvarming,
|
||||
settpunkt_natt,
|
||||
settpunkt_kjoling,
|
||||
lufting,
|
||||
regulering: Regulering::Operativ,
|
||||
innvendig_varmekapasitet,
|
||||
maks_varmeeffekt: b.oppvarming.maks_effekt,
|
||||
maks_kjoleeffekt: b.kjoling.maks_effekt,
|
||||
albedo: b.klima.albedo.unwrap_or(0.2),
|
||||
forutsetninger: f,
|
||||
})
|
||||
}
|
||||
|
||||
/// Samlet areal mot det fri og mot grunn/uoppvarmet (klimaskjermareal), m².
|
||||
pub fn klimaskjermareal(&self) -> f64 {
|
||||
self.flater.iter().map(|f| f.areal).sum()
|
||||
}
|
||||
|
||||
pub fn formfaktor(&self) -> f64 {
|
||||
self.klimaskjermareal() / self.bra
|
||||
}
|
||||
|
||||
pub fn vindusareal(&self) -> f64 {
|
||||
self.flater
|
||||
.iter()
|
||||
.filter(|f| f.glass)
|
||||
.map(|f| f.areal)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
fn los_flate(
|
||||
fl: &Flate,
|
||||
materialer: &Materialer,
|
||||
periode: Option<&crate::data::Periode>,
|
||||
logg: &mut Vec<Forutsetning>,
|
||||
) -> Result<LostFlate> {
|
||||
if fl.areal <= 0.0 {
|
||||
bail!("flaten «{}» har areal {} m²", fl.navn, fl.areal);
|
||||
}
|
||||
// U-verdi: oppgitt, utledet av lag, eller typisk for byggeperioden.
|
||||
let (u, kappa_lag) = if let Some(u) = fl.u_verdi {
|
||||
(u, None)
|
||||
} else if let Some(lag) = &fl.lag {
|
||||
let mut r = R_SI + R_SE;
|
||||
let mut kappa = 0.0;
|
||||
for l in lag {
|
||||
let m = materialer.materiale(&l.materiale).with_context(|| {
|
||||
format!("ukjent materiale «{}» i flaten «{}»", l.materiale, fl.navn)
|
||||
})?;
|
||||
r += l.tykkelse / m.lambda;
|
||||
kappa += l.tykkelse * m.rho * m.c;
|
||||
}
|
||||
(1.0 / r, Some(kappa))
|
||||
} else {
|
||||
let v = periode
|
||||
.map(|p| match fl.typ {
|
||||
FlateType::Vegg => p.vegg,
|
||||
FlateType::Tak => p.tak,
|
||||
FlateType::Gulv => p.gulv,
|
||||
})
|
||||
.with_context(|| format!("flaten «{}» mangler u_verdi og lag", fl.navn))?;
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.u_verdi", fl.navn),
|
||||
verdi: format!("{v:.2} W/(m²K)"),
|
||||
kilde: periode
|
||||
.map(|p| format!("typisk for {}", p.navn))
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
(v, None)
|
||||
};
|
||||
|
||||
let helning = fl.helning.unwrap_or(match fl.typ {
|
||||
FlateType::Vegg => 90.0,
|
||||
FlateType::Tak => 0.0,
|
||||
FlateType::Gulv => 180.0,
|
||||
});
|
||||
|
||||
let kappa = match (kappa_lag, fl.masseklasse) {
|
||||
(_, Some(k)) => k.kappa(),
|
||||
(Some(k), None) => k,
|
||||
(None, None) => {
|
||||
let k = Masseklasse::Middels.kappa();
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.masseklasse", fl.navn),
|
||||
verdi: "middels".into(),
|
||||
kilde: "ISO 52016-1 tabell B.14".into(),
|
||||
});
|
||||
k
|
||||
}
|
||||
};
|
||||
|
||||
let b_faktor = match fl.mot {
|
||||
Mot::Ute => 1.0,
|
||||
Mot::Grunn => 1.0, // håndteres med egen bakketemperatur i timesberegningen
|
||||
Mot::Uoppvarmet => fl.b_faktor.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("flate.{}.b_faktor", fl.navn),
|
||||
verdi: "0,5".into(),
|
||||
kilde: "NS-EN ISO 13789, uoppvarmet rom".into(),
|
||||
});
|
||||
0.5
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(LostFlate {
|
||||
navn: fl.navn.clone(),
|
||||
typ: fl.typ,
|
||||
areal: fl.areal,
|
||||
u_verdi: u,
|
||||
helning,
|
||||
orientering: fl.orientering,
|
||||
kappa,
|
||||
masseplassering: fl.masseplassering,
|
||||
mot: fl.mot,
|
||||
b_faktor,
|
||||
absorptans: fl.absorptans.unwrap_or(0.6),
|
||||
glass: false,
|
||||
g_effektiv: 0.0,
|
||||
skjermingsfaktor: 1.0,
|
||||
automatisk_skjerming: false,
|
||||
horisont: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
||||
let areal = v.areal * v.antall;
|
||||
let karm = v.karmandel.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("vindu.{}.karmandel", v.navn),
|
||||
verdi: "0,20".into(),
|
||||
kilde: "normert karm- og rammeandel".into(),
|
||||
});
|
||||
0.20
|
||||
});
|
||||
let g = if v.dor {
|
||||
0.0
|
||||
} else {
|
||||
v.g_verdi.unwrap_or_else(|| {
|
||||
let antatt = if v.u_verdi <= 1.2 { 0.50 } else { 0.75 };
|
||||
logg.push(Forutsetning {
|
||||
felt: format!("vindu.{}.g_verdi", v.navn),
|
||||
verdi: format!("{antatt:.2}"),
|
||||
kilde: if v.u_verdi <= 1.2 {
|
||||
"antatt 3-lags energiglass"
|
||||
} else {
|
||||
"antatt 2-lags glass"
|
||||
}
|
||||
.into(),
|
||||
});
|
||||
antatt
|
||||
})
|
||||
};
|
||||
// ISO 52016-1: F_w = 0,9 korrigerer for ikke-vinkelrett innfall over året.
|
||||
let g_effektiv = g * (1.0 - karm) * 0.9;
|
||||
let (skjermingsfaktor, automatisk) = match &v.solskjerming {
|
||||
None => (1.0, false),
|
||||
Some(s) => match s.typ {
|
||||
SolskjermingType::Ingen => (1.0, false),
|
||||
SolskjermingType::Manuell => (s.faktor.unwrap_or(0.8), false),
|
||||
SolskjermingType::Automatisk => (s.faktor.unwrap_or(0.4), true),
|
||||
},
|
||||
};
|
||||
LostFlate {
|
||||
navn: v.navn.clone(),
|
||||
typ: FlateType::Vegg,
|
||||
areal,
|
||||
u_verdi: v.u_verdi,
|
||||
helning: v.helning,
|
||||
orientering: v.orientering,
|
||||
kappa: 0.0,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot: Mot::Ute,
|
||||
b_faktor: 1.0,
|
||||
absorptans: 0.0,
|
||||
glass: true,
|
||||
g_effektiv,
|
||||
skjermingsfaktor,
|
||||
automatisk_skjerming: automatisk,
|
||||
horisont: v.horisont.unwrap_or(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostVentilasjon {
|
||||
let luftmengde = v.luftmengde.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: "ventilasjon.luftmengde".into(),
|
||||
verdi: format!("{:.2} m³/(h·m²)", n.ventilasjon.luftmengde_driftstid),
|
||||
kilde: format!("normert verdi, {}", n.kilde),
|
||||
});
|
||||
n.ventilasjon.luftmengde_driftstid
|
||||
});
|
||||
let luftmengde_natt = v
|
||||
.luftmengde_natt
|
||||
.unwrap_or(n.ventilasjon.luftmengde_utenfor_driftstid);
|
||||
let gjenvinning = match v.typ {
|
||||
VentilasjonType::Balansert => v.gjenvinning.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: "ventilasjon.gjenvinning".into(),
|
||||
verdi: format!("{:.0} %", n.ventilasjon.temperaturvirkningsgrad * 100.0),
|
||||
kilde: "normert temperaturvirkningsgrad".into(),
|
||||
});
|
||||
n.ventilasjon.temperaturvirkningsgrad
|
||||
}),
|
||||
_ => 0.0,
|
||||
};
|
||||
let sfp = v.sfp.unwrap_or(match v.typ {
|
||||
VentilasjonType::Balansert => n.ventilasjon.sfp,
|
||||
VentilasjonType::Avtrekk => 0.6,
|
||||
VentilasjonType::Naturlig => 0.0,
|
||||
});
|
||||
let varmebatteri = v
|
||||
.varmebatteri
|
||||
.unwrap_or(matches!(v.typ, VentilasjonType::Balansert));
|
||||
LostVentilasjon {
|
||||
typ: v.typ,
|
||||
luftmengde,
|
||||
luftmengde_natt,
|
||||
gjenvinning,
|
||||
sfp,
|
||||
varmebatteri,
|
||||
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
|
||||
driftstid: n.driftstid.ventilasjon,
|
||||
}
|
||||
}
|
||||
|
||||
fn profil(effekt: f64, tid: crate::data::Tidsrom, egen: Option<[f64; 24]>) -> [f64; 24] {
|
||||
if let Some(p) = egen {
|
||||
return p;
|
||||
}
|
||||
let mut ut = [0.0; 24];
|
||||
for (h, v) in ut.iter_mut().enumerate() {
|
||||
if tid.aktiv(h) {
|
||||
*v = effekt;
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLaster {
|
||||
let bel = b.drift.belysning_effekt.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: "belysning".into(),
|
||||
verdi: format!(
|
||||
"{:.2} W/m² i {:.0} h/døgn",
|
||||
n.belysning.effekt,
|
||||
n.driftstid.belysning.timer()
|
||||
),
|
||||
kilde: format!("normert, {}", n.kilde),
|
||||
});
|
||||
n.belysning.effekt
|
||||
});
|
||||
let utstyr = b.drift.utstyr_effekt.unwrap_or(n.utstyr.effekt);
|
||||
// Oppgitt utstyrseffekt skalerer varmetilskuddet i samme forhold som den normerte verdien.
|
||||
let utstyr_varme = match b.drift.utstyr_effekt {
|
||||
Some(e) => e * (n.utstyr.varmetilskudd / n.utstyr.effekt.max(1e-9)),
|
||||
None => n.utstyr.varmetilskudd,
|
||||
};
|
||||
let personer = b
|
||||
.drift
|
||||
.personer_varmetilskudd
|
||||
.unwrap_or(n.personer.varmetilskudd);
|
||||
let tappevann = b.tappevann.energibehov.map(|kwh| {
|
||||
// kWh/(m²·år) → W/m² i driftstiden.
|
||||
kwh * 1000.0 / (365.0 * n.driftstid.varmtvann.timer())
|
||||
});
|
||||
let vv = tappevann.unwrap_or_else(|| {
|
||||
logg.push(Forutsetning {
|
||||
felt: "tappevann.energibehov".into(),
|
||||
verdi: format!(
|
||||
"{:.1} kWh/(m²·år)",
|
||||
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * 365.0 / 1000.0
|
||||
),
|
||||
kilde: format!("normert, {}", n.kilde),
|
||||
});
|
||||
n.varmtvann.effekt
|
||||
});
|
||||
let pumpeeffekt = match b.distribusjon.typ {
|
||||
DistribusjonType::Punkt => b.distribusjon.pumpe_effekt.unwrap_or(0.0),
|
||||
_ => b
|
||||
.distribusjon
|
||||
.pumpe_effekt
|
||||
.unwrap_or(n.pumper.effekt_vannbaaren),
|
||||
};
|
||||
LostLaster {
|
||||
belysning: profil(bel, n.driftstid.belysning, n.profil_belysning),
|
||||
utstyr_effekt: profil(utstyr, n.driftstid.utstyr, n.profil_utstyr),
|
||||
utstyr_varme: profil(utstyr_varme, n.driftstid.utstyr, None),
|
||||
personer: profil(personer, n.driftstid.personer, n.profil_personer),
|
||||
varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann),
|
||||
pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None),
|
||||
oppvarming_driftstid: n.driftstid.oppvarming,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
//! Inndatamodell for en bygning (`bygning.toml`). Alt en energirådgiver kan variere ligger her;
|
||||
//! det som utelates hentes fra normerte verdier for kategorien og merkes i rapporten.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Kategori {
|
||||
Smaahus,
|
||||
Boligblokk,
|
||||
Barnehage,
|
||||
Kontorbygg,
|
||||
Skolebygg,
|
||||
Universitet,
|
||||
Sykehus,
|
||||
Sykehjem,
|
||||
Hotell,
|
||||
Idrettsbygg,
|
||||
Forretningsbygg,
|
||||
Kulturbygg,
|
||||
Industribygg,
|
||||
}
|
||||
|
||||
impl Kategori {
|
||||
pub fn nokkel(&self) -> &'static str {
|
||||
match self {
|
||||
Kategori::Smaahus => "smaahus",
|
||||
Kategori::Boligblokk => "boligblokk",
|
||||
Kategori::Barnehage => "barnehage",
|
||||
Kategori::Kontorbygg => "kontorbygg",
|
||||
Kategori::Skolebygg => "skolebygg",
|
||||
Kategori::Universitet => "universitet",
|
||||
Kategori::Sykehus => "sykehus",
|
||||
Kategori::Sykehjem => "sykehjem",
|
||||
Kategori::Hotell => "hotell",
|
||||
Kategori::Idrettsbygg => "idrettsbygg",
|
||||
Kategori::Forretningsbygg => "forretningsbygg",
|
||||
Kategori::Kulturbygg => "kulturbygg",
|
||||
Kategori::Industribygg => "industribygg",
|
||||
}
|
||||
}
|
||||
pub fn er_bolig(&self) -> bool {
|
||||
matches!(self, Kategori::Smaahus | Kategori::Boligblokk)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Bygning {
|
||||
pub bygning: BygningInfo,
|
||||
#[serde(default)]
|
||||
pub klima: Klima,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub flate: Vec<Flate>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub vindu: Vec<Vindu>,
|
||||
#[serde(default)]
|
||||
pub kuldebroer: Kuldebroer,
|
||||
#[serde(default)]
|
||||
pub infiltrasjon: Infiltrasjon,
|
||||
#[serde(default)]
|
||||
pub ventilasjon: Ventilasjon,
|
||||
#[serde(default)]
|
||||
pub tappevann: Tappevann,
|
||||
#[serde(default)]
|
||||
pub oppvarming: Oppvarming,
|
||||
#[serde(default)]
|
||||
pub distribusjon: Distribusjon,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub solcelle: Option<Solcelle>,
|
||||
#[serde(default)]
|
||||
pub kjoling: Kjoling,
|
||||
#[serde(default)]
|
||||
pub lufting: Lufting,
|
||||
#[serde(default)]
|
||||
pub drift: Drift,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub scenario: Vec<Scenario>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct BygningInfo {
|
||||
pub navn: String,
|
||||
pub kategori: Kategori,
|
||||
pub byggeaar: i32,
|
||||
/// Oppvarmet del av bruksareal, m².
|
||||
pub oppvarmet_bra: f64,
|
||||
/// Oppvarmet luftvolum, m³. Utelatt: 2,4 m × BRA.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oppvarmet_volum: Option<f64>,
|
||||
#[serde(default = "en")]
|
||||
pub antall_boenheter: u32,
|
||||
/// Samlet oppvarmet BRA for hele bygningen når én boenhet i flerbolighus merkes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bygningens_oppvarmede_bra: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub adresse: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gnr_bnr: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub beskrivelse: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub energiraadgiver: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub oppdragsgiver: Option<String>,
|
||||
}
|
||||
|
||||
fn en() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Klima {
|
||||
/// Kommune for lokalt klima (TMY-fil fra Zenodo 14779899). Utelatt: bare Oslo.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommune: Option<String>,
|
||||
/// Egen EPW-fil for lokalt klima (overstyrer kommune).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub epw: Option<PathBuf>,
|
||||
/// Referanseklima for energimerket. Standard «Oslo».
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub referanse: Option<String>,
|
||||
/// Bakkerefleksjon (albedo) for solstråling. Standard 0,2.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub albedo: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FlateType {
|
||||
Vegg,
|
||||
Tak,
|
||||
Gulv,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Mot {
|
||||
#[default]
|
||||
Ute,
|
||||
Grunn,
|
||||
Uoppvarmet,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Masseklasse {
|
||||
SvaertLett,
|
||||
Lett,
|
||||
Middels,
|
||||
Tung,
|
||||
SvaertTung,
|
||||
}
|
||||
|
||||
impl Masseklasse {
|
||||
/// Arealspesifikk varmekapasitet κ_m, J/(m²K) (ISO 52016-1 Tabell B.14).
|
||||
pub fn kappa(&self) -> f64 {
|
||||
match self {
|
||||
Masseklasse::SvaertLett => 50_000.0,
|
||||
Masseklasse::Lett => 75_000.0,
|
||||
Masseklasse::Middels => 110_000.0,
|
||||
Masseklasse::Tung => 175_000.0,
|
||||
Masseklasse::SvaertTung => 250_000.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hvor massen sitter i konstruksjonen (ISO 52016-1 klasse I/E/IE/D/M).
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Masseplassering {
|
||||
Innvendig,
|
||||
Utvendig,
|
||||
Begge,
|
||||
#[default]
|
||||
Fordelt,
|
||||
Midt,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Lag {
|
||||
pub materiale: String,
|
||||
pub tykkelse: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Flate {
|
||||
pub navn: String,
|
||||
#[serde(rename = "type")]
|
||||
pub typ: FlateType,
|
||||
pub areal: f64,
|
||||
/// Himmelretning flatens utside vender mot, grader fra nord med klokka (0 N, 90 Ø, 180 S, 270 V).
|
||||
#[serde(default)]
|
||||
pub orientering: f64,
|
||||
/// Helning fra horisontalplanet: 90 = vertikal vegg, 0 = flatt tak. Utelatt: 90 for vegg, 0 for tak/gulv.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub helning: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub u_verdi: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lag: Option<Vec<Lag>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub masseklasse: Option<Masseklasse>,
|
||||
#[serde(default)]
|
||||
pub masseplassering: Masseplassering,
|
||||
#[serde(default)]
|
||||
pub mot: Mot,
|
||||
/// Temperaturfaktor b for uoppvarmet rom. Utelatt: 0,5 (loft/kjeller), jf. NS-EN ISO 13789.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub b_faktor: Option<f64>,
|
||||
/// Solabsorptans for utvendig overflate. Utelatt: 0,6.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub absorptans: Option<f64>,
|
||||
/// For gulv mot grunn / kjellervegg mot grunn: eksponert omkrets (m) og dybde under terreng (m).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub omkrets: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dybde: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommentar: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SolskjermingType {
|
||||
#[default]
|
||||
Ingen,
|
||||
Manuell,
|
||||
Automatisk,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Solskjerming {
|
||||
#[serde(rename = "type")]
|
||||
pub typ: SolskjermingType,
|
||||
/// Gjennomslipp når skjermingen er i bruk (0–1). Utelatt: 0,8 manuell, 0,4 automatisk.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub faktor: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Vindu {
|
||||
pub navn: String,
|
||||
/// Areal inkl. karm, m².
|
||||
pub areal: f64,
|
||||
#[serde(default = "en_f64")]
|
||||
pub antall: f64,
|
||||
#[serde(default)]
|
||||
pub orientering: f64,
|
||||
#[serde(default = "nitti")]
|
||||
pub helning: f64,
|
||||
pub u_verdi: f64,
|
||||
/// 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")]
|
||||
pub g_verdi: Option<f64>,
|
||||
/// Karm- og rammeandel av arealet. Utelatt: 0,2.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub karmandel: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub solskjerming: Option<Solskjerming>,
|
||||
/// Horisontavskjerming i grader (terreng, nabobygg). Utelatt: 0.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub horisont: Option<f64>,
|
||||
/// Er dette en dør (regnes uten solinnstråling)?
|
||||
#[serde(default)]
|
||||
pub dor: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommentar: Option<String>,
|
||||
}
|
||||
|
||||
fn en_f64() -> f64 {
|
||||
1.0
|
||||
}
|
||||
fn nitti() -> f64 {
|
||||
90.0
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Kuldebroer {
|
||||
/// Normalisert kuldebroverdi, W/(m²K) av oppvarmet BRA. Utelatt: verdi etter byggeår.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub normalisert: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub liste: Vec<Kuldebro>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Kuldebro {
|
||||
pub navn: String,
|
||||
pub psi: f64,
|
||||
pub lengde: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Skjerming {
|
||||
Skjermet,
|
||||
#[default]
|
||||
Moderat,
|
||||
Utsatt,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Infiltrasjon {
|
||||
/// Lekkasjetall ved 50 Pa, 1/h. Utelatt: verdi etter byggeår.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub n50: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub skjerming: Skjerming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maalt_dato: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VentilasjonType {
|
||||
#[default]
|
||||
Naturlig,
|
||||
Avtrekk,
|
||||
Balansert,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Ventilasjon {
|
||||
#[serde(rename = "type", default)]
|
||||
pub typ: VentilasjonType,
|
||||
/// Gjennomsnittlig luftmengde i driftstiden, m³/(h·m²). Utelatt: normert.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub luftmengde: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub luftmengde_natt: Option<f64>,
|
||||
/// Temperaturvirkningsgrad varmegjenvinner (0–1). Bare balansert. Utelatt: normert.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gjenvinning: Option<f64>,
|
||||
/// Spesifikk vifteeffekt, kW/(m³/s). Utelatt: normert (balansert) / 0,6 (avtrekk).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sfp: Option<f64>,
|
||||
/// Har anlegget varmebatteri som ettervarmer tilluften?
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub varmebatteri: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TappevannSystem {
|
||||
#[default]
|
||||
Aapent,
|
||||
Sirkulasjon,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Tappevann {
|
||||
#[serde(default)]
|
||||
pub system: TappevannSystem,
|
||||
/// Netto energibehov, kWh/(m²·år). Utelatt: normert.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub energibehov: Option<f64>,
|
||||
/// Tap i bereder og rør som andel av netto behov. Utelatt: 0,25 åpent, 0,45 sirkulasjon.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tap_andel: Option<f64>,
|
||||
/// Andel av tapet som avgis i oppvarmet sone (uregulerbart varmetilskudd). Utelatt: 0,8 / 0,5.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tap_i_oppvarmet_sone: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VarmekildeType {
|
||||
DirekteEl,
|
||||
ElKjel,
|
||||
LuftLuftVp,
|
||||
LuftVannVp,
|
||||
VaeskeVannVp,
|
||||
AvtrekkVp,
|
||||
Vedovn,
|
||||
Pellets,
|
||||
Olje,
|
||||
Gass,
|
||||
Fjernvarme,
|
||||
Solfanger,
|
||||
}
|
||||
|
||||
impl VarmekildeType {
|
||||
pub fn nokkel(&self) -> &'static str {
|
||||
match self {
|
||||
VarmekildeType::DirekteEl => "direkte_el",
|
||||
VarmekildeType::ElKjel => "el_kjel",
|
||||
VarmekildeType::LuftLuftVp => "luft_luft_vp",
|
||||
VarmekildeType::LuftVannVp => "luft_vann_vp",
|
||||
VarmekildeType::VaeskeVannVp => "vaeske_vann_vp",
|
||||
VarmekildeType::AvtrekkVp => "avtrekk_vp",
|
||||
VarmekildeType::Vedovn => "vedovn",
|
||||
VarmekildeType::Pellets => "pellets",
|
||||
VarmekildeType::Olje => "olje",
|
||||
VarmekildeType::Gass => "gass",
|
||||
VarmekildeType::Fjernvarme => "fjernvarme",
|
||||
VarmekildeType::Solfanger => "solfanger",
|
||||
}
|
||||
}
|
||||
pub fn beskrivelse(&self) -> &'static str {
|
||||
match self {
|
||||
VarmekildeType::DirekteEl => "Direkte elektrisk (panelovner, varmekabler)",
|
||||
VarmekildeType::ElKjel => "Elektrokjel, vannbåren varme",
|
||||
VarmekildeType::LuftLuftVp => "Varmepumpe luft–luft",
|
||||
VarmekildeType::LuftVannVp => "Varmepumpe luft–vann",
|
||||
VarmekildeType::VaeskeVannVp => "Varmepumpe væske–vann (berg/jord/sjø)",
|
||||
VarmekildeType::AvtrekkVp => "Avtrekksvarmepumpe",
|
||||
VarmekildeType::Vedovn => "Vedovn",
|
||||
VarmekildeType::Pellets => "Pelletskamin/-kjel",
|
||||
VarmekildeType::Olje => "Oljekjel",
|
||||
VarmekildeType::Gass => "Gasskjel",
|
||||
VarmekildeType::Fjernvarme => "Fjernvarme",
|
||||
VarmekildeType::Solfanger => "Solfanger",
|
||||
}
|
||||
}
|
||||
pub fn er_varmepumpe(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
VarmekildeType::LuftLuftVp
|
||||
| VarmekildeType::LuftVannVp
|
||||
| VarmekildeType::VaeskeVannVp
|
||||
| VarmekildeType::AvtrekkVp
|
||||
)
|
||||
}
|
||||
/// Kan kilden levere til vannbåren distribusjon (ventilasjonsvarme, tappevann)?
|
||||
pub fn vannbaaren(&self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
VarmekildeType::DirekteEl | VarmekildeType::LuftLuftVp | VarmekildeType::Vedovn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Varmekilde {
|
||||
#[serde(rename = "type")]
|
||||
pub typ: VarmekildeType,
|
||||
/// Andel av romoppvarmingsbehovet kilden dekker (0–1).
|
||||
#[serde(default)]
|
||||
pub andel_rom: f64,
|
||||
/// Andel av ventilasjonsvarme. Utelatt: som andel_rom for vannbårne kilder, ellers 0.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub andel_ventilasjon: Option<f64>,
|
||||
#[serde(default)]
|
||||
pub andel_tappevann: f64,
|
||||
/// Årsvarmefaktor (SPF) for varmepumper eller virkningsgrad for kjeler. Utelatt: tabellverdi.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub spf: Option<f64>,
|
||||
/// Turtemperatur °C for vannbårne varmepumper (35/45/55).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub turtemperatur: Option<f64>,
|
||||
/// Vedovn: lukket ildsted med lavt partikkelutslipp (gir vektingsfaktor 0,45).
|
||||
#[serde(default = "sann")]
|
||||
pub lukket: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kommentar: Option<String>,
|
||||
}
|
||||
|
||||
fn sann() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Oppvarming {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub kilde: Vec<Varmekilde>,
|
||||
/// Maks varmeeffekt installert, W (begrenser innetemperatur ved underdekning). Utelatt: ubegrenset.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maks_effekt: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DistribusjonType {
|
||||
#[default]
|
||||
Punkt,
|
||||
Radiator,
|
||||
Gulvvarme,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Distribusjon {
|
||||
#[serde(rename = "type", default)]
|
||||
pub typ: DistribusjonType,
|
||||
/// Distribusjonstap som andel av avgitt romvarme. Utelatt: 0 punkt, 0,03 radiator, 0,05 gulvvarme.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tap_andel: Option<f64>,
|
||||
/// Andel av tapet som går til uoppvarmet areal (rør i kjeller/loft). Utelatt: 0,2.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub roer_uoppvarmet: Option<f64>,
|
||||
/// Pumpeeffekt, W/m² i driftstid. Utelatt: normert for vannbåren, 0 for punkt.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pumpe_effekt: Option<f64>,
|
||||
/// Turtemperatur °C i vannbårent anlegg (60 radiator, 35 gulvvarme). Brukes for SPF-oppslag.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub turtemperatur: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Solcelle {
|
||||
/// Modulareal, m².
|
||||
pub areal: f64,
|
||||
#[serde(default = "tjue_prosent")]
|
||||
pub virkningsgrad: f64,
|
||||
#[serde(default = "sor")]
|
||||
pub orientering: f64,
|
||||
#[serde(default = "tretti")]
|
||||
pub helning: f64,
|
||||
/// Ytelsesfaktor (inverter, kabler, snø, smuss). Utelatt: 0,8.
|
||||
#[serde(default = "aatti_prosent")]
|
||||
pub ytelsesfaktor: f64,
|
||||
}
|
||||
|
||||
fn tjue_prosent() -> f64 {
|
||||
0.20
|
||||
}
|
||||
fn sor() -> f64 {
|
||||
180.0
|
||||
}
|
||||
fn tretti() -> f64 {
|
||||
30.0
|
||||
}
|
||||
fn aatti_prosent() -> f64 {
|
||||
0.8
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Kjoling {
|
||||
#[serde(default = "sann")]
|
||||
pub ingen: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub settpunkt: Option<f64>,
|
||||
/// Årskuldefaktor (SEER). Utelatt: 3,0.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub spf: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maks_effekt: Option<f64>,
|
||||
}
|
||||
|
||||
impl Default for Kjoling {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ingen: true,
|
||||
settpunkt: None,
|
||||
spf: None,
|
||||
maks_effekt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vinduslufting: økt luftmengde når rommene blir for varme.
|
||||
///
|
||||
/// Bygninger uten kjøleanlegg luftes gjennom vinduer når det blir for varmt. Uten denne
|
||||
/// modellen vil en tett bygning med store vindusflater få urealistisk høye innetemperaturer
|
||||
/// om våren og høsten, og lagret solvarme vil kunstig redusere oppvarmingsbehovet.
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Lufting {
|
||||
/// Slår av vinduslufting. Standard er lufting for bygninger uten kjøleanlegg.
|
||||
#[serde(default)]
|
||||
pub ingen: bool,
|
||||
/// Operativ temperatur der luftingen starter, °C. Utelatt: 24.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub settpunkt: Option<f64>,
|
||||
/// Største luftmengde ved vinduslufting, m³/(h·m²) oppvarmet BRA. Utelatt: 8.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub maks_luftmengde: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
pub struct Drift {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub settpunkt_oppvarming: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nattsenking: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub belysning_effekt: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub utstyr_effekt: Option<f64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub personer_varmetilskudd: Option<f64>,
|
||||
/// Effektiv innvendig varmekapasitet for luft og inventar, J/(m²K). Utelatt: 10 000 (ISO 52016-1).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub innvendig_varmekapasitet: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Scenario {
|
||||
pub navn: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub beskrivelse: Option<String>,
|
||||
/// Delvis bygningsbeskrivelse som flettes inn: tabeller flettes rekursivt, tabellarrayer
|
||||
/// (flate, vindu, oppvarming.kilde …) matches på `navn`/`type`, skalarer erstattes.
|
||||
/// `slett = true` fjerner et element.
|
||||
pub endringer: toml::Value,
|
||||
}
|
||||
|
||||
pub mod lost;
|
||||
@@ -0,0 +1,518 @@
|
||||
//! Dynamisk timesberegning av netto energibehov (beregningspunkt A) etter
|
||||
//! NS-EN ISO 52016-1, slik NS 3031:2025 krever.
|
||||
//!
|
||||
//! Hver opake bygningsdel modelleres med fem noder der varmekapasiteten fordeles etter
|
||||
//! masseplassering (ISO 52016-1 tabell B.13/B.14), og vinduer med to noder uten masse.
|
||||
//! Sonen har én luftnode med varmekapasitet for luft og inventar, og én strålingsnode
|
||||
//! («stjernenode») som fordeler langbølget strålingsutveksling mellom innvendige overflater.
|
||||
//! Systemet løses implisitt (bakover-Euler) for hver time.
|
||||
//!
|
||||
//! Varme- og kjølebehovet bestemmes som i standarden: systemet løses først uten tilført
|
||||
//! effekt og deretter med en prøveeffekt. Siden systemet er lineært, gir interpolasjon
|
||||
//! mellom de to løsningene nøyaktig den effekten som treffer settpunktet.
|
||||
|
||||
use crate::klima::{sol, Vaerdata};
|
||||
use crate::modell::lost::{Lost, Regulering};
|
||||
use crate::modell::Mot;
|
||||
use crate::motor::{lin, ventilasjon, RHO_C_LUFT};
|
||||
use crate::{Result, TIMER};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Konvektiv varmeovergang innvendig overflate, W/(m²K). Gir sammen med h_ri R_si = 0,13.
|
||||
const H_CI: f64 = 2.5;
|
||||
/// Langbølget strålingsutveksling innvendig, W/(m²K).
|
||||
const H_RI: f64 = 5.13;
|
||||
/// Konvektiv varmeovergang utvendig, W/(m²K).
|
||||
const H_CE: f64 = 20.0;
|
||||
/// Langbølget stråling utvendig, W/(m²K). Gir sammen med h_ce R_se = 0,04.
|
||||
const H_RE: f64 = 4.14;
|
||||
/// Konvektiv andel av interne varmetilskudd (ISO 52016-1).
|
||||
const KONVEKTIV_ANDEL: f64 = 0.4;
|
||||
/// Innstråling der solskjerming trer i kraft, W/m².
|
||||
const SKJERMING_TERSKEL: f64 = 300.0;
|
||||
/// Utetemperatur der manuell solskjerming antas tatt i bruk, °C.
|
||||
const SKJERMING_TEMP: f64 = 10.0;
|
||||
const DT: f64 = 3600.0;
|
||||
/// Prøveeffekt per m² ved bestemmelse av varme-/kjølebehov.
|
||||
const PROVEEFFEKT: f64 = 10.0;
|
||||
|
||||
/// Nodeoppsett for én bygningsdel.
|
||||
struct Del {
|
||||
areal: f64,
|
||||
/// Indeks til delens første node i tilstandsvektoren.
|
||||
forste: usize,
|
||||
/// Antall noder (5 for opake deler, 2 for vinduer og dører).
|
||||
antall: usize,
|
||||
/// Konduktans mellom nabonoder, W/K (allerede multiplisert med areal).
|
||||
h_indre: f64,
|
||||
/// Konduktans mot ytre miljø fra node 1, W/K.
|
||||
h_ytre: f64,
|
||||
/// Varmekapasitet per node, J/K.
|
||||
kapasitet: Vec<f64>,
|
||||
glass: bool,
|
||||
mot: Mot,
|
||||
b_faktor: f64,
|
||||
absorptans: f64,
|
||||
helning: f64,
|
||||
/// Solinnstråling på flaten, W/m², for hele året.
|
||||
innstraaling: Vec<f64>,
|
||||
g_effektiv: f64,
|
||||
skjermingsfaktor: f64,
|
||||
automatisk_skjerming: bool,
|
||||
horisontfaktor: f64,
|
||||
}
|
||||
|
||||
/// Resultatet av en årssimulering: alle timesserier, W (energistrømmer) og °C.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Timeserie {
|
||||
pub t_ute: Vec<f64>,
|
||||
pub t_inne: Vec<f64>,
|
||||
pub t_operativ: Vec<f64>,
|
||||
/// Romoppvarming, post 1a, W.
|
||||
pub romoppvarming: Vec<f64>,
|
||||
/// Ventilasjonsvarme, post 1b, W.
|
||||
pub ventilasjonsvarme: Vec<f64>,
|
||||
/// Romkjøling, post 3a, W.
|
||||
pub romkjoling: Vec<f64>,
|
||||
/// Ventilasjonskjøling, post 3b, W.
|
||||
pub ventilasjonskjoling: Vec<f64>,
|
||||
/// Varmtvann, post 2, W.
|
||||
pub varmtvann: Vec<f64>,
|
||||
/// Vifter, post 4a, W.
|
||||
pub vifter: Vec<f64>,
|
||||
/// Pumper, post 4b, W.
|
||||
pub pumper: Vec<f64>,
|
||||
/// Belysning, post 5, W.
|
||||
pub belysning: Vec<f64>,
|
||||
/// Teknisk utstyr, post 6, W.
|
||||
pub utstyr: Vec<f64>,
|
||||
/// Sum solinnstråling gjennom vinduer, W.
|
||||
pub soltilskudd: Vec<f64>,
|
||||
/// Interne varmetilskudd, W.
|
||||
pub internlast: Vec<f64>,
|
||||
/// Solstråling på horisontal flate, W/m².
|
||||
pub sol_horisontal: Vec<f64>,
|
||||
/// Luftmengde ved vinduslufting, m³/h.
|
||||
pub vinduslufting: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Timeserie {
|
||||
fn ny() -> Self {
|
||||
let n = || vec![0.0; TIMER];
|
||||
Timeserie {
|
||||
t_ute: n(),
|
||||
t_inne: n(),
|
||||
t_operativ: n(),
|
||||
romoppvarming: n(),
|
||||
ventilasjonsvarme: n(),
|
||||
romkjoling: n(),
|
||||
ventilasjonskjoling: n(),
|
||||
varmtvann: n(),
|
||||
vifter: n(),
|
||||
pumper: n(),
|
||||
belysning: n(),
|
||||
utstyr: n(),
|
||||
soltilskudd: n(),
|
||||
internlast: n(),
|
||||
sol_horisontal: n(),
|
||||
vinduslufting: n(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Årssum av en serie, kWh.
|
||||
pub fn sum_kwh(serie: &[f64]) -> f64 {
|
||||
serie.iter().sum::<f64>() / 1000.0
|
||||
}
|
||||
|
||||
/// Månedssummer, kWh.
|
||||
pub fn maaned_kwh(serie: &[f64]) -> [f64; 12] {
|
||||
let mut ut = [0.0; 12];
|
||||
for (t, v) in serie.iter().enumerate() {
|
||||
ut[crate::maaned_for_time(t)] += v / 1000.0;
|
||||
}
|
||||
ut
|
||||
}
|
||||
}
|
||||
|
||||
/// Uregulerbart varmetilskudd i sonen fra systemtap, W.
|
||||
///
|
||||
/// Tap fra tappevannsbereder og varmtvannsrør i oppvarmet sone varmer rommene og reduserer
|
||||
/// romoppvarmingsbehovet, samtidig som energien telles under tappevann. Distribusjonstap for
|
||||
/// romvarme i oppvarmet sone regnes ikke med her: den varmen ville uansett blitt avgitt i sonen,
|
||||
/// og påvirker derfor bare fordelingen mellom rør og varmegiver, ikke energibehovet.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Systemtilskudd {
|
||||
/// Konstant tilskudd fra tappevannsanlegget, W.
|
||||
pub tappevann: f64,
|
||||
}
|
||||
|
||||
pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Timeserie> {
|
||||
let deler = bygg_deler(l, v);
|
||||
let n_noder: usize = 2 + deler.iter().map(|d| d.antall).sum::<usize>();
|
||||
let areal_innvendig: f64 = deler.iter().map(|d| d.areal).sum();
|
||||
let c_luft = l.innvendig_varmekapasitet * l.bra;
|
||||
let h_inf = ventilasjon::infiltrasjonsluftmengde(l) * RHO_C_LUFT;
|
||||
let bakketemp = bakketemperatur(v);
|
||||
|
||||
let mut ts = Timeserie::ny();
|
||||
let mut tilstand = vec![l.settpunkt_oppvarming; n_noder];
|
||||
let mut t_a_forrige = l.settpunkt_oppvarming;
|
||||
|
||||
// Innkjøring: gjenta de første to ukene til starttilstanden har stabilisert seg.
|
||||
for innkjoring in 0..3 {
|
||||
let skriv = innkjoring == 2;
|
||||
let start_tilstand = tilstand.clone();
|
||||
let timer: Box<dyn Iterator<Item = usize>> = if skriv {
|
||||
Box::new(0..TIMER)
|
||||
} else {
|
||||
Box::new(0..(24 * 14))
|
||||
};
|
||||
if !skriv {
|
||||
tilstand = start_tilstand;
|
||||
}
|
||||
for t in timer {
|
||||
let klokke = t % 24;
|
||||
let t_ute = v.t_ute[t];
|
||||
let t_himmel = sol::himmeltemperatur(v, t);
|
||||
|
||||
// ── laster ──────────────────────────────────────────────
|
||||
let bel = l.laster.belysning[klokke] * l.bra;
|
||||
let utstyr_el = l.laster.utstyr_effekt[klokke] * l.bra;
|
||||
let utstyr_varme = l.laster.utstyr_varme[klokke] * l.bra;
|
||||
let personer = l.laster.personer[klokke] * l.bra;
|
||||
let vv = l.laster.varmtvann[klokke] * l.bra;
|
||||
let pumper = l.laster.pumper[klokke] * l.bra;
|
||||
let vifter = ventilasjon::vifteeffekt(l, klokke);
|
||||
// Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten).
|
||||
let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter;
|
||||
|
||||
// ── sol ─────────────────────────────────────────────────
|
||||
let mut sol_gjennom = 0.0;
|
||||
for d in &deler {
|
||||
if d.glass && d.g_effektiv > 0.0 {
|
||||
sol_gjennom += soltransmisjon(d, t, t_ute);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ventilasjon ─────────────────────────────────────────
|
||||
let q_vent = ventilasjon::luftmengde(l, klokke);
|
||||
let h_ve = q_vent * RHO_C_LUFT;
|
||||
let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige);
|
||||
let (t_sup, vent_varme) =
|
||||
if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt {
|
||||
(
|
||||
l.ventilasjon.tilluft_settpunkt,
|
||||
h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx),
|
||||
)
|
||||
} else {
|
||||
(t_sup_hx, 0.0)
|
||||
};
|
||||
|
||||
// ── settpunkt ───────────────────────────────────────────
|
||||
let sett_varme = if l.laster.oppvarming_driftstid.aktiv(klokke) {
|
||||
l.settpunkt_oppvarming
|
||||
} else {
|
||||
l.settpunkt_natt
|
||||
};
|
||||
|
||||
// ── løsning ─────────────────────────────────────────────
|
||||
let prove = PROVEEFFEKT * l.bra;
|
||||
let ctrl = |s: &[f64]| -> f64 {
|
||||
match l.regulering {
|
||||
Regulering::Luft => s[0],
|
||||
Regulering::Operativ => 0.5 * s[0] + 0.5 * s[1],
|
||||
}
|
||||
};
|
||||
let lag_ctx = |h_luft: f64| Kontekst {
|
||||
deler: &deler,
|
||||
n_noder,
|
||||
areal_innvendig,
|
||||
c_luft,
|
||||
h_inf,
|
||||
h_ve: h_ve + h_luft,
|
||||
// Lufteluften kommer rett utenfra, uten varmegjenvinning.
|
||||
t_sup: if h_ve + h_luft > 1e-9 {
|
||||
(h_ve * t_sup + h_luft * t_ute) / (h_ve + h_luft)
|
||||
} else {
|
||||
t_sup
|
||||
},
|
||||
t_ute,
|
||||
t_himmel,
|
||||
t_bakke: bakketemp[crate::maaned_for_time(t)],
|
||||
t_a_forrige,
|
||||
internlast,
|
||||
sol_gjennom,
|
||||
time: t,
|
||||
};
|
||||
|
||||
// Vinduslufting: øk luftmengden til den operative temperaturen holder settpunktet.
|
||||
let mut h_luft = 0.0;
|
||||
let mut ctx = lag_ctx(0.0);
|
||||
let mut los0 = ctx.los(&tilstand, 0.0)?;
|
||||
if let Some((sett_luft, maks_luft)) = l.lufting {
|
||||
if ctrl(&los0) > sett_luft {
|
||||
let h_maks = maks_luft * l.bra * RHO_C_LUFT;
|
||||
let ctx_maks = lag_ctx(h_maks);
|
||||
let los_maks = ctx_maks.los(&tilstand, 0.0)?;
|
||||
if ctrl(&los_maks) >= sett_luft {
|
||||
h_luft = h_maks;
|
||||
ctx = ctx_maks;
|
||||
los0 = los_maks;
|
||||
} else {
|
||||
let (mut lav, mut hoy) = (0.0, h_maks);
|
||||
for _ in 0..12 {
|
||||
let midt = 0.5 * (lav + hoy);
|
||||
let k = lag_ctx(midt);
|
||||
let l0 = k.los(&tilstand, 0.0)?;
|
||||
if ctrl(&l0) > sett_luft {
|
||||
lav = midt;
|
||||
} else {
|
||||
hoy = midt;
|
||||
}
|
||||
h_luft = midt;
|
||||
ctx = k;
|
||||
los0 = l0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let los1 = ctx.los(&tilstand, prove)?;
|
||||
let c0 = ctrl(&los0);
|
||||
let c1 = ctrl(&los1);
|
||||
let stigning = (c1 - c0) / prove; // K per W
|
||||
|
||||
let mut fi = 0.0;
|
||||
if c0 < sett_varme - 1e-9 && stigning > 1e-12 {
|
||||
fi = (sett_varme - c0) / stigning;
|
||||
if let Some(maks) = l.maks_varmeeffekt {
|
||||
fi = fi.min(maks);
|
||||
}
|
||||
} else if let Some(sett_kjol) = l.settpunkt_kjoling {
|
||||
if c0 > sett_kjol + 1e-9 && stigning > 1e-12 {
|
||||
fi = (sett_kjol - c0) / stigning;
|
||||
if let Some(maks) = l.maks_kjoleeffekt {
|
||||
fi = fi.max(-maks);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Eksakt tilstand ved interpolasjon (systemet er lineært).
|
||||
let andel = fi / prove;
|
||||
for i in 0..n_noder {
|
||||
tilstand[i] = los0[i] + andel * (los1[i] - los0[i]);
|
||||
}
|
||||
t_a_forrige = tilstand[0];
|
||||
|
||||
if skriv {
|
||||
ts.t_ute[t] = t_ute;
|
||||
ts.t_inne[t] = tilstand[0];
|
||||
ts.t_operativ[t] = 0.5 * tilstand[0] + 0.5 * tilstand[1];
|
||||
ts.romoppvarming[t] = fi.max(0.0);
|
||||
ts.romkjoling[t] = (-fi).max(0.0);
|
||||
ts.ventilasjonsvarme[t] = vent_varme;
|
||||
ts.varmtvann[t] = vv;
|
||||
ts.vifter[t] = vifter;
|
||||
ts.pumper[t] = pumper;
|
||||
ts.belysning[t] = bel;
|
||||
ts.utstyr[t] = utstyr_el;
|
||||
ts.soltilskudd[t] = sol_gjennom;
|
||||
ts.internlast[t] = internlast;
|
||||
ts.sol_horisontal[t] = v.ghi[t];
|
||||
ts.vinduslufting[t] = h_luft / RHO_C_LUFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ts)
|
||||
}
|
||||
|
||||
struct Kontekst<'a> {
|
||||
deler: &'a [Del],
|
||||
n_noder: usize,
|
||||
areal_innvendig: f64,
|
||||
c_luft: f64,
|
||||
h_inf: f64,
|
||||
h_ve: f64,
|
||||
t_sup: f64,
|
||||
t_ute: f64,
|
||||
t_himmel: f64,
|
||||
t_bakke: f64,
|
||||
t_a_forrige: f64,
|
||||
internlast: f64,
|
||||
sol_gjennom: f64,
|
||||
time: usize,
|
||||
}
|
||||
|
||||
impl Kontekst<'_> {
|
||||
/// Setter opp og løser timesligningene med gitt tilført effekt til sonen.
|
||||
fn los(&self, forrige: &[f64], fi_hc: f64) -> Result<Vec<f64>> {
|
||||
let n = self.n_noder;
|
||||
let mut a = vec![0.0; n * n];
|
||||
let mut b = vec![0.0; n];
|
||||
let idx = |i: usize, j: usize| i * n + j;
|
||||
|
||||
// ── rad 0: sonens luftnode ──────────────────────────────────
|
||||
let c_dt = self.c_luft / DT;
|
||||
a[idx(0, 0)] = c_dt + self.h_ve + self.h_inf;
|
||||
b[0] = c_dt * forrige[0]
|
||||
+ self.h_ve * self.t_sup
|
||||
+ self.h_inf * self.t_ute
|
||||
+ KONVEKTIV_ANDEL * self.internlast
|
||||
+ fi_hc;
|
||||
for d in self.deler {
|
||||
let indre = d.forste + d.antall - 1;
|
||||
let h = d.areal * H_CI;
|
||||
a[idx(0, 0)] += h;
|
||||
a[idx(0, indre)] -= h;
|
||||
}
|
||||
|
||||
// ── rad 1: strålingsnoden (ingen masse, netto null) ─────────
|
||||
for d in self.deler {
|
||||
let indre = d.forste + d.antall - 1;
|
||||
let h = d.areal * H_RI;
|
||||
a[idx(1, 1)] += h;
|
||||
a[idx(1, indre)] -= h;
|
||||
}
|
||||
b[1] = 0.0;
|
||||
|
||||
// ── bygningsdelene ──────────────────────────────────────────
|
||||
let rad_last = (1.0 - KONVEKTIV_ANDEL) * self.internlast + self.sol_gjennom;
|
||||
for d in self.deler {
|
||||
let t_ytre = self.ytre_temperatur(d);
|
||||
let sol_absorbert = if d.glass {
|
||||
0.0
|
||||
} else {
|
||||
d.areal * d.absorptans * d.innstraaling[self.time]
|
||||
};
|
||||
for k in 0..d.antall {
|
||||
let i = d.forste + k;
|
||||
let c = d.kapasitet[k] / DT;
|
||||
a[idx(i, i)] += c;
|
||||
b[i] += c * forrige[i];
|
||||
if k == 0 {
|
||||
a[idx(i, i)] += d.h_ytre;
|
||||
b[i] += d.h_ytre * t_ytre + sol_absorbert;
|
||||
}
|
||||
if k + 1 < d.antall {
|
||||
a[idx(i, i)] += d.h_indre;
|
||||
a[idx(i, i + 1)] -= d.h_indre;
|
||||
}
|
||||
if k > 0 {
|
||||
a[idx(i, i)] += d.h_indre;
|
||||
a[idx(i, i - 1)] -= d.h_indre;
|
||||
}
|
||||
if k == d.antall - 1 {
|
||||
a[idx(i, i)] += d.areal * (H_CI + H_RI);
|
||||
a[idx(i, 0)] -= d.areal * H_CI;
|
||||
a[idx(i, 1)] -= d.areal * H_RI;
|
||||
b[i] += rad_last * d.areal / self.areal_innvendig;
|
||||
}
|
||||
}
|
||||
}
|
||||
lin::los(&mut a, &mut b, n)?;
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
fn ytre_temperatur(&self, d: &Del) -> f64 {
|
||||
match d.mot {
|
||||
Mot::Grunn => self.t_bakke,
|
||||
Mot::Uoppvarmet => self.t_a_forrige - d.b_faktor * (self.t_a_forrige - self.t_ute),
|
||||
Mot::Ute => {
|
||||
// Himmelandel etter flatens helning; resten ser bakken med utelufttemperatur.
|
||||
let f_himmel = (1.0 + d.helning.to_radians().cos()) / 2.0;
|
||||
let t_str = f_himmel * self.t_himmel + (1.0 - f_himmel) * self.t_ute;
|
||||
(H_CE * self.t_ute + H_RE * t_str) / (H_CE + H_RE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn soltransmisjon(d: &Del, t: usize, t_ute: f64) -> f64 {
|
||||
let i_sol = d.innstraaling[t] * d.horisontfaktor;
|
||||
let skjerm = if d.skjermingsfaktor < 1.0
|
||||
&& i_sol > SKJERMING_TERSKEL
|
||||
&& (d.automatisk_skjerming || t_ute > SKJERMING_TEMP)
|
||||
{
|
||||
d.skjermingsfaktor
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
d.areal * d.g_effektiv * skjerm * i_sol
|
||||
}
|
||||
|
||||
fn bygg_deler(l: &Lost, v: &Vaerdata) -> Vec<Del> {
|
||||
let mut deler = Vec::with_capacity(l.flater.len());
|
||||
let mut neste = 2usize;
|
||||
for f in &l.flater {
|
||||
let antall = if f.glass { 2 } else { 5 };
|
||||
let r_total = 1.0 / f.u_verdi.max(1e-6);
|
||||
let r_konstruksjon = (r_total - 1.0 / (H_CI + H_RI) - 1.0 / (H_CE + H_RE)).max(1e-3);
|
||||
let gap = (antall - 1) as f64;
|
||||
let h_indre = f.areal * gap / r_konstruksjon;
|
||||
let h_ytre = f.areal * (H_CE + H_RE);
|
||||
let kapasitet = fordel_kapasitet(f, antall);
|
||||
let innstraaling = if f.glass || f.mot == Mot::Ute {
|
||||
sol::innstraaling_aar(v, f.helning, f.orientering, l.albedo)
|
||||
} else {
|
||||
vec![0.0; TIMER]
|
||||
};
|
||||
deler.push(Del {
|
||||
areal: f.areal,
|
||||
forste: neste,
|
||||
antall,
|
||||
h_indre,
|
||||
h_ytre,
|
||||
kapasitet,
|
||||
glass: f.glass,
|
||||
mot: f.mot,
|
||||
b_faktor: f.b_faktor,
|
||||
absorptans: f.absorptans,
|
||||
helning: f.helning,
|
||||
innstraaling,
|
||||
g_effektiv: f.g_effektiv,
|
||||
skjermingsfaktor: f.skjermingsfaktor,
|
||||
automatisk_skjerming: f.automatisk_skjerming,
|
||||
horisontfaktor: (1.0 - f.horisont / 90.0).clamp(0.0, 1.0),
|
||||
});
|
||||
neste += antall;
|
||||
}
|
||||
deler
|
||||
}
|
||||
|
||||
/// Fordeler varmekapasiteten over nodene etter ISO 52016-1 tabell B.13.
|
||||
fn fordel_kapasitet(f: &crate::modell::lost::LostFlate, antall: usize) -> Vec<f64> {
|
||||
use crate::modell::Masseplassering::*;
|
||||
if antall == 2 {
|
||||
return vec![0.0, 0.0];
|
||||
}
|
||||
let k = f.kappa * f.areal;
|
||||
let mut c = vec![0.0; 5];
|
||||
match f.masseplassering {
|
||||
Innvendig => c[4] = k,
|
||||
Utvendig => c[0] = k,
|
||||
Begge => {
|
||||
c[0] = k / 2.0;
|
||||
c[4] = k / 2.0;
|
||||
}
|
||||
Midt => c[2] = k,
|
||||
Fordelt => {
|
||||
c[0] = k / 8.0;
|
||||
c[1] = k / 4.0;
|
||||
c[2] = k / 4.0;
|
||||
c[3] = k / 4.0;
|
||||
c[4] = k / 8.0;
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
/// Dempet bakketemperatur per måned etter NS-EN ISO 13370 (forenklet): årsmiddel med
|
||||
/// halvert amplitude og én måneds forsinkelse.
|
||||
fn bakketemperatur(v: &Vaerdata) -> [f64; 12] {
|
||||
let aar = v.aarsmiddeltemperatur();
|
||||
let maaned = v.maanedsmiddeltemperatur();
|
||||
let mut ut = [0.0; 12];
|
||||
for m in 0..12 {
|
||||
let forsinket = maaned[(m + 11) % 12];
|
||||
ut[m] = aar + 0.5 * (forsinket - aar);
|
||||
}
|
||||
ut
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Liten tett LU-løser med delvis pivotering. Systemene i timesberegningen har typisk
|
||||
//! 30–80 ukjente, så en tett løser er raskere enn oppsett av en glissen struktur.
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::bail;
|
||||
|
||||
/// Løser `a · x = b` in-place. `a` er radvis lagret n×n.
|
||||
pub fn los(a: &mut [f64], b: &mut [f64], n: usize) -> Result<()> {
|
||||
debug_assert_eq!(a.len(), n * n);
|
||||
debug_assert_eq!(b.len(), n);
|
||||
for k in 0..n {
|
||||
// Delvis pivotering.
|
||||
let mut piv = k;
|
||||
let mut maks = a[k * n + k].abs();
|
||||
for i in (k + 1)..n {
|
||||
let v = a[i * n + k].abs();
|
||||
if v > maks {
|
||||
maks = v;
|
||||
piv = i;
|
||||
}
|
||||
}
|
||||
if maks < 1e-14 {
|
||||
bail!("singulær matrise i timesberegningen (rad {k})");
|
||||
}
|
||||
if piv != k {
|
||||
for j in 0..n {
|
||||
a.swap(k * n + j, piv * n + j);
|
||||
}
|
||||
b.swap(k, piv);
|
||||
}
|
||||
let d = a[k * n + k];
|
||||
for i in (k + 1)..n {
|
||||
let f = a[i * n + k] / d;
|
||||
if f == 0.0 {
|
||||
continue;
|
||||
}
|
||||
a[i * n + k] = 0.0;
|
||||
for j in (k + 1)..n {
|
||||
a[i * n + j] -= f * a[k * n + j];
|
||||
}
|
||||
b[i] -= f * b[k];
|
||||
}
|
||||
}
|
||||
for i in (0..n).rev() {
|
||||
let mut s = b[i];
|
||||
for j in (i + 1)..n {
|
||||
s -= a[i * n + j] * b[j];
|
||||
}
|
||||
b[i] = s / a[i * n + i];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn loser_lite_system() {
|
||||
// 2x + y = 5 ; x + 3y = 10 → x = 1, y = 3
|
||||
let mut a = vec![2.0, 1.0, 1.0, 3.0];
|
||||
let mut b = vec![5.0, 10.0];
|
||||
los(&mut a, &mut b, 2).unwrap();
|
||||
assert!((b[0] - 1.0).abs() < 1e-12);
|
||||
assert!((b[1] - 3.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pivoterer_naar_noekkelelement_er_null() {
|
||||
let mut a = vec![0.0, 1.0, 1.0, 0.0];
|
||||
let mut b = vec![2.0, 3.0];
|
||||
los(&mut a, &mut b, 2).unwrap();
|
||||
assert!((b[0] - 3.0).abs() < 1e-12);
|
||||
assert!((b[1] - 2.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Beregningsmotoren: varmetapsbudsjett, laster, ventilasjon og den dynamiske timesberegningen.
|
||||
|
||||
pub mod iso52016;
|
||||
pub mod lin;
|
||||
pub mod varmetap;
|
||||
pub mod ventilasjon;
|
||||
|
||||
pub use iso52016::{simuler, Timeserie};
|
||||
pub use varmetap::Varmetapsbudsjett;
|
||||
|
||||
/// Varmekapasitet for luft ved konstant trykk, Wh/(m³·K).
|
||||
pub const RHO_C_LUFT: f64 = 1200.0 / 3600.0;
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Varmetapsbudsjett (NS 3031 Tabell 3): varmetapstall H_tr per post, W/(m²K) av oppvarmet BRA.
|
||||
|
||||
use crate::modell::lost::Lost;
|
||||
use crate::modell::{FlateType, Mot};
|
||||
use crate::motor::ventilasjon;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Clone, Debug, Default)]
|
||||
pub struct Varmetapsbudsjett {
|
||||
pub vegger: f64,
|
||||
pub tak: f64,
|
||||
pub gulv: f64,
|
||||
pub vinduer_dorer: f64,
|
||||
pub kuldebroer: f64,
|
||||
pub transmisjon: f64,
|
||||
pub infiltrasjon: f64,
|
||||
pub klimaskjerm: f64,
|
||||
pub ventilasjon: f64,
|
||||
pub totalt: f64,
|
||||
/// Absolutte verdier, W/K.
|
||||
pub transmisjon_w_per_k: f64,
|
||||
pub infiltrasjon_w_per_k: f64,
|
||||
pub ventilasjon_w_per_k: f64,
|
||||
}
|
||||
|
||||
impl Varmetapsbudsjett {
|
||||
pub fn beregn(l: &Lost) -> Self {
|
||||
let mut b = Varmetapsbudsjett::default();
|
||||
let a = l.bra;
|
||||
for f in &l.flater {
|
||||
// Gulv mot grunn tas med sin U-verdi; den dempede bakketemperaturen håndteres
|
||||
// i timesberegningen, mens varmetapstallet oppgis som for flate mot det fri.
|
||||
let bidrag = f.areal
|
||||
* f.u_verdi
|
||||
* if f.mot == Mot::Uoppvarmet {
|
||||
f.b_faktor
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if f.glass {
|
||||
b.vinduer_dorer += bidrag;
|
||||
} else {
|
||||
match f.typ {
|
||||
FlateType::Vegg => b.vegger += bidrag,
|
||||
FlateType::Tak => b.tak += bidrag,
|
||||
FlateType::Gulv => b.gulv += bidrag,
|
||||
}
|
||||
}
|
||||
}
|
||||
b.transmisjon_w_per_k = b.vegger + b.tak + b.gulv + b.vinduer_dorer + l.kuldebro_w_per_k;
|
||||
b.kuldebroer = l.kuldebro_w_per_k / a;
|
||||
b.vegger /= a;
|
||||
b.tak /= a;
|
||||
b.gulv /= a;
|
||||
b.vinduer_dorer /= a;
|
||||
b.transmisjon = b.vegger + b.tak + b.gulv + b.vinduer_dorer + b.kuldebroer;
|
||||
|
||||
let q_inf = ventilasjon::infiltrasjonsluftmengde(l); // m³/h
|
||||
b.infiltrasjon_w_per_k = q_inf * crate::motor::RHO_C_LUFT;
|
||||
b.infiltrasjon = b.infiltrasjon_w_per_k / a;
|
||||
b.klimaskjerm = b.transmisjon + b.infiltrasjon;
|
||||
|
||||
// Ventilasjonstapet oppgis som årsgjennomsnittlig, med varmegjenvinning.
|
||||
let q_vent = l.ventilasjon.luftmengde * a; // m³/h i driftstiden
|
||||
b.ventilasjon_w_per_k =
|
||||
q_vent * crate::motor::RHO_C_LUFT * (1.0 - l.ventilasjon.gjenvinning);
|
||||
b.ventilasjon = b.ventilasjon_w_per_k / a;
|
||||
b.totalt = b.klimaskjerm + b.ventilasjon;
|
||||
b
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Ventilasjon og infiltrasjon.
|
||||
|
||||
use crate::modell::lost::Lost;
|
||||
use crate::modell::{Skjerming, VentilasjonType};
|
||||
|
||||
/// Infiltrasjonsluftmengde, m³/h, etter NS-EN 15242 slik NS 3031 anvender den:
|
||||
///
|
||||
/// `n_inf = n50 · e / (1 + (f/e) · ((q_til − q_fra) / (V · n50))²)`
|
||||
///
|
||||
/// `e` er skjermingskoeffisienten og `f` en anleggskoeffisient. Ved balansert ventilasjon
|
||||
/// faller ubalanseleddet bort, og infiltrasjonen blir `n50 · e`.
|
||||
pub fn infiltrasjonsluftmengde(l: &Lost) -> f64 {
|
||||
let e = match l.skjerming {
|
||||
Skjerming::Skjermet => 0.03,
|
||||
Skjerming::Moderat => 0.07,
|
||||
Skjerming::Utsatt => 0.10,
|
||||
};
|
||||
let f = 15.0;
|
||||
let v = l.volum;
|
||||
let q_snitt = l.ventilasjon.luftmengde * l.bra;
|
||||
let (q_til, q_fra) = match l.ventilasjon.typ {
|
||||
VentilasjonType::Balansert => (q_snitt, q_snitt),
|
||||
VentilasjonType::Avtrekk => (0.0, q_snitt),
|
||||
VentilasjonType::Naturlig => (0.0, 0.0),
|
||||
};
|
||||
let ubalanse = (q_til - q_fra).abs();
|
||||
let nevner = if l.n50 * v > 1e-9 {
|
||||
1.0 + (f / e) * (ubalanse / (v * l.n50)).powi(2)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let n_inf = l.n50 * e / nevner;
|
||||
n_inf * v
|
||||
}
|
||||
|
||||
/// Ventilasjonsluftmengde for en klokketime, m³/h.
|
||||
pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
|
||||
let spesifikk = if l.ventilasjon.driftstid.aktiv(klokketime) {
|
||||
l.ventilasjon.luftmengde
|
||||
} else {
|
||||
l.ventilasjon.luftmengde_natt
|
||||
};
|
||||
spesifikk * l.bra
|
||||
}
|
||||
|
||||
/// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C.
|
||||
pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 {
|
||||
match l.ventilasjon.typ {
|
||||
VentilasjonType::Balansert => t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute),
|
||||
_ => t_ute,
|
||||
}
|
||||
}
|
||||
|
||||
/// Vifteeffekt for en klokketime, W.
|
||||
pub fn vifteeffekt(l: &Lost, klokketime: usize) -> f64 {
|
||||
if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) {
|
||||
return 0.0;
|
||||
}
|
||||
let q = luftmengde(l, klokketime) / 3600.0; // m³/s
|
||||
l.ventilasjon.sfp * 1000.0 * q
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! CSV-utskrift: timeserie for hele året og månedssummer.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn timeserie(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let mut w = csv::Writer::from_path(sti)?;
|
||||
w.write_record([
|
||||
"time",
|
||||
"maaned",
|
||||
"t_ute_C",
|
||||
"t_inne_C",
|
||||
"t_operativ_C",
|
||||
"sol_horisontal_W_per_m2",
|
||||
"soltilskudd_W",
|
||||
"internlast_W",
|
||||
"romoppvarming_W",
|
||||
"ventilasjonsvarme_W",
|
||||
"varmtvann_W",
|
||||
"romkjoling_W",
|
||||
"ventilasjonskjoling_W",
|
||||
"vifter_W",
|
||||
"pumper_W",
|
||||
"belysning_W",
|
||||
"utstyr_W",
|
||||
"solceller_W",
|
||||
])?;
|
||||
let sol = r.solceller.as_ref().map(|p| p.serie.as_slice());
|
||||
let ts = &r.timeserie;
|
||||
for t in 0..crate::TIMER {
|
||||
let f = |v: f64| format!("{v:.1}");
|
||||
w.write_record([
|
||||
t.to_string(),
|
||||
crate::MAANEDER[crate::maaned_for_time(t)].to_string(),
|
||||
format!("{:.2}", ts.t_ute[t]),
|
||||
format!("{:.2}", ts.t_inne[t]),
|
||||
format!("{:.2}", ts.t_operativ[t]),
|
||||
f(ts.sol_horisontal[t]),
|
||||
f(ts.soltilskudd[t]),
|
||||
f(ts.internlast[t]),
|
||||
f(ts.romoppvarming[t]),
|
||||
f(ts.ventilasjonsvarme[t]),
|
||||
f(ts.varmtvann[t]),
|
||||
f(ts.romkjoling[t]),
|
||||
f(ts.ventilasjonskjoling[t]),
|
||||
f(ts.vifter[t]),
|
||||
f(ts.pumper[t]),
|
||||
f(ts.belysning[t]),
|
||||
f(ts.utstyr[t]),
|
||||
f(sol.map(|s| s[t]).unwrap_or(0.0)),
|
||||
])?;
|
||||
}
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn maanedsverdier(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let m = r.sammendrag().maanedsverdier;
|
||||
let mut w = csv::Writer::from_path(sti)?;
|
||||
w.write_record([
|
||||
"maaned",
|
||||
"utetemperatur_C",
|
||||
"romoppvarming_kWh",
|
||||
"ventilasjonsvarme_kWh",
|
||||
"varmtvann_kWh",
|
||||
"kjoling_kWh",
|
||||
"vifter_pumper_kWh",
|
||||
"belysning_kWh",
|
||||
"utstyr_kWh",
|
||||
"sum_kWh",
|
||||
])?;
|
||||
for i in 0..12 {
|
||||
let sum = m.romoppvarming[i]
|
||||
+ m.ventilasjonsvarme[i]
|
||||
+ m.varmtvann[i]
|
||||
+ m.kjoling[i]
|
||||
+ m.vifter_pumper[i]
|
||||
+ m.belysning[i]
|
||||
+ m.utstyr[i];
|
||||
w.write_record([
|
||||
m.maaned[i].to_string(),
|
||||
format!("{:.1}", m.utetemperatur[i]),
|
||||
format!("{:.0}", m.romoppvarming[i]),
|
||||
format!("{:.0}", m.ventilasjonsvarme[i]),
|
||||
format!("{:.0}", m.varmtvann[i]),
|
||||
format!("{:.0}", m.kjoling[i]),
|
||||
format!("{:.0}", m.vifter_pumper[i]),
|
||||
format!("{:.0}", m.belysning[i]),
|
||||
format!("{:.0}", m.utstyr[i]),
|
||||
format!("{sum:.0}"),
|
||||
])?;
|
||||
}
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
//! Eksport til XML-formatet energimerkesystemet (EMS) tar imot for NS 3031:2014-beregninger.
|
||||
//!
|
||||
//! Feltnavnene følger Enovas eksempelfil i veiledningen for opplasting av flere filer.
|
||||
//! Formatet er knyttet til NS 3031:2014; Enova tar i tillegg imot JSON etter NS 3031:2025,
|
||||
//! men den spesifikasjonen er bare tilgjengelig bak ekspertinnlogging. Bruk derfor denne
|
||||
//! eksporten bevisst: tallene er beregnet etter NS 3031:2025 og skrives inn i 2014-strukturen.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::modell::{FlateType, VentilasjonType};
|
||||
use crate::systemer::Energibaerer;
|
||||
use crate::Result;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
fn esc(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
fn areal(r: &Resultat, f: impl Fn(&crate::modell::lost::LostFlate) -> bool) -> f64 {
|
||||
r.lost.flater.iter().filter(|x| f(x)).map(|x| x.areal).sum()
|
||||
}
|
||||
|
||||
fn u_snitt(r: &Resultat, f: impl Fn(&crate::modell::lost::LostFlate) -> bool + Copy) -> f64 {
|
||||
let a = areal(r, f);
|
||||
if a <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
r.lost
|
||||
.flater
|
||||
.iter()
|
||||
.filter(|x| f(x))
|
||||
.map(|x| x.areal * x.u_verdi)
|
||||
.sum::<f64>()
|
||||
/ a
|
||||
}
|
||||
|
||||
pub fn skriv(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
std::fs::write(sti, kilde(r))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kilde(r: &Resultat) -> String {
|
||||
let l = &r.lost;
|
||||
let b = &r.bygning.bygning;
|
||||
let ts = &r.timeserie;
|
||||
let kwh = crate::motor::Timeserie::sum_kwh;
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, r#"<?xml version="1.0" encoding="UTF-8"?>"#);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
r#"<Enheter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">"#
|
||||
);
|
||||
let _ = writeln!(s, " <Enhet>");
|
||||
|
||||
// ── adresse ────────────────────────────────────────────────────
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Adresser>\n <BygningsAdresse>\n <AdresseInfo>"
|
||||
);
|
||||
if let Some(a) = &b.adresse {
|
||||
let _ = writeln!(s, " <Gateadresse>{}</Gateadresse>", esc(a));
|
||||
}
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <BygningsbeskrivelseFritekst>{}</BygningsbeskrivelseFritekst>",
|
||||
esc(&b.navn)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" </AdresseInfo>\n </BygningsAdresse>\n </Adresser>"
|
||||
);
|
||||
|
||||
// ── inngangsverdier ────────────────────────────────────────────
|
||||
let _ = writeln!(s, " <Inngangsverdi>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Bygningskategori>{}</Bygningskategori>",
|
||||
esc(&r.karakter.kategorinavn.to_uppercase())
|
||||
);
|
||||
let _ = writeln!(s, " <Byggeaar>{}</Byggeaar>", b.byggeaar);
|
||||
let vegger = areal(r, |f| !f.glass && f.typ == FlateType::Vegg);
|
||||
let tak = areal(r, |f| !f.glass && f.typ == FlateType::Tak);
|
||||
let gulv = areal(r, |f| !f.glass && f.typ == FlateType::Gulv);
|
||||
let glass = areal(r, |f| f.glass);
|
||||
let _ = writeln!(s, " <M2Yttervegger>{vegger:.0}</M2Yttervegger>");
|
||||
let _ = writeln!(s, " <M2Tak>{tak:.0}</M2Tak>");
|
||||
let _ = writeln!(s, " <M2Gulv>{gulv:.0}</M2Gulv>");
|
||||
let _ = writeln!(s, " <M2VinduDorGlass>{glass:.0}</M2VinduDorGlass>");
|
||||
let _ = writeln!(s, " <BRAOppvarmet>{:.0}</BRAOppvarmet>", l.bra);
|
||||
let _ = writeln!(s, " <BRA>{:.0}</BRA>", l.bra);
|
||||
let _ = writeln!(s, " <M3OppvarmetLuft>{:.0}</M3OppvarmetLuft>", l.volum);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UYttervegger>{:.2}</UYttervegger>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Vegg)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UTak>{:.2}</UTak>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Tak)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UGulv>{:.2}</UGulv>",
|
||||
u_snitt(r, |f| !f.glass && f.typ == FlateType::Gulv)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <UVinduer>{:.2}</UVinduer>",
|
||||
u_snitt(r, |f| f.glass)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <ArealAndelVinduDorGlass>{:.1}</ArealAndelVinduDorGlass>",
|
||||
glass / l.bra * 100.0
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <NormalisertKuldebro>{:.2}</NormalisertKuldebro>",
|
||||
l.kuldebro_w_per_k / l.bra
|
||||
);
|
||||
let _ = writeln!(s, " <Lekkasjetall>{:.2}</Lekkasjetall>", l.n50);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <TempVirkningsgradVarmegjenvinner>{:.2}</TempVirkningsgradVarmegjenvinner>",
|
||||
l.ventilasjon.gjenvinning * 100.0
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <VifteeffektLuftmengderIDriftstiden>{:.2}</VifteeffektLuftmengderIDriftstiden>",
|
||||
l.ventilasjon.sfp
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <VentilasjonsluftmengdeIDriftstidenGjSn>{:.2}</VentilasjonsluftmengdeIDriftstidenGjSn>",
|
||||
l.ventilasjon.luftmengde
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <SettpunkttemperaturOppvarming>{:.1}</SettpunkttemperaturOppvarming>",
|
||||
l.settpunkt_oppvarming
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <SettpunkttemperaturKjoling>{:.1}</SettpunkttemperaturKjoling>",
|
||||
l.settpunkt_kjoling.unwrap_or(0.0)
|
||||
);
|
||||
let _ = writeln!(s, " <Driftstider>");
|
||||
let d = &l.laster.oppvarming_driftstid;
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Ventilasjon>{:.0}</Ventilasjon>",
|
||||
l.ventilasjon.driftstid.timer()
|
||||
);
|
||||
let _ = writeln!(s, " <Oppvarming>{:.0}</Oppvarming>", d.timer());
|
||||
let _ = writeln!(s, " </Driftstider>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Ventilasjonstype>{}</Ventilasjonstype>",
|
||||
match l.ventilasjon.typ {
|
||||
VentilasjonType::Balansert => "BALANSERT",
|
||||
VentilasjonType::Avtrekk => "AVTREKK",
|
||||
VentilasjonType::Naturlig => "NATURLIG",
|
||||
}
|
||||
);
|
||||
let kilder: Vec<String> = r
|
||||
.systemer
|
||||
.kilder
|
||||
.iter()
|
||||
.filter(|k| k.andel_rom > 1e-6)
|
||||
.map(|k| k.beskrivelse.clone())
|
||||
.collect();
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Oppvarmingssystem>{}</Oppvarmingssystem>",
|
||||
esc(&kilder.join("; "))
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <KlimastasjonKilde>{} (TMY CERRA 1991-2020)</KlimastasjonKilde>",
|
||||
esc(&r.klimanavn)
|
||||
);
|
||||
let _ = writeln!(s, " <Programvare>");
|
||||
let _ = writeln!(s, " <Applikasjon>enok</Applikasjon>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Versjon>{}</Versjon>",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <FriBeskrivelse>Dynamisk timesberegning etter NS 3031:2025 / NS-EN ISO 52016-1</FriBeskrivelse>"
|
||||
);
|
||||
let _ = writeln!(s, " </Programvare>");
|
||||
if let Some(e) = &b.energiraadgiver {
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Energiradgiver>\n <Navn>{}</Navn>\n </Energiradgiver>",
|
||||
esc(e)
|
||||
);
|
||||
}
|
||||
let _ = writeln!(s, " </Inngangsverdi>");
|
||||
|
||||
// ── beregningsresultat ─────────────────────────────────────────
|
||||
let _ = writeln!(s, " <Beregningsresultat>");
|
||||
let _ = writeln!(s, " <NettoEnergibudsjettPrKvm>");
|
||||
let p = |navn: &str, v: f64| format!(" <{navn}>{:.1}</{navn}>", v / l.bra);
|
||||
let _ = writeln!(s, "{}", p("Romoppvarming", kwh(&ts.romoppvarming)));
|
||||
let _ = writeln!(s, "{}", p("Ventilasjonsvarme", kwh(&ts.ventilasjonsvarme)));
|
||||
let _ = writeln!(s, "{}", p("Varmtvann", kwh(&ts.varmtvann)));
|
||||
let _ = writeln!(s, "{}", p("Vifter", kwh(&ts.vifter)));
|
||||
let _ = writeln!(s, "{}", p("Pumper", kwh(&ts.pumper)));
|
||||
let _ = writeln!(s, "{}", p("Belysning", kwh(&ts.belysning)));
|
||||
let _ = writeln!(s, "{}", p("TekniskUtstyr", kwh(&ts.utstyr)));
|
||||
let _ = writeln!(s, "{}", p("Romkjoling", kwh(&ts.romkjoling)));
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"{}",
|
||||
p("Ventilasjonskjoling", kwh(&ts.ventilasjonskjoling))
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <TotaltNettoEnergibehov>{:.1}</TotaltNettoEnergibehov>",
|
||||
r.budsjett.netto_spesifikk(l.bra)
|
||||
);
|
||||
let _ = writeln!(s, " </NettoEnergibudsjettPrKvm>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <LevertEnergiSpesifiktNormalisertKlima>{:.2}</LevertEnergiSpesifiktNormalisertKlima>",
|
||||
r.levert.spesifikk_vektet
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <LevertEnergiNormalisertKlima>{:.0}</LevertEnergiNormalisertKlima>",
|
||||
r.levert.vektet_totalt
|
||||
);
|
||||
let _ = writeln!(s, " <ForventetLevertEnergi>\n <LevertEnergi>");
|
||||
let hent = |b: Energibaerer| -> f64 {
|
||||
r.levert
|
||||
.per_baerer
|
||||
.iter()
|
||||
.find(|(x, _)| *x == b)
|
||||
.map(|(_, v)| *v)
|
||||
.unwrap_or(0.0)
|
||||
};
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Elektrisitet>{:.0}</Elektrisitet>",
|
||||
hent(Energibaerer::Elektrisitet)
|
||||
);
|
||||
let _ = writeln!(s, " <Olje>{:.0}</Olje>", hent(Energibaerer::Olje));
|
||||
let _ = writeln!(s, " <Gass>{:.0}</Gass>", hent(Energibaerer::Gass));
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Fjernvarme>{:.0}</Fjernvarme>",
|
||||
hent(Energibaerer::Fjernvarme)
|
||||
);
|
||||
let bio = hent(Energibaerer::BiobrenselLukket) + hent(Energibaerer::BiobrenselAapent);
|
||||
let _ = writeln!(s, " <Biobrensel>{bio:.0}</Biobrensel>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <AnnenEnergivare>{:.0}</AnnenEnergivare>",
|
||||
hent(Energibaerer::Annet)
|
||||
);
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Totalt>{:.0}</Totalt>",
|
||||
r.levert.levert_totalt
|
||||
);
|
||||
let _ = writeln!(s, " </LevertEnergi>\n </ForventetLevertEnergi>");
|
||||
let _ = writeln!(
|
||||
s,
|
||||
" <Energikarakter>{}</Energikarakter>",
|
||||
r.karakter.bokstav
|
||||
);
|
||||
let _ = writeln!(s, " </Beregningsresultat>");
|
||||
let _ = writeln!(s, " </Enhet>\n</Enheter>");
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! JSON-utskrift av beregningsresultatet.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::Result;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn skriv(r: &Resultat, sti: &Path) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(&r.sammendrag())?;
|
||||
std::fs::write(sti, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn til_streng(r: &Resultat) -> Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&r.sammendrag())?)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Rapportering: CSV-timeserier, JSON-resultat, Typst-rapport med PDF, og Enova-XML.
|
||||
|
||||
pub mod csv_ut;
|
||||
pub mod enova_xml;
|
||||
pub mod json_ut;
|
||||
pub mod svg;
|
||||
pub mod typst_ut;
|
||||
@@ -0,0 +1,260 @@
|
||||
//! Enkle SVG-figurer som bygges inn i Typst-rapporten. Diagrammene tegnes direkte som SVG
|
||||
//! slik at rapporten ikke er avhengig av eksterne Typst-pakker.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
|
||||
const BREDDE: f64 = 760.0;
|
||||
const HOYDE: f64 = 300.0;
|
||||
const VENSTRE: f64 = 58.0;
|
||||
const HOYRE: f64 = 16.0;
|
||||
const TOPP: f64 = 16.0;
|
||||
const BUNN: f64 = 42.0;
|
||||
|
||||
/// Fargene i figurene. Holdt mørke nok til å leses i svart-hvitt-utskrift.
|
||||
const FARGER: [&str; 7] = [
|
||||
"#8c2f1f", // romoppvarming
|
||||
"#c2703d", // ventilasjonsvarme
|
||||
"#3f6f8f", // varmtvann
|
||||
"#6f9ec4", // vifter og pumper
|
||||
"#d8b44a", // belysning
|
||||
"#9a9a4a", // utstyr
|
||||
"#4f8f8f", // kjøling
|
||||
];
|
||||
const NAVN: [&str; 7] = [
|
||||
"Romoppvarming",
|
||||
"Ventilasjonsvarme",
|
||||
"Varmtvann",
|
||||
"Vifter og pumper",
|
||||
"Belysning",
|
||||
"Teknisk utstyr",
|
||||
"Kjøling",
|
||||
];
|
||||
|
||||
fn esc(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
fn pent_maks(v: f64) -> f64 {
|
||||
if v <= 0.0 {
|
||||
return 1.0;
|
||||
}
|
||||
let eksponent = v.log10().floor();
|
||||
let skala = 10f64.powf(eksponent);
|
||||
let normalisert = v / skala;
|
||||
let trinn = [1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0];
|
||||
let valgt = trinn
|
||||
.iter()
|
||||
.find(|t| **t >= normalisert)
|
||||
.copied()
|
||||
.unwrap_or(10.0);
|
||||
valgt * skala
|
||||
}
|
||||
|
||||
/// Stablet søylediagram over månedlig energibruk, kWh.
|
||||
pub fn maanedsdiagram(r: &Resultat) -> String {
|
||||
let m = r.sammendrag().maanedsverdier;
|
||||
let serier = [
|
||||
m.romoppvarming,
|
||||
m.ventilasjonsvarme,
|
||||
m.varmtvann,
|
||||
m.vifter_pumper,
|
||||
m.belysning,
|
||||
m.utstyr,
|
||||
m.kjoling,
|
||||
];
|
||||
let mut totaler = [0.0f64; 12];
|
||||
for s in &serier {
|
||||
for i in 0..12 {
|
||||
totaler[i] += s[i];
|
||||
}
|
||||
}
|
||||
let maks = pent_maks(totaler.iter().cloned().fold(0.0, f64::max));
|
||||
let plot_b = BREDDE - VENSTRE - HOYRE;
|
||||
let plot_h = HOYDE - TOPP - BUNN;
|
||||
let sb = plot_b / 12.0;
|
||||
let bredde = sb * 0.68;
|
||||
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {BREDDE} {HOYDE}" font-family="sans-serif" font-size="10">"##
|
||||
));
|
||||
// Rutenett og akse.
|
||||
for i in 0..=4 {
|
||||
let v = maks * i as f64 / 4.0;
|
||||
let y = TOPP + plot_h - plot_h * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{y:.1}" x2="{:.1}" y2="{y:.1}" stroke="#d8d8d8" stroke-width="0.7"/>"##,
|
||||
BREDDE - HOYRE
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" text-anchor="end" fill="#555">{v:.0}</text>"##,
|
||||
VENSTRE - 6.0,
|
||||
y + 3.5
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<text x="4" y="12" fill="#555">kWh</text><line x1="{VENSTRE}" y1="{:.1}" x2="{:.1}" y2="{:.1}" stroke="#444" stroke-width="1"/>"##,
|
||||
TOPP + plot_h,
|
||||
BREDDE - HOYRE,
|
||||
TOPP + plot_h
|
||||
));
|
||||
// Søyler.
|
||||
for i in 0..12 {
|
||||
let x = VENSTRE + sb * i as f64 + (sb - bredde) / 2.0;
|
||||
let mut y = TOPP + plot_h;
|
||||
for (k, serie) in serier.iter().enumerate() {
|
||||
let h = serie[i] / maks * plot_h;
|
||||
if h <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
y -= h;
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="{x:.1}" y="{y:.1}" width="{bredde:.1}" height="{h:.1}" fill="{}"/>"##,
|
||||
FARGER[k]
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" text-anchor="middle" fill="#444">{}</text>"##,
|
||||
VENSTRE + sb * i as f64 + sb / 2.0,
|
||||
TOPP + plot_h + 14.0,
|
||||
m.maaned[i]
|
||||
));
|
||||
}
|
||||
// Tegnforklaring.
|
||||
let mut x = VENSTRE;
|
||||
let y = HOYDE - 12.0;
|
||||
for (k, navn) in NAVN.iter().enumerate() {
|
||||
if serier[k].iter().sum::<f64>() <= 0.5 {
|
||||
continue;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="{x:.1}" y="{:.1}" width="9" height="9" fill="{}"/><text x="{:.1}" y="{:.1}" fill="#333">{}</text>"##,
|
||||
y - 8.0,
|
||||
FARGER[k],
|
||||
x + 13.0,
|
||||
y,
|
||||
esc(navn)
|
||||
));
|
||||
x += 16.0 + navn.chars().count() as f64 * 5.3;
|
||||
}
|
||||
s.push_str("</svg>");
|
||||
s
|
||||
}
|
||||
|
||||
/// Varighetskurve for romoppvarming: effekt sortert fallende over årets timer.
|
||||
pub fn varighetskurve(r: &Resultat) -> String {
|
||||
let mut v: Vec<f64> = r
|
||||
.timeserie
|
||||
.romoppvarming
|
||||
.iter()
|
||||
.zip(&r.timeserie.ventilasjonsvarme)
|
||||
.map(|(a, b)| (a + b) / 1000.0)
|
||||
.collect();
|
||||
v.sort_by(|a, b| b.partial_cmp(a).unwrap());
|
||||
let maks = pent_maks(v.first().copied().unwrap_or(1.0));
|
||||
let plot_b = BREDDE - VENSTRE - HOYRE;
|
||||
let plot_h = HOYDE - TOPP - BUNN;
|
||||
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {BREDDE} {HOYDE}" font-family="sans-serif" font-size="10">"##
|
||||
));
|
||||
for i in 0..=4 {
|
||||
let val = maks * i as f64 / 4.0;
|
||||
let y = TOPP + plot_h - plot_h * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{y:.1}" x2="{:.1}" y2="{y:.1}" stroke="#d8d8d8" stroke-width="0.7"/><text x="{:.1}" y="{:.1}" text-anchor="end" fill="#555">{val:.1}</text>"##,
|
||||
BREDDE - HOYRE,
|
||||
VENSTRE - 6.0,
|
||||
y + 3.5
|
||||
));
|
||||
}
|
||||
s.push_str(r##"<text x="4" y="12" fill="#555">kW</text>"##);
|
||||
let mut d = String::from("M");
|
||||
for (i, verdi) in v.iter().enumerate().step_by(4) {
|
||||
let x = VENSTRE + plot_b * i as f64 / v.len() as f64;
|
||||
let y = TOPP + plot_h - verdi / maks * plot_h;
|
||||
d.push_str(&format!(" {x:.1},{y:.1}"));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<path d="{d}" fill="none" stroke="{}" stroke-width="1.6"/>"##,
|
||||
FARGER[0]
|
||||
));
|
||||
for i in 0..=4 {
|
||||
let timer = 8760 * i / 4;
|
||||
let x = VENSTRE + plot_b * i as f64 / 4.0;
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{x:.1}" y="{:.1}" text-anchor="middle" fill="#444">{timer}</text>"##,
|
||||
TOPP + plot_h + 14.0
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
r##"<line x1="{VENSTRE}" y1="{:.1}" x2="{:.1}" y2="{:.1}" stroke="#444" stroke-width="1"/><text x="{:.1}" y="{:.1}" text-anchor="middle" fill="#555">timer per år</text></svg>"##,
|
||||
TOPP + plot_h,
|
||||
BREDDE - HOYRE,
|
||||
TOPP + plot_h,
|
||||
VENSTRE + plot_b / 2.0,
|
||||
HOYDE - 10.0
|
||||
));
|
||||
s
|
||||
}
|
||||
|
||||
/// Karakterskalaen A–G med bygningens plassering markert.
|
||||
pub fn karakterstige(r: &Resultat) -> String {
|
||||
let k = &r.karakter;
|
||||
let farger = [
|
||||
"#1d7a35", "#4aab3a", "#a8c93a", "#f2d21e", "#f2a01e", "#e0661e", "#cf3030",
|
||||
];
|
||||
let rad_h = 30.0;
|
||||
let h = rad_h * 7.0 + 30.0;
|
||||
let b = 470.0;
|
||||
let mut s = String::new();
|
||||
s.push_str(&format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {b} {h}" font-family="sans-serif" font-size="11">"##
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="0" y="12" fill="#555">Spesifikk vektet levert energi, kWh/(m²·år) — {}</text>"##,
|
||||
esc(&k.kategorinavn)
|
||||
));
|
||||
for (i, bokstav) in crate::merking::skala::BOKSTAVER.iter().enumerate() {
|
||||
let y = 24.0 + rad_h * i as f64;
|
||||
let bredde = 120.0 + 28.0 * i as f64;
|
||||
let valgt = *bokstav == k.bokstav;
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="0" y="{y:.1}" width="{bredde:.1}" height="{:.1}" rx="3" fill="{}"/>"##,
|
||||
rad_h - 5.0,
|
||||
farger[i]
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="12" y="{:.1}" font-size="15" font-weight="bold" fill="#ffffff">{bokstav}</text>"##,
|
||||
y + 17.0
|
||||
));
|
||||
let tekst = if i < 6 {
|
||||
format!("≤ {:.0}", k.grenser[i])
|
||||
} else {
|
||||
format!("over {:.0}", k.grenser[5])
|
||||
};
|
||||
s.push_str(&format!(
|
||||
r##"<text x="36" y="{:.1}" fill="#ffffff">{tekst}</text>"##,
|
||||
y + 16.0
|
||||
));
|
||||
if valgt {
|
||||
s.push_str(&format!(
|
||||
r##"<rect x="-2.5" y="{:.1}" width="{:.1}" height="{:.1}" rx="4" fill="none" stroke="#111" stroke-width="2"/>"##,
|
||||
y - 2.5,
|
||||
bredde + 5.0,
|
||||
rad_h
|
||||
));
|
||||
s.push_str(&format!(
|
||||
r##"<text x="{:.1}" y="{:.1}" font-size="13" font-weight="bold" fill="#111">◀ {:.0} kWh/(m²·år)</text>"##,
|
||||
bredde + 14.0,
|
||||
y + 17.0,
|
||||
k.spesifikk_vektet
|
||||
));
|
||||
}
|
||||
}
|
||||
s.push_str("</svg>");
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
//! Genererer energirapporten som Typst-kilde og kompilerer den til PDF.
|
||||
|
||||
use crate::kjor::Resultat;
|
||||
use crate::merking::skala::BOKSTAVER;
|
||||
use crate::modell::{FlateType, Mot};
|
||||
use crate::rapport::svg;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
const STIL: &str = include_str!("../../typst/stil.typ");
|
||||
|
||||
const KARAKTERFARGER: [&str; 7] = [
|
||||
"#1d7a35", "#4aab3a", "#a8c93a", "#f2d21e", "#f2a01e", "#e0661e", "#cf3030",
|
||||
];
|
||||
|
||||
/// Escaper tekst slik at den kan settes inn i Typst-markup.
|
||||
fn e(s: &str) -> String {
|
||||
let mut ut = String::with_capacity(s.len() + 8);
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' | '#' | '$' | '*' | '_' | '`' | '<' | '>' | '@' | '[' | ']' | '"' => {
|
||||
ut.push('\\');
|
||||
ut.push(c);
|
||||
}
|
||||
_ => ut.push(c),
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn tall(v: f64, desimaler: usize) -> String {
|
||||
// Norsk desimalkomma og tusenskille med tynt mellomrom.
|
||||
let s = format!("{v:.desimaler$}");
|
||||
let (heltall, rest) = match s.split_once('.') {
|
||||
Some((a, b)) => (a.to_string(), Some(b.to_string())),
|
||||
None => (s.clone(), None),
|
||||
};
|
||||
let negativ = heltall.starts_with('-');
|
||||
let siffer: Vec<char> = heltall.trim_start_matches('-').chars().collect();
|
||||
let mut gruppert = String::new();
|
||||
for (i, c) in siffer.iter().enumerate() {
|
||||
if i > 0 && (siffer.len() - i).is_multiple_of(3) {
|
||||
gruppert.push('\u{202f}');
|
||||
}
|
||||
gruppert.push(*c);
|
||||
}
|
||||
let mut ut = String::new();
|
||||
if negativ {
|
||||
ut.push('−');
|
||||
}
|
||||
ut.push_str(&gruppert);
|
||||
if let Some(r) = rest {
|
||||
ut.push(',');
|
||||
ut.push_str(&r);
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn rad(celler: &[String]) -> String {
|
||||
let inner: Vec<String> = celler.iter().map(|c| format!("[{c}]")).collect();
|
||||
format!("({}),", inner.join(", "))
|
||||
}
|
||||
|
||||
/// Skriver `rapport.typ`, `stil.typ` og figurene til katalogen.
|
||||
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
||||
std::fs::create_dir_all(katalog)?;
|
||||
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
||||
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
|
||||
std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?;
|
||||
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
||||
std::fs::write(katalog.join("rapport.typ"), kilde(r, scenarier))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kompilerer rapporten med `typst`. Returnerer stien til PDF-en.
|
||||
pub fn kompiler(katalog: &Path) -> Result<std::path::PathBuf> {
|
||||
let typ = katalog.join("rapport.typ");
|
||||
let pdf = katalog.join("rapport.pdf");
|
||||
let ut = std::process::Command::new("typst")
|
||||
.arg("compile")
|
||||
.arg(&typ)
|
||||
.arg(&pdf)
|
||||
.output()
|
||||
.context("kunne ikke kjøre `typst`. Installer Typst, eller bruk --ingen-pdf")?;
|
||||
if !ut.status.success() {
|
||||
bail!(
|
||||
"typst klarte ikke å kompilere rapporten:\n{}",
|
||||
String::from_utf8_lossy(&ut.stderr)
|
||||
);
|
||||
}
|
||||
Ok(pdf)
|
||||
}
|
||||
|
||||
fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
||||
let l = &r.lost;
|
||||
let k = &r.karakter;
|
||||
let b = &r.bygning.bygning;
|
||||
let farge = KARAKTERFARGER[BOKSTAVER.iter().position(|x| *x == k.bokstav).unwrap_or(6)];
|
||||
let mut s = String::new();
|
||||
|
||||
s.push_str("#import \"stil.typ\": *\n\n");
|
||||
s.push_str(&format!(
|
||||
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n",
|
||||
e(&l.navn),
|
||||
e(&bunntekst(r))
|
||||
));
|
||||
|
||||
// ── forside ─────────────────────────────────────────────────────
|
||||
s.push_str(&format!(
|
||||
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\
|
||||
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
||||
e(&l.navn)
|
||||
));
|
||||
if let Some(a) = &b.adresse {
|
||||
s.push_str(&format!("#text(size: 11pt, fill: mid)[{}]\n\n", e(a)));
|
||||
}
|
||||
s.push_str("#v(0.8cm)\n");
|
||||
s.push_str(&format!(
|
||||
"#grid(columns: (auto, 1fr), gutter: 18pt,\n karakterrute(\"{}\", \"{}\", rgb(\"{}\")),\n faktaboks[\n",
|
||||
k.bokstav,
|
||||
tall(k.spesifikk_vektet, 0),
|
||||
farge
|
||||
));
|
||||
s.push_str(&format!(
|
||||
" *Energikarakter {}* for {}. \\\n",
|
||||
k.bokstav,
|
||||
e(&k.kategorinavn.to_lowercase())
|
||||
));
|
||||
s.push_str(
|
||||
" Karakteren bygger på beregnet klimakorrigert normert energiytelse etter NS 3031:2025, \
|
||||
oppgitt som vektet levert energi per kvadratmeter oppvarmet BRA. \\\n",
|
||||
);
|
||||
if let Some(t) = k.til_neste {
|
||||
s.push_str(&format!(
|
||||
" Energiytelsen må ned {} kWh/(m²·år) for å nå neste karakter. \\\n",
|
||||
tall(t, 0)
|
||||
));
|
||||
} else {
|
||||
s.push_str(" Dette er den beste karakteren på skalaen. \\\n");
|
||||
}
|
||||
s.push_str(" ]\n)\n\n#v(0.6cm)\n");
|
||||
|
||||
// Nøkkeltall.
|
||||
let nøkkel = vec![
|
||||
("Bygningskategori".to_string(), e(&k.kategorinavn)),
|
||||
("Byggeår".to_string(), l.byggeaar.to_string()),
|
||||
(
|
||||
"Oppvarmet BRA".to_string(),
|
||||
format!("{} m²", tall(l.bra, 0)),
|
||||
),
|
||||
(
|
||||
"Netto energibehov".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(r.budsjett.netto_spesifikk(l.bra), 1)),
|
||||
),
|
||||
(
|
||||
"Levert energi".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(r.levert.levert_totalt / l.bra, 1)),
|
||||
),
|
||||
(
|
||||
"Vektet levert energi".to_string(),
|
||||
format!("{} kWh/(m²·år)", tall(k.spesifikk_vektet, 1)),
|
||||
),
|
||||
("Klimadata".to_string(), e(&r.klimanavn)),
|
||||
(
|
||||
"Varmetapstall".to_string(),
|
||||
format!("{} W/(m²K)", tall(r.varmetap.totalt, 2)),
|
||||
),
|
||||
];
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto),\n justering: (left, right),\n overskrift: (\"Nøkkeltall\", \"\"),\n rader: (\n");
|
||||
for (a, b2) in &nøkkel {
|
||||
s.push_str(&format!(" {}\n", rad(&[a.clone(), b2.clone()])));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
if let Some(kor) = &r.levert.klimakorrigering {
|
||||
s.push_str(&format!(
|
||||
"#merknad[Beregningen er gjort med klimadata for {}. De klimaavhengige energipostene er \
|
||||
korrigert til referanseklima Oslo med klimakorrigeringsfaktor {} etter NS 3031:2025, \
|
||||
slik energimerkeforskriften krever.]\n\n",
|
||||
e(&kor.sted),
|
||||
tall(kor.faktor, 3)
|
||||
));
|
||||
}
|
||||
|
||||
s.push_str("#pagebreak()\n\n");
|
||||
|
||||
// ── energikarakter ──────────────────────────────────────────────
|
||||
s.push_str("= Energikarakter\n\n");
|
||||
s.push_str("#image(\"fig-karakter.svg\", width: 100%)\n\n");
|
||||
s.push_str("Skalaen følger Enovas fastsettelse fra 1. januar 2026, jf. energimerkeforskriften § 10b. \
|
||||
Nedre grense for karakter A tilsvarer energikravene i TEK17 fratrukket 15 prosent, og et bygg \
|
||||
som innfrir TEK17 får karakter B. De 15 prosent minst energieffektive bygningene i hver \
|
||||
kategori får karakter G.\n\n");
|
||||
if k.grenser[0] != r.karakter.grenser[0] || l.skala_areal != l.bra {
|
||||
s.push_str(&format!(
|
||||
"Grensene er arealkorrigert med {} m² oppvarmet BRA, slik at små og store bygninger med \
|
||||
samme energikvaliteter får samme karakter.\n\n",
|
||||
tall(l.skala_areal, 0)
|
||||
));
|
||||
}
|
||||
|
||||
// ── varmetapsbudsjett ───────────────────────────────────────────
|
||||
let v = &r.varmetap;
|
||||
s.push_str("= Varmetapsbudsjett\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto),\n justering: (center, left, right),\n overskrift: (\"\", \"Varmetapspost\", \"Varmetapstall, W/(m²K)\"),\n sum-rader: (5, 7, 9),\n rader: (\n");
|
||||
let poster: Vec<(String, String, f64)> = vec![
|
||||
("1".into(), "Vegger".into(), v.vegger),
|
||||
("2".into(), "Tak".into(), v.tak),
|
||||
("3".into(), "Gulv".into(), v.gulv),
|
||||
("4".into(), "Vinduer og dører".into(), v.vinduer_dorer),
|
||||
("5".into(), "Kuldebroer".into(), v.kuldebroer),
|
||||
(
|
||||
"".into(),
|
||||
"Varmetapstall, transmisjon".into(),
|
||||
v.transmisjon,
|
||||
),
|
||||
("6".into(), "Infiltrasjon".into(), v.infiltrasjon),
|
||||
(
|
||||
"".into(),
|
||||
"Varmetapstall, klimaskjerm".into(),
|
||||
v.klimaskjerm,
|
||||
),
|
||||
("7".into(), "Ventilasjon".into(), v.ventilasjon),
|
||||
("".into(), "Varmetapstall, totalt".into(), v.totalt),
|
||||
];
|
||||
for (nr, navn, verdi) in &poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[nr.clone(), e(navn), tall(*verdi, 2)])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
s.push_str(&format!(
|
||||
"Klimaskjermarealet er {} m², som gir formfaktor {} m²/m². Vindus- og dørarealet utgjør \
|
||||
{} prosent av oppvarmet BRA. Lekkasjetallet er satt til {} luftvekslinger per time ved 50 Pa.\n\n",
|
||||
tall(l.klimaskjermareal(), 0),
|
||||
tall(l.formfaktor(), 2),
|
||||
tall(l.vindusareal() / l.bra * 100.0, 0),
|
||||
tall(l.n50, 1)
|
||||
));
|
||||
|
||||
// ── energibudsjett ──────────────────────────────────────────────
|
||||
s.push_str("= Energibudsjett\n\n== Netto energibehov (beregningspunkt A)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto, auto),\n justering: (center, left, right, right),\n overskrift: (\"\", \"Energipost\", \"kWh/år\", \"kWh/(m²·år)\"),\n rader: (\n");
|
||||
for p in &r.budsjett.poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
p.nr.to_string(),
|
||||
e(p.navn),
|
||||
tall(p.netto, 0),
|
||||
tall(p.netto / l.bra, 1)
|
||||
])
|
||||
));
|
||||
}
|
||||
let sumrad = r.budsjett.poster.len();
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({sumrad},),\n)\n\n",
|
||||
rad(&[
|
||||
"".into(),
|
||||
"Totalt netto energibehov".into(),
|
||||
tall(r.budsjett.netto_totalt, 0),
|
||||
tall(r.budsjett.netto_spesifikk(l.bra), 1)
|
||||
])
|
||||
));
|
||||
|
||||
s.push_str("== Fra netto til tilført energi (beregningspunkt A–C)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (auto, 1fr, auto, auto, auto, auto, auto),\n justering: (center, left, right, right, right, right, right),\n overskrift: (\"\", \"Energipost\", \"Netto\", \"Tap\", \"Brutto\", \"SPF\", \"Tilført\"),\n rader: (\n");
|
||||
for p in &r.budsjett.poster {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
p.nr.to_string(),
|
||||
e(p.navn),
|
||||
tall(p.netto / l.bra, 1),
|
||||
tall(p.tap / l.bra, 1),
|
||||
tall(p.brutto / l.bra, 1),
|
||||
if p.spf > 0.0 {
|
||||
tall(p.spf, 2)
|
||||
} else {
|
||||
"—".into()
|
||||
},
|
||||
tall(p.tilfort / l.bra, 1),
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({sumrad},),\n)\n\n",
|
||||
rad(&[
|
||||
"".into(),
|
||||
"Sum".into(),
|
||||
tall(r.budsjett.netto_totalt / l.bra, 1),
|
||||
tall(
|
||||
(r.budsjett.brutto_totalt - r.budsjett.netto_totalt) / l.bra,
|
||||
1
|
||||
),
|
||||
tall(r.budsjett.brutto_totalt / l.bra, 1),
|
||||
"".into(),
|
||||
tall(r.budsjett.tilfort_totalt / l.bra, 1),
|
||||
])
|
||||
));
|
||||
s.push_str("Alle tall i kWh per kvadratmeter oppvarmet BRA og år. SPF er systemets sesongjusterte ytelsesfaktor: forholdet mellom avgitt energi og tilført energi.\n\n");
|
||||
|
||||
// ── levert og vektet ────────────────────────────────────────────
|
||||
s.push_str("== Levert og vektet energi (beregningspunkt D og E)\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto),\n justering: (left, right, right, right),\n overskrift: (\"Energibærer\", \"kWh/år\", \"Vektingsfaktor\", \"Vektet, kWh/(m²·år)\"),\n rader: (\n");
|
||||
for (baerer, levert, vektet) in &r.levert.vektet {
|
||||
let faktor = if *levert > 1e-9 { vektet / levert } else { 0.0 };
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(baerer.navn()),
|
||||
tall(*levert, 0),
|
||||
tall(faktor, 2),
|
||||
tall(vektet / l.bra, 1)
|
||||
])
|
||||
));
|
||||
}
|
||||
let mut n_rader = r.levert.vektet.len();
|
||||
if r.levert.eksportert > 0.1 {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
"Egenprodusert el til eksport".into(),
|
||||
format!("−{}", tall(r.levert.eksportert, 0)),
|
||||
"0,00".into(),
|
||||
"0,0".into()
|
||||
])
|
||||
));
|
||||
n_rader += 1;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n ),\n sum-rader: ({n_rader},),\n)\n\n",
|
||||
rad(&[
|
||||
"Totalt vektet levert energi".into(),
|
||||
tall(r.levert.levert_totalt, 0),
|
||||
"".into(),
|
||||
tall(r.levert.spesifikk_vektet, 1)
|
||||
])
|
||||
));
|
||||
|
||||
if let Some(p) = &r.solceller {
|
||||
s.push_str(&format!(
|
||||
"Solcelleanlegget produserer {} kWh i året. Av dette dekker {} kWh byggets eget forbruk time \
|
||||
for time, mens {} kWh eksporteres. Eksportert energi har vektingsfaktor 0 og teller derfor \
|
||||
ikke med i energikarakteren.\n\n",
|
||||
tall(p.produsert_kwh, 0),
|
||||
tall(p.egenbruk_kwh, 0),
|
||||
tall(p.eksportert_kwh, 0)
|
||||
));
|
||||
}
|
||||
|
||||
// ── figurer ─────────────────────────────────────────────────────
|
||||
s.push_str("= Energibruk gjennom året\n\n#image(\"fig-maaned.svg\", width: 100%)\n\n");
|
||||
s.push_str(&format!(
|
||||
"Figuren viser netto energibehov per måned. Årsmiddeltemperaturen i klimadataene er {} °C, \
|
||||
og det er {} graddager under 17 °C.\n\n",
|
||||
tall(r.vaerdata.aarsmiddeltemperatur(), 1),
|
||||
tall(r.vaerdata.graddager(17.0), 0)
|
||||
));
|
||||
s.push_str("#image(\"fig-varighet.svg\", width: 100%)\n\n");
|
||||
s.push_str("Varighetskurven viser romoppvarming og ventilasjonsvarme sortert fallende over årets timer.\n\n");
|
||||
|
||||
let ef = &r.effektbehov;
|
||||
s.push_str("== Effektbehov\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto),\n justering: (left, right, right, left),\n overskrift: (\"Effektpost\", \"kW\", \"W/m²\", \"Tidspunkt\"),\n rader: (\n");
|
||||
s.push_str(&format!(
|
||||
" {}\n {}\n {}\n ),\n sum-rader: (2,),\n)\n\n",
|
||||
rad(&[
|
||||
"Maksimalt samtidig brutto termisk effektbehov".into(),
|
||||
tall(ef.termisk_w / 1000.0, 1),
|
||||
tall(ef.termisk_w / l.bra, 1),
|
||||
e(&ef.beskriv_termisk())
|
||||
]),
|
||||
rad(&[
|
||||
"Maksimalt samtidig el-spesifikt effektbehov".into(),
|
||||
tall(ef.elspesifikk_w / 1000.0, 1),
|
||||
tall(ef.elspesifikk_w / l.bra, 1),
|
||||
"".into()
|
||||
]),
|
||||
rad(&[
|
||||
"Maksimalt samtidig brutto effektbehov".into(),
|
||||
tall(ef.samlet_w / 1000.0, 1),
|
||||
tall(ef.samlet_w / l.bra, 1),
|
||||
e(&ef.beskriv_samlet())
|
||||
])
|
||||
));
|
||||
|
||||
// ── inndata ─────────────────────────────────────────────────────
|
||||
s.push_str("#pagebreak()\n\n= Inndata\n\n== Bygningskropp\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto, auto),\n justering: (left, left, right, right, right, left),\n overskrift: (\"Bygningsdel\", \"Type\", \"Areal, m²\", \"U-verdi\", \"Orientering\", \"Mot\"),\n rader: (\n");
|
||||
for f in &l.flater {
|
||||
let typ = if f.glass {
|
||||
"Vindu/dør"
|
||||
} else {
|
||||
match f.typ {
|
||||
FlateType::Vegg => "Vegg",
|
||||
FlateType::Tak => "Tak",
|
||||
FlateType::Gulv => "Gulv",
|
||||
}
|
||||
};
|
||||
let mot = match f.mot {
|
||||
Mot::Ute => "det fri",
|
||||
Mot::Grunn => "grunnen",
|
||||
Mot::Uoppvarmet => "uoppvarmet",
|
||||
};
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(&f.navn),
|
||||
typ.into(),
|
||||
tall(f.areal, 1),
|
||||
tall(f.u_verdi, 2),
|
||||
himmelretning(f.orientering, f.helning),
|
||||
mot.into()
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
s.push_str("== Varmeforsyning\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto),\n justering: (left, right, right, right, left),\n overskrift: (\"Varmekilde\", \"Romvarme\", \"Vent.varme\", \"Tappevann\", \"SPF\"),\n rader: (\n");
|
||||
for kilde in &r.systemer.kilder {
|
||||
if kilde.andel_rom + kilde.andel_ventilasjon + kilde.andel_tappevann < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(&kilde.beskrivelse),
|
||||
format!("{} %", tall(kilde.andel_rom * 100.0, 0)),
|
||||
format!("{} %", tall(kilde.andel_ventilasjon * 100.0, 0)),
|
||||
format!("{} %", tall(kilde.andel_tappevann * 100.0, 0)),
|
||||
format!("{} ({})", tall(kilde.spf, 2), e(&kilde.spf_kilde)),
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
|
||||
// ── forutsetninger ──────────────────────────────────────────────
|
||||
s.push_str("= Forutsetninger og normerte verdier\n\n");
|
||||
s.push_str(
|
||||
"Energimerkeberegningen bruker normerte inndata for drift og interne laster, slik at bygninger \
|
||||
kan sammenlignes på egenskaper og ikke på hvordan de brukes. Beregnet levert energi vil derfor \
|
||||
normalt avvike fra målt energiforbruk.\n\n",
|
||||
);
|
||||
if l.forutsetninger.is_empty() {
|
||||
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
||||
} else {
|
||||
s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
||||
for f in &l.forutsetninger {
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[e(&f.felt), e(&f.verdi), e(&f.kilde)])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
}
|
||||
for m in &r.merknader {
|
||||
s.push_str(&format!("#merknad[{}]\n\n", e(m)));
|
||||
}
|
||||
|
||||
// ── scenarier ───────────────────────────────────────────────────
|
||||
if !scenarier.is_empty() {
|
||||
s.push_str("#pagebreak()\n\n= Tiltak\n\n");
|
||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto),\n justering: (left, right, right, center, right),\n overskrift: (\"Alternativ\", \"Netto, kWh/(m²·år)\", \"Vektet levert, kWh/(m²·år)\", \"Karakter\", \"Endring\"),\n rader: (\n");
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
"Dagens situasjon".into(),
|
||||
tall(r.budsjett.netto_spesifikk(l.bra), 1),
|
||||
tall(r.levert.spesifikk_vektet, 1),
|
||||
r.karakter.bokstav.into(),
|
||||
"—".into()
|
||||
])
|
||||
));
|
||||
for (navn, sr) in scenarier {
|
||||
let endring = sr.levert.spesifikk_vektet - r.levert.spesifikk_vektet;
|
||||
let prosent = endring / r.levert.spesifikk_vektet * 100.0;
|
||||
s.push_str(&format!(
|
||||
" {}\n",
|
||||
rad(&[
|
||||
e(navn),
|
||||
tall(sr.budsjett.netto_spesifikk(sr.lost.bra), 1),
|
||||
tall(sr.levert.spesifikk_vektet, 1),
|
||||
sr.karakter.bokstav.into(),
|
||||
format!("{} %", tall(prosent, 1))
|
||||
])
|
||||
));
|
||||
}
|
||||
s.push_str(" ),\n)\n\n");
|
||||
for (navn, sr) in scenarier {
|
||||
if let Some(besk) = r
|
||||
.bygning
|
||||
.scenario
|
||||
.iter()
|
||||
.find(|x| &x.navn == navn)
|
||||
.and_then(|x| x.beskrivelse.as_ref())
|
||||
{
|
||||
s.push_str(&format!("*{}.* {}\n\n", e(navn), e(besk)));
|
||||
}
|
||||
let _ = sr;
|
||||
}
|
||||
}
|
||||
|
||||
s.push_str(&format!(
|
||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet med enok {} etter NS 3031:2025 (timesmetode, \
|
||||
NS-EN ISO 52016-1). Karakterskala fastsatt av Enova SF, gjeldende fra 1. januar 2026. \
|
||||
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
));
|
||||
s
|
||||
}
|
||||
|
||||
fn bunntekst(r: &Resultat) -> String {
|
||||
let b = &r.bygning.bygning;
|
||||
let mut deler = Vec::new();
|
||||
if let Some(a) = &b.adresse {
|
||||
deler.push(a.clone());
|
||||
}
|
||||
if let Some(g) = &b.gnr_bnr {
|
||||
deler.push(g.clone());
|
||||
}
|
||||
if let Some(e) = &b.energiraadgiver {
|
||||
deler.push(e.clone());
|
||||
}
|
||||
deler.join(" · ")
|
||||
}
|
||||
|
||||
fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||
if helning < 20.0 {
|
||||
return "horisontal".into();
|
||||
}
|
||||
if helning > 160.0 {
|
||||
return "mot grunn".into();
|
||||
}
|
||||
let retninger = [
|
||||
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||||
];
|
||||
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
||||
retninger[i].into()
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
//! Fra netto energibehov til vektet levert energi: beregningspunktene A–E i NS 3031:2025.
|
||||
//!
|
||||
//! * **A** netto energibehov, justert for nyttiggjort uregulerbart varmetilskudd
|
||||
//! * **B** brutto energibehov, etter tap i distribusjon og akkumulering
|
||||
//! * **C** tilført energi til bygningen, etter systemvirkningsgrad/SPF for hver varmekilde
|
||||
//! * **D** levert (kjøpt) energi per energibærer, med fradrag for egenprodusert og eksportert el
|
||||
//! * **E** vektet levert energi
|
||||
|
||||
pub mod solcelle;
|
||||
|
||||
use crate::data::{spf_oppslag, Datakilde, Vekting};
|
||||
use crate::klima::korreksjon::Korrigering;
|
||||
use crate::modell::lost::Lost;
|
||||
use crate::modell::*;
|
||||
use crate::motor::Timeserie;
|
||||
use crate::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Energibærere i energiattesten.
|
||||
#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Energibaerer {
|
||||
Elektrisitet,
|
||||
Olje,
|
||||
Gass,
|
||||
Fjernvarme,
|
||||
Fjernkjoling,
|
||||
BiobrenselLukket,
|
||||
BiobrenselAapent,
|
||||
Annet,
|
||||
}
|
||||
|
||||
impl Energibaerer {
|
||||
pub fn navn(&self) -> &'static str {
|
||||
match self {
|
||||
Energibaerer::Elektrisitet => "Elektrisitet",
|
||||
Energibaerer::Olje => "Olje",
|
||||
Energibaerer::Gass => "Gass",
|
||||
Energibaerer::Fjernvarme => "Fjernvarme",
|
||||
Energibaerer::Fjernkjoling => "Fjernkjøling",
|
||||
Energibaerer::BiobrenselLukket => "Biobrensel (lukket ildsted)",
|
||||
Energibaerer::BiobrenselAapent => "Biobrensel (åpent ildsted)",
|
||||
Energibaerer::Annet => "Andre energibærere",
|
||||
}
|
||||
}
|
||||
pub fn vekt(&self, v: &Vekting) -> f64 {
|
||||
match self {
|
||||
Energibaerer::Elektrisitet => v.faktor.elektrisitet,
|
||||
Energibaerer::Olje => v.faktor.olje,
|
||||
Energibaerer::Gass => v.faktor.gass,
|
||||
Energibaerer::Fjernvarme => v.faktor.fjernvarme,
|
||||
Energibaerer::Fjernkjoling => v.faktor.fjernkjoling,
|
||||
Energibaerer::BiobrenselLukket => v.faktor.biobrensel_lukket,
|
||||
Energibaerer::BiobrenselAapent => v.faktor.biobrensel_aapen,
|
||||
Energibaerer::Annet => v.faktor.annet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Én energipost i budsjettet, kWh/år for hele bygningen.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Energipost {
|
||||
pub nr: &'static str,
|
||||
pub navn: &'static str,
|
||||
/// Netto energibehov (beregningspunkt A).
|
||||
pub netto: f64,
|
||||
/// Distribusjons- og akkumuleringstap.
|
||||
pub tap: f64,
|
||||
/// Brutto energibehov (beregningspunkt B).
|
||||
pub brutto: f64,
|
||||
/// Vektet systemvirkningsgrad / SPF for posten.
|
||||
pub spf: f64,
|
||||
/// Tilført energi (beregningspunkt C).
|
||||
pub tilfort: f64,
|
||||
/// Er posten klimaavhengig (1a, 1b, 3a, 3b)?
|
||||
pub klimaavhengig: bool,
|
||||
/// Fordeling av tilført energi på energibærere.
|
||||
pub baerere: Vec<(Energibaerer, f64)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Energibudsjett {
|
||||
pub poster: Vec<Energipost>,
|
||||
pub netto_totalt: f64,
|
||||
pub brutto_totalt: f64,
|
||||
pub tilfort_totalt: f64,
|
||||
}
|
||||
|
||||
impl Energibudsjett {
|
||||
pub fn netto_spesifikk(&self, bra: f64) -> f64 {
|
||||
self.netto_totalt / bra
|
||||
}
|
||||
}
|
||||
|
||||
/// Levert og eksportert energi (beregningspunkt D) og vektet levert energi (E).
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Levert {
|
||||
pub per_baerer: Vec<(Energibaerer, f64)>,
|
||||
pub levert_totalt: f64,
|
||||
/// Egenprodusert elektrisitet brukt i bygningen, kWh.
|
||||
pub egenbruk: f64,
|
||||
/// Egenprodusert elektrisitet eksportert, kWh.
|
||||
pub eksportert: f64,
|
||||
pub netto_levert: f64,
|
||||
pub vektet: Vec<(Energibaerer, f64, f64)>,
|
||||
pub vektet_totalt: f64,
|
||||
/// Vektet levert energi per m² oppvarmet BRA, etter eventuell klimakorrigering.
|
||||
pub spesifikk_vektet: f64,
|
||||
pub klimakorrigering: Option<Korrigering>,
|
||||
}
|
||||
|
||||
/// Maksimalt samtidig effektbehov (NS 3031 Tabell 8).
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Effektbehov {
|
||||
pub termisk_w: f64,
|
||||
pub termisk_time: usize,
|
||||
pub elspesifikk_w: f64,
|
||||
pub elspesifikk_time: usize,
|
||||
pub samlet_w: f64,
|
||||
pub samlet_time: usize,
|
||||
}
|
||||
|
||||
fn beskriv_time(t: usize) -> String {
|
||||
let dag = t / 24;
|
||||
let time = t % 24;
|
||||
let mut d = dag as i64;
|
||||
const DAGER: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut m = 0;
|
||||
while m < 12 && d >= DAGER[m] {
|
||||
d -= DAGER[m];
|
||||
m += 1;
|
||||
}
|
||||
format!(
|
||||
"{}. {} kl. {:02}:00",
|
||||
d + 1,
|
||||
crate::MAANEDER[m.min(11)],
|
||||
time
|
||||
)
|
||||
}
|
||||
|
||||
impl Effektbehov {
|
||||
pub fn beskriv_termisk(&self) -> String {
|
||||
beskriv_time(self.termisk_time)
|
||||
}
|
||||
pub fn beskriv_samlet(&self) -> String {
|
||||
beskriv_time(self.samlet_time)
|
||||
}
|
||||
}
|
||||
|
||||
/// Andelen av romoppvarming/ventilasjonsvarme/tappevann hver varmekilde dekker,
|
||||
/// etter at vedovnens tak på 10 % og normalisering er anvendt.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Kildeandel {
|
||||
pub typ: VarmekildeType,
|
||||
pub beskrivelse: String,
|
||||
pub andel_rom: f64,
|
||||
pub andel_ventilasjon: f64,
|
||||
pub andel_tappevann: f64,
|
||||
pub spf: f64,
|
||||
pub baerer: Energibaerer,
|
||||
pub spf_kilde: String,
|
||||
}
|
||||
|
||||
pub struct Systemoppsett {
|
||||
pub kilder: Vec<Kildeandel>,
|
||||
pub tappevann_tap_andel: f64,
|
||||
pub tappevann_tap_i_sone: f64,
|
||||
pub distribusjon_tap_ut: f64,
|
||||
pub kjoling_spf: f64,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bestemmer varmekildenes andeler og virkningsgrader.
|
||||
pub fn sett_opp_systemer(b: &Bygning, l: &Lost, kilde: &Datakilde) -> Result<Systemoppsett> {
|
||||
let tabell = kilde.varmepumper()?;
|
||||
let mut merknader = Vec::new();
|
||||
let mut kilder: Vec<Kildeandel> = Vec::new();
|
||||
|
||||
for k in &b.oppvarming.kilde {
|
||||
let mut andel_rom = k.andel_rom.clamp(0.0, 1.0);
|
||||
if k.typ == VarmekildeType::Vedovn {
|
||||
let tak = kilde.vekting()?.regler.vedovn_maks_andel_rom;
|
||||
if andel_rom > tak {
|
||||
merknader.push(format!(
|
||||
"Vedovnen er satt til å dekke {:.0} % av romoppvarmingen, men energimerkeforskriften \
|
||||
regner med inntil {:.0} %. Resten dekkes av de øvrige varmekildene.",
|
||||
andel_rom * 100.0,
|
||||
tak * 100.0
|
||||
));
|
||||
andel_rom = tak;
|
||||
}
|
||||
}
|
||||
let andel_vent =
|
||||
k.andel_ventilasjon
|
||||
.unwrap_or(if k.typ.vannbaaren() { andel_rom } else { 0.0 });
|
||||
let turtemp = k.turtemperatur.or(b.distribusjon.turtemperatur);
|
||||
let (spf, spf_kilde) = match k.spf {
|
||||
Some(s) => (s, "oppgitt i inndata".to_string()),
|
||||
None if k.typ.er_varmepumpe() => {
|
||||
match spf_oppslag(&tabell, k.typ.nokkel(), turtemp, l.kategori) {
|
||||
Some(s) => (s, "NS 3031:2014 tabell B.9-3 (veiledende)".to_string()),
|
||||
None => (2.5, "antatt".to_string()),
|
||||
}
|
||||
}
|
||||
None => (
|
||||
standard_virkningsgrad(k.typ),
|
||||
"veiledende systemvirkningsgrad".to_string(),
|
||||
),
|
||||
};
|
||||
kilder.push(Kildeandel {
|
||||
typ: k.typ,
|
||||
beskrivelse: k.typ.beskrivelse().to_string(),
|
||||
andel_rom,
|
||||
andel_ventilasjon: andel_vent,
|
||||
andel_tappevann: k.andel_tappevann.clamp(0.0, 1.0),
|
||||
spf,
|
||||
baerer: baerer_for(k.typ, k.lukket),
|
||||
spf_kilde,
|
||||
});
|
||||
}
|
||||
|
||||
// Resten dekkes av direkte elektrisk oppvarming.
|
||||
let rest = |f: fn(&Kildeandel) -> f64, kilder: &[Kildeandel]| -> f64 {
|
||||
1.0 - kilder.iter().map(f).sum::<f64>()
|
||||
};
|
||||
let rest_rom = rest(|k| k.andel_rom, &kilder).max(0.0);
|
||||
let rest_vent = rest(|k| k.andel_ventilasjon, &kilder).max(0.0);
|
||||
let rest_vv = rest(|k| k.andel_tappevann, &kilder).max(0.0);
|
||||
if rest_rom > 1e-6 || rest_vent > 1e-6 || rest_vv > 1e-6 {
|
||||
if !b.oppvarming.kilde.is_empty() {
|
||||
merknader.push(format!(
|
||||
"Restbehovet dekkes av direkte elektrisk oppvarming: {:.0} % av romoppvarming, \
|
||||
{:.0} % av ventilasjonsvarme og {:.0} % av tappevann.",
|
||||
rest_rom * 100.0,
|
||||
rest_vent * 100.0,
|
||||
rest_vv * 100.0
|
||||
));
|
||||
}
|
||||
kilder.push(Kildeandel {
|
||||
typ: VarmekildeType::DirekteEl,
|
||||
beskrivelse: VarmekildeType::DirekteEl.beskrivelse().to_string(),
|
||||
andel_rom: rest_rom,
|
||||
andel_ventilasjon: rest_vent,
|
||||
andel_tappevann: rest_vv,
|
||||
spf: 1.0,
|
||||
baerer: Energibaerer::Elektrisitet,
|
||||
spf_kilde: "elektrisk oppvarming, virkningsgrad 1,0".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let sirkulasjon = b.tappevann.system == TappevannSystem::Sirkulasjon;
|
||||
let tappevann_tap_andel =
|
||||
b.tappevann
|
||||
.tap_andel
|
||||
.unwrap_or(if sirkulasjon { 0.45 } else { 0.25 });
|
||||
let tappevann_tap_i_sone =
|
||||
b.tappevann
|
||||
.tap_i_oppvarmet_sone
|
||||
.unwrap_or(if sirkulasjon { 0.5 } else { 0.8 });
|
||||
|
||||
let tap_andel = b
|
||||
.distribusjon
|
||||
.tap_andel
|
||||
.unwrap_or(match b.distribusjon.typ {
|
||||
DistribusjonType::Punkt => 0.0,
|
||||
DistribusjonType::Radiator => 0.03,
|
||||
DistribusjonType::Gulvvarme => 0.05,
|
||||
});
|
||||
let ut = b.distribusjon.roer_uoppvarmet.unwrap_or(0.2);
|
||||
let distribusjon_tap_ut = tap_andel * ut;
|
||||
|
||||
Ok(Systemoppsett {
|
||||
kilder,
|
||||
tappevann_tap_andel,
|
||||
tappevann_tap_i_sone,
|
||||
distribusjon_tap_ut,
|
||||
kjoling_spf: b.kjoling.spf.unwrap_or(3.0),
|
||||
merknader,
|
||||
})
|
||||
}
|
||||
|
||||
fn standard_virkningsgrad(t: VarmekildeType) -> f64 {
|
||||
match t {
|
||||
VarmekildeType::DirekteEl | VarmekildeType::ElKjel => 1.0,
|
||||
VarmekildeType::Vedovn | VarmekildeType::Pellets => 0.77,
|
||||
VarmekildeType::Olje => 0.80,
|
||||
VarmekildeType::Gass => 0.85,
|
||||
VarmekildeType::Fjernvarme => 0.90,
|
||||
VarmekildeType::Solfanger => 1e9,
|
||||
_ => 2.5,
|
||||
}
|
||||
}
|
||||
|
||||
fn baerer_for(t: VarmekildeType, lukket: bool) -> Energibaerer {
|
||||
match t {
|
||||
VarmekildeType::Vedovn | VarmekildeType::Pellets => {
|
||||
if lukket {
|
||||
Energibaerer::BiobrenselLukket
|
||||
} else {
|
||||
Energibaerer::BiobrenselAapent
|
||||
}
|
||||
}
|
||||
VarmekildeType::Olje => Energibaerer::Olje,
|
||||
VarmekildeType::Gass => Energibaerer::Gass,
|
||||
VarmekildeType::Fjernvarme => Energibaerer::Fjernvarme,
|
||||
VarmekildeType::Solfanger => Energibaerer::Annet,
|
||||
_ => Energibaerer::Elektrisitet,
|
||||
}
|
||||
}
|
||||
|
||||
/// Uregulerbart varmetilskudd fra tappevannsanlegget, W.
|
||||
pub fn tappevannstilskudd(l: &Lost, s: &Systemoppsett) -> f64 {
|
||||
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra;
|
||||
netto_w * s.tappevann_tap_andel * s.tappevann_tap_i_sone
|
||||
}
|
||||
|
||||
/// Bygger energibudsjettet (punkt A–C) av timeseriene.
|
||||
pub fn energibudsjett(ts: &Timeserie, s: &Systemoppsett) -> Energibudsjett {
|
||||
let kwh = Timeserie::sum_kwh;
|
||||
let mut poster = Vec::new();
|
||||
|
||||
let andeler_rom: Vec<(Energibaerer, f64, f64)> = s
|
||||
.kilder
|
||||
.iter()
|
||||
.map(|k| (k.baerer, k.andel_rom, k.spf))
|
||||
.collect();
|
||||
let andeler_vent: Vec<(Energibaerer, f64, f64)> = s
|
||||
.kilder
|
||||
.iter()
|
||||
.map(|k| (k.baerer, k.andel_ventilasjon, k.spf))
|
||||
.collect();
|
||||
let andeler_vv: Vec<(Energibaerer, f64, f64)> = s
|
||||
.kilder
|
||||
.iter()
|
||||
.map(|k| (k.baerer, k.andel_tappevann, k.spf))
|
||||
.collect();
|
||||
let el = vec![(Energibaerer::Elektrisitet, 1.0, 1.0)];
|
||||
let kjol = vec![(Energibaerer::Elektrisitet, 1.0, s.kjoling_spf)];
|
||||
|
||||
let tap_rom =
|
||||
|netto: f64| netto * s.distribusjon_tap_ut / (1.0 - s.distribusjon_tap_ut).max(1e-6);
|
||||
|
||||
let romvarme = kwh(&ts.romoppvarming);
|
||||
poster.push(post(
|
||||
"1a",
|
||||
"Romoppvarming",
|
||||
romvarme,
|
||||
tap_rom(romvarme),
|
||||
&andeler_rom,
|
||||
true,
|
||||
));
|
||||
let ventvarme = kwh(&ts.ventilasjonsvarme);
|
||||
poster.push(post(
|
||||
"1b",
|
||||
"Ventilasjonsvarme",
|
||||
ventvarme,
|
||||
tap_rom(ventvarme),
|
||||
&andeler_vent,
|
||||
true,
|
||||
));
|
||||
let vv = kwh(&ts.varmtvann);
|
||||
poster.push(post(
|
||||
"2",
|
||||
"Varmtvann",
|
||||
vv,
|
||||
vv * s.tappevann_tap_andel,
|
||||
&andeler_vv,
|
||||
false,
|
||||
));
|
||||
let romkjol = kwh(&ts.romkjoling);
|
||||
poster.push(post("3a", "Romkjøling", romkjol, 0.0, &kjol, true));
|
||||
let ventkjol = kwh(&ts.ventilasjonskjoling);
|
||||
poster.push(post(
|
||||
"3b",
|
||||
"Ventilasjonskjøling",
|
||||
ventkjol,
|
||||
0.0,
|
||||
&kjol,
|
||||
true,
|
||||
));
|
||||
poster.push(post("4a", "Vifter", kwh(&ts.vifter), 0.0, &el, false));
|
||||
poster.push(post("4b", "Pumper", kwh(&ts.pumper), 0.0, &el, false));
|
||||
poster.push(post("5", "Belysning", kwh(&ts.belysning), 0.0, &el, false));
|
||||
poster.push(post(
|
||||
"6",
|
||||
"Teknisk utstyr",
|
||||
kwh(&ts.utstyr),
|
||||
0.0,
|
||||
&el,
|
||||
false,
|
||||
));
|
||||
|
||||
let netto_totalt = poster.iter().map(|p| p.netto).sum();
|
||||
let brutto_totalt = poster.iter().map(|p| p.brutto).sum();
|
||||
let tilfort_totalt = poster.iter().map(|p| p.tilfort).sum();
|
||||
Energibudsjett {
|
||||
poster,
|
||||
netto_totalt,
|
||||
brutto_totalt,
|
||||
tilfort_totalt,
|
||||
}
|
||||
}
|
||||
|
||||
fn post(
|
||||
nr: &'static str,
|
||||
navn: &'static str,
|
||||
netto: f64,
|
||||
tap: f64,
|
||||
andeler: &[(Energibaerer, f64, f64)],
|
||||
klimaavhengig: bool,
|
||||
) -> Energipost {
|
||||
let brutto = netto + tap;
|
||||
let mut baerere: Vec<(Energibaerer, f64)> = Vec::new();
|
||||
let mut tilfort = 0.0;
|
||||
for (b, andel, spf) in andeler {
|
||||
if *andel <= 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let e = brutto * andel / spf.max(1e-9);
|
||||
tilfort += e;
|
||||
match baerere.iter_mut().find(|(bb, _)| bb == b) {
|
||||
Some((_, v)) => *v += e,
|
||||
None => baerere.push((*b, e)),
|
||||
}
|
||||
}
|
||||
let spf = if tilfort > 1e-9 {
|
||||
brutto / tilfort
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
Energipost {
|
||||
nr,
|
||||
navn,
|
||||
netto,
|
||||
tap,
|
||||
brutto,
|
||||
spf,
|
||||
tilfort,
|
||||
klimaavhengig,
|
||||
baerere,
|
||||
}
|
||||
}
|
||||
|
||||
/// Beregner levert og vektet energi (punkt D og E), med klimakorrigering og solceller.
|
||||
pub fn levert(
|
||||
budsjett: &Energibudsjett,
|
||||
vekting: &Vekting,
|
||||
bra: f64,
|
||||
korrigering: Option<Korrigering>,
|
||||
egenbruk: f64,
|
||||
eksportert: f64,
|
||||
) -> Levert {
|
||||
let kf = korrigering.as_ref().map(|k| k.faktor).unwrap_or(1.0);
|
||||
let mut per: Vec<(Energibaerer, f64)> = Vec::new();
|
||||
for p in &budsjett.poster {
|
||||
let skala = if p.klimaavhengig { 1.0 / kf } else { 1.0 };
|
||||
for (b, v) in &p.baerere {
|
||||
let v = v * skala;
|
||||
match per.iter_mut().find(|(bb, _)| bb == b) {
|
||||
Some((_, s)) => *s += v,
|
||||
None => per.push((*b, v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Egenprodusert elektrisitet trekkes fra levert elektrisitet.
|
||||
if let Some((_, v)) = per
|
||||
.iter_mut()
|
||||
.find(|(b, _)| *b == Energibaerer::Elektrisitet)
|
||||
{
|
||||
*v = (*v - egenbruk).max(0.0);
|
||||
}
|
||||
per.sort_by_key(|(b, _)| *b);
|
||||
let levert_totalt: f64 = per.iter().map(|(_, v)| v).sum();
|
||||
let netto_levert = levert_totalt - eksportert;
|
||||
let vektet: Vec<(Energibaerer, f64, f64)> = per
|
||||
.iter()
|
||||
.map(|(b, v)| (*b, *v, v * b.vekt(vekting)))
|
||||
.collect();
|
||||
let vektet_totalt: f64 =
|
||||
vektet.iter().map(|(_, _, v)| v).sum::<f64>() + -(eksportert * vekting.faktor.eksportert);
|
||||
Levert {
|
||||
per_baerer: per,
|
||||
levert_totalt,
|
||||
egenbruk,
|
||||
eksportert,
|
||||
netto_levert,
|
||||
vektet,
|
||||
vektet_totalt,
|
||||
spesifikk_vektet: vektet_totalt / bra,
|
||||
klimakorrigering: korrigering,
|
||||
}
|
||||
}
|
||||
|
||||
/// Klimaavhengig tilført energi per m², som grunnlag for klimakorrigering.
|
||||
pub fn klimaavhengig_spesifikk(budsjett: &Energibudsjett, bra: f64) -> f64 {
|
||||
budsjett
|
||||
.poster
|
||||
.iter()
|
||||
.filter(|p| p.klimaavhengig)
|
||||
.map(|p| p.tilfort)
|
||||
.sum::<f64>()
|
||||
/ bra
|
||||
}
|
||||
|
||||
/// Maksimalt samtidig effektbehov.
|
||||
pub fn effektbehov(ts: &Timeserie, s: &Systemoppsett) -> Effektbehov {
|
||||
let mut termisk = (0.0f64, 0usize);
|
||||
let mut elspes = (0.0f64, 0usize);
|
||||
let mut samlet = (0.0f64, 0usize);
|
||||
let tap = 1.0 / (1.0 - s.distribusjon_tap_ut).max(1e-6);
|
||||
for t in 0..crate::TIMER {
|
||||
let term = (ts.romoppvarming[t] + ts.ventilasjonsvarme[t]) * tap
|
||||
+ ts.varmtvann[t] * (1.0 + s.tappevann_tap_andel)
|
||||
+ ts.romkjoling[t]
|
||||
+ ts.ventilasjonskjoling[t];
|
||||
let el = ts.vifter[t] + ts.pumper[t] + ts.belysning[t] + ts.utstyr[t];
|
||||
if term > termisk.0 {
|
||||
termisk = (term, t);
|
||||
}
|
||||
if el > elspes.0 {
|
||||
elspes = (el, t);
|
||||
}
|
||||
if term + el > samlet.0 {
|
||||
samlet = (term + el, t);
|
||||
}
|
||||
}
|
||||
Effektbehov {
|
||||
termisk_w: termisk.0,
|
||||
termisk_time: termisk.1,
|
||||
elspesifikk_w: elspes.0,
|
||||
elspesifikk_time: elspes.1,
|
||||
samlet_w: samlet.0,
|
||||
samlet_time: samlet.1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Lokal elproduksjon fra solceller, med timesvis egenbruk og eksport.
|
||||
|
||||
use crate::klima::{sol, Vaerdata};
|
||||
use crate::modell::Solcelle;
|
||||
use crate::motor::Timeserie;
|
||||
use crate::systemer::Systemoppsett;
|
||||
use crate::TIMER;
|
||||
|
||||
pub struct Produksjon {
|
||||
/// Timesserie for produksjon, W.
|
||||
pub serie: Vec<f64>,
|
||||
pub produsert_kwh: f64,
|
||||
pub egenbruk_kwh: f64,
|
||||
pub eksportert_kwh: f64,
|
||||
}
|
||||
|
||||
/// Beregner produksjon og hvor mye som dekkes internt time for time.
|
||||
///
|
||||
/// Egenbruket måles mot byggets samlede elektriske effektbehov i timen: el-spesifikke poster
|
||||
/// pluss elektrisitet til varme og kjøling etter systemvirkningsgrad.
|
||||
pub fn beregn(
|
||||
s: &Solcelle,
|
||||
v: &Vaerdata,
|
||||
ts: &Timeserie,
|
||||
sys: &Systemoppsett,
|
||||
albedo: f64,
|
||||
) -> Produksjon {
|
||||
let innstraaling = sol::innstraaling_aar(v, s.helning, s.orientering, albedo);
|
||||
let mut serie = vec![0.0; TIMER];
|
||||
let mut produsert = 0.0;
|
||||
let mut egenbruk = 0.0;
|
||||
let mut eksport = 0.0;
|
||||
|
||||
let el_andel_varme: f64 = sys
|
||||
.kilder
|
||||
.iter()
|
||||
.filter(|k| k.baerer == crate::systemer::Energibaerer::Elektrisitet)
|
||||
.map(|k| k.andel_rom / k.spf.max(1e-9))
|
||||
.sum();
|
||||
let el_andel_vv: f64 = sys
|
||||
.kilder
|
||||
.iter()
|
||||
.filter(|k| k.baerer == crate::systemer::Energibaerer::Elektrisitet)
|
||||
.map(|k| k.andel_tappevann / k.spf.max(1e-9))
|
||||
.sum();
|
||||
|
||||
for t in 0..TIMER {
|
||||
let p = s.areal * s.virkningsgrad * s.ytelsesfaktor * innstraaling[t];
|
||||
serie[t] = p;
|
||||
produsert += p;
|
||||
let forbruk = ts.belysning[t]
|
||||
+ ts.utstyr[t]
|
||||
+ ts.vifter[t]
|
||||
+ ts.pumper[t]
|
||||
+ ts.romoppvarming[t] * el_andel_varme
|
||||
+ ts.varmtvann[t] * el_andel_vv
|
||||
+ (ts.romkjoling[t] + ts.ventilasjonskjoling[t]) / sys.kjoling_spf.max(1e-9);
|
||||
let brukt = p.min(forbruk);
|
||||
egenbruk += brukt;
|
||||
eksport += p - brukt;
|
||||
}
|
||||
Produksjon {
|
||||
serie,
|
||||
produsert_kwh: produsert / 1000.0,
|
||||
egenbruk_kwh: egenbruk / 1000.0,
|
||||
eksportert_kwh: eksport / 1000.0,
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
//! Kontroll av beregningsmotoren mot analytiske sammenhenger og mot publiserte eksempler.
|
||||
|
||||
use enok::data::Datakilde;
|
||||
use enok::klima::Vaerdata;
|
||||
use enok::modell::lost::{Lost, Regulering};
|
||||
use enok::modell::Bygning;
|
||||
use enok::motor::{iso52016::Systemtilskudd, simuler, Timeserie, Varmetapsbudsjett};
|
||||
use enok::TIMER;
|
||||
|
||||
/// Bygger et kunstig værdatasett med konstant utetemperatur og uten sol.
|
||||
fn stille_vaer(t_ute: f64) -> Vaerdata {
|
||||
Vaerdata {
|
||||
sted: "Prøve".into(),
|
||||
land: "NOR".into(),
|
||||
bredde: 60.0,
|
||||
lengde: 10.0,
|
||||
tidssone: 1.0,
|
||||
hoyde: 0.0,
|
||||
t_ute: vec![t_ute; TIMER],
|
||||
rf: vec![50.0; TIMER],
|
||||
ghi: vec![0.0; TIMER],
|
||||
dni: vec![0.0; TIMER],
|
||||
dhi: vec![0.0; TIMER],
|
||||
// Himmeltemperatur lik utetemperatur, slik at langbølget utstråling ikke spiller inn.
|
||||
ir_himmel: vec![5.67e-8 * (t_ute + 273.15f64).powi(4); TIMER],
|
||||
vind: vec![0.0; TIMER],
|
||||
}
|
||||
}
|
||||
|
||||
const ENKEL: &str = r#"
|
||||
[bygning]
|
||||
navn = "Kassen"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 2020
|
||||
oppvarmet_bra = 100.0
|
||||
oppvarmet_volum = 240.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Vegger"
|
||||
type = "vegg"
|
||||
areal = 120.0
|
||||
u_verdi = 0.20
|
||||
masseklasse = "middels"
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 100.0
|
||||
u_verdi = 0.15
|
||||
masseklasse = "middels"
|
||||
|
||||
[[flate]]
|
||||
navn = "Gulv"
|
||||
type = "gulv"
|
||||
areal = 100.0
|
||||
u_verdi = 0.15
|
||||
masseklasse = "tung"
|
||||
|
||||
[kuldebroer]
|
||||
normalisert = 0.0
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 0.0
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig"
|
||||
luftmengde = 0.0
|
||||
luftmengde_natt = 0.0
|
||||
|
||||
[drift]
|
||||
nattsenking = 21.0
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "direkte_el"
|
||||
andel_rom = 1.0
|
||||
andel_ventilasjon = 1.0
|
||||
andel_tappevann = 1.0
|
||||
"#;
|
||||
|
||||
/// Uten varmetilskudd, sol, ventilasjon og infiltrasjon skal effektbehovet i stasjonær
|
||||
/// tilstand være varmetapskoeffisienten ganger temperaturdifferansen.
|
||||
///
|
||||
/// Overgangsmotstanden R_si = 0,13 m²K/W som U-verdien bygger på, gjelder mot operativ
|
||||
/// temperatur. Nodemodellen skiller luft- og strålingstemperatur, så sammenligningen gjøres
|
||||
/// med regulering på operativ temperatur.
|
||||
#[test]
|
||||
fn stasjonaert_varmetap_stemmer_med_varmetapstallet() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let mut l = Lost::fra(&b, &kilde).unwrap();
|
||||
// Slå av alle interne laster og bruk lufttemperatur som reguleringsstørrelse.
|
||||
l.laster.belysning = [0.0; 24];
|
||||
l.laster.utstyr_effekt = [0.0; 24];
|
||||
l.laster.utstyr_varme = [0.0; 24];
|
||||
l.laster.personer = [0.0; 24];
|
||||
l.laster.varmtvann = [0.0; 24];
|
||||
l.laster.pumper = [0.0; 24];
|
||||
l.regulering = Regulering::Operativ;
|
||||
|
||||
let t_ute = 1.0;
|
||||
let v = stille_vaer(t_ute);
|
||||
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
|
||||
let budsjett = Varmetapsbudsjett::beregn(&l);
|
||||
// Gulvet står mot det fri i denne prøven, så hele transmisjonstapet er mot utetemperaturen.
|
||||
let h = budsjett.transmisjon_w_per_k;
|
||||
let forventet = h * (l.settpunkt_oppvarming - t_ute);
|
||||
|
||||
// Se bort fra innkjøringen: bruk siste halvdel av året.
|
||||
let siste: Vec<f64> = ts.romoppvarming[TIMER / 2..].to_vec();
|
||||
let snitt = siste.iter().sum::<f64>() / siste.len() as f64;
|
||||
let avvik = (snitt - forventet).abs() / forventet;
|
||||
assert!(
|
||||
avvik < 0.02,
|
||||
"stasjonært varmetap avviker {:.2} %: beregnet {snitt:.1} W, forventet {forventet:.1} W",
|
||||
avvik * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Energibalansen skal gå opp: tilført varme og varmetilskudd skal være lik varmetapet.
|
||||
#[test]
|
||||
fn aarlig_energibalanse_gaar_opp() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let l = Lost::fra(&b, &kilde).unwrap();
|
||||
let v = stille_vaer(1.0);
|
||||
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
let budsjett = Varmetapsbudsjett::beregn(&l);
|
||||
|
||||
let tilfort: f64 = ts.romoppvarming.iter().sum::<f64>() + ts.internlast.iter().sum::<f64>();
|
||||
// Varmetapet regnet av de simulerte operative temperaturene, som U-verdiene gjelder mot.
|
||||
let tap: f64 = ts
|
||||
.t_operativ
|
||||
.iter()
|
||||
.map(|t| budsjett.transmisjon_w_per_k * (t - 1.0))
|
||||
.sum();
|
||||
let avvik = (tilfort - tap).abs() / tap;
|
||||
assert!(
|
||||
avvik < 0.02,
|
||||
"energibalansen avviker {:.2} %",
|
||||
avvik * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Dobbelt så stort temperaturfall skal gi dobbelt så stort varmebehov i en lineær modell.
|
||||
#[test]
|
||||
fn varmebehovet_er_lineaert_i_temperaturdifferansen() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let mut l = Lost::fra(&b, &kilde).unwrap();
|
||||
l.laster.belysning = [0.0; 24];
|
||||
l.laster.utstyr_effekt = [0.0; 24];
|
||||
l.laster.utstyr_varme = [0.0; 24];
|
||||
l.laster.personer = [0.0; 24];
|
||||
l.laster.varmtvann = [0.0; 24];
|
||||
l.laster.pumper = [0.0; 24];
|
||||
l.regulering = Regulering::Operativ;
|
||||
|
||||
let en = Timeserie::sum_kwh(
|
||||
&simuler(&l, &stille_vaer(11.0), Systemtilskudd::default())
|
||||
.unwrap()
|
||||
.romoppvarming,
|
||||
);
|
||||
let to = Timeserie::sum_kwh(
|
||||
&simuler(&l, &stille_vaer(1.0), Systemtilskudd::default())
|
||||
.unwrap()
|
||||
.romoppvarming,
|
||||
);
|
||||
let forhold = to / en;
|
||||
assert!(
|
||||
(forhold - 2.0).abs() < 0.01,
|
||||
"forholdet skulle vært 2,0, men ble {forhold:.3}"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! Kontroll mot Standard Norges lanseringseksempel for NS 3031:2025 (Multiconsult, mai 2025):
|
||||
//! småhus med TEK17-standard i Oslo-klima.
|
||||
//!
|
||||
//! Eksempelet er beregnet med Simien 8. Varmetapsbudsjettet og de normerte energipostene
|
||||
//! gjengis her nøyaktig. Romoppvarmingen avviker, se kommentaren nederst i filen.
|
||||
|
||||
use enok::data::Datakilde;
|
||||
use enok::inndata;
|
||||
use enok::kjor;
|
||||
use std::path::Path;
|
||||
|
||||
fn kjor_referansebygg() -> kjor::Resultat {
|
||||
let b = inndata::les_bygning(Path::new("eksempler/tek17-smaahus.toml")).unwrap();
|
||||
kjor::kjor(&b, &Datakilde::default()).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, toleranse: f64, hva: &str) {
|
||||
assert!(
|
||||
(faktisk - forventet).abs() <= toleranse,
|
||||
"{hva}: beregnet {faktisk:.2}, forventet {forventet:.2} (toleranse {toleranse})"
|
||||
);
|
||||
}
|
||||
|
||||
/// Varmetapsbudsjettet skal gjengi NS 3031:2025 Tabell 3 for TEK17-referansebygget.
|
||||
#[test]
|
||||
fn varmetapsbudsjett_som_publisert() {
|
||||
let r = kjor_referansebygg();
|
||||
let v = &r.varmetap;
|
||||
naer(v.vegger, 0.22, 0.005, "vegger");
|
||||
naer(v.tak, 0.07, 0.005, "tak");
|
||||
naer(v.gulv, 0.05, 0.005, "gulv");
|
||||
naer(v.vinduer_dorer, 0.20, 0.005, "vinduer og dører");
|
||||
naer(v.kuldebroer, 0.05, 0.005, "kuldebroer");
|
||||
naer(v.transmisjon, 0.59, 0.005, "transmisjon");
|
||||
naer(v.infiltrasjon, 0.04, 0.005, "infiltrasjon");
|
||||
naer(v.klimaskjerm, 0.63, 0.005, "klimaskjerm");
|
||||
naer(v.ventilasjon, 0.06, 0.005, "ventilasjon");
|
||||
naer(v.totalt, 0.69, 0.005, "totalt varmetapstall");
|
||||
}
|
||||
|
||||
/// De normerte energipostene skal treffe verdiene i NS 3031:2025 Tillegg A for småhus.
|
||||
#[test]
|
||||
fn normerte_energiposter_som_publisert() {
|
||||
let r = kjor_referansebygg();
|
||||
naer(post(&r, "2"), 25.1, 0.2, "varmtvann");
|
||||
naer(post(&r, "5"), 11.4, 0.2, "belysning");
|
||||
naer(post(&r, "6"), 17.5, 0.2, "teknisk utstyr");
|
||||
naer(post(&r, "3a"), 0.0, 0.01, "romkjøling");
|
||||
naer(post(&r, "3b"), 0.0, 0.01, "ventilasjonskjøling");
|
||||
}
|
||||
|
||||
/// Energikarakteren for et TEK17-bygg skal være B, jf. energimerkeforskriften § 10b.
|
||||
#[test]
|
||||
fn tek17_bygg_faar_karakter_b_eller_bedre() {
|
||||
let r = kjor_referansebygg();
|
||||
assert!(
|
||||
matches!(r.karakter.bokstav, "A" | "B"),
|
||||
"TEK17-referansebygget fikk karakter {}, forventet A eller B",
|
||||
r.karakter.bokstav
|
||||
);
|
||||
}
|
||||
|
||||
/// Regresjonsvakt for romoppvarming og samlet netto energibehov.
|
||||
///
|
||||
/// Det publiserte eksempelet oppgir 46,2 kWh/(m²·år) romoppvarming og 106,4 kWh/(m²·år)
|
||||
/// netto energibehov. enok beregner vesentlig lavere romoppvarming. Kontrollene som er gjort:
|
||||
///
|
||||
/// * solinnstrålingen på skrå og orienterte flater er sammenlignet med pvlib (Hay–Davies) og
|
||||
/// avviker under 1 % for alle himmelretninger i Oslo-klima;
|
||||
/// * varmetapet i stasjonær tilstand stemmer med U·A·ΔT innenfor 2 % (se `tests/motor.rs`);
|
||||
/// * årlig energibalanse går opp innenfor 2 %;
|
||||
/// * varmetapsbudsjettet og de normerte energipostene er identiske med de publiserte.
|
||||
///
|
||||
/// Avviket ligger dermed i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025 Tillegg A
|
||||
/// er ikke offentlig tilgjengelig, så de normerte solfaktorene, skjermingsforutsetningene og
|
||||
/// timesprofilene som Simien 8 bruker, lar seg ikke etterprøve her. Testen låser dagens verdier
|
||||
/// slik at endringer i motoren blir synlige, og oppdateres når standardens forutsetninger er
|
||||
/// tilgjengelige.
|
||||
#[test]
|
||||
fn regresjonsvakt_for_romoppvarming() {
|
||||
let r = kjor_referansebygg();
|
||||
naer(
|
||||
post(&r, "1a"),
|
||||
24.7,
|
||||
1.5,
|
||||
"romoppvarming (regresjon, publisert verdi 46,2)",
|
||||
);
|
||||
naer(
|
||||
r.budsjett.netto_spesifikk(r.lost.bra),
|
||||
84.1,
|
||||
2.0,
|
||||
"netto energibehov (regresjon, publisert verdi 106,4)",
|
||||
);
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Stilark for enok-energirapporten. Kopieres til utdatakatalogen og kan tilpasses fritt.
|
||||
|
||||
#let aksent = rgb("#8c2f1f")
|
||||
#let aksent-lys = rgb("#b9633f")
|
||||
#let moerk = rgb("#1a1a1a")
|
||||
#let mid = rgb("#555555")
|
||||
#let lys = rgb("#f4f1ee")
|
||||
#let kantlinje = rgb("#ddd4cc")
|
||||
|
||||
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
|
||||
|
||||
#let oppsett(tittel: "", undertittel: "", bunntekst: "", doc) = {
|
||||
set page(
|
||||
paper: "a4",
|
||||
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
||||
header: context {
|
||||
if counter(page).get().first() > 1 {
|
||||
grid(
|
||||
columns: (1fr, auto),
|
||||
align: (left + horizon, right + horizon),
|
||||
text(font: font-heading, size: 8pt, fill: mid)[#tittel],
|
||||
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
|
||||
)
|
||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||
}
|
||||
},
|
||||
footer: context {
|
||||
if counter(page).get().first() > 1 and bunntekst != "" {
|
||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||
v(3pt)
|
||||
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
|
||||
}
|
||||
},
|
||||
)
|
||||
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
||||
set par(justify: true, leading: 0.62em)
|
||||
show heading: it => {
|
||||
set text(font: font-heading, fill: aksent)
|
||||
block(above: 1.2em, below: 0.6em, it)
|
||||
}
|
||||
show heading.where(level: 1): it => {
|
||||
set text(size: 15pt, weight: "bold")
|
||||
block(above: 1.6em, below: 0.7em)[
|
||||
#it.body
|
||||
#v(-0.45em)
|
||||
#line(length: 100%, stroke: 1pt + aksent)
|
||||
]
|
||||
}
|
||||
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
|
||||
set table(stroke: none, inset: (x: 6pt, y: 4pt))
|
||||
show table.cell.where(y: 0): set text(weight: "bold", size: 9pt)
|
||||
doc
|
||||
}
|
||||
|
||||
// Tabell med stripete rader og linje under overskriften.
|
||||
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = {
|
||||
let j = if justering == none { kolonner.map(_ => right) } else { justering }
|
||||
block(breakable: true, table(
|
||||
columns: kolonner,
|
||||
align: (x, y) => if y == 0 { center + bottom } else { j.at(x) + horizon },
|
||||
fill: (x, y) => if y == 0 { lys }
|
||||
else if sum-rader.contains(y - 1) { rgb("#efe9e4") }
|
||||
else if calc.rem(y, 2) == 0 { rgb("#fbfaf9") } else { white },
|
||||
stroke: (x, y) => (
|
||||
bottom: if y == 0 { 0.8pt + aksent } else { 0.3pt + kantlinje },
|
||||
),
|
||||
..overskrift.map(h => [#h]),
|
||||
..rader.flatten(),
|
||||
))
|
||||
}
|
||||
|
||||
// Stor karakterrute på forsiden.
|
||||
#let karakterrute(bokstav, verdi, farge) = {
|
||||
box(
|
||||
fill: farge,
|
||||
radius: 6pt,
|
||||
inset: (x: 16pt, y: 12pt),
|
||||
stack(
|
||||
spacing: 3pt,
|
||||
align(center, text(size: 46pt, weight: "bold", fill: white)[#bokstav]),
|
||||
align(center, text(size: 9pt, fill: white)[#verdi kWh/(m²·år)]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#let faktaboks(innhold) = block(
|
||||
fill: lys,
|
||||
radius: 4pt,
|
||||
inset: 9pt,
|
||||
width: 100%,
|
||||
stroke: 0.4pt + kantlinje,
|
||||
innhold,
|
||||
)
|
||||
|
||||
#let merknad(innhold) = block(
|
||||
inset: (left: 9pt),
|
||||
stroke: (left: 2pt + aksent-lys),
|
||||
text(size: 9pt)[#innhold],
|
||||
)
|
||||
Reference in New Issue
Block a user