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