Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19162d9a7
|
||
|
|
c89561b0f5
|
||
|
|
e3f37afe36
|
||
|
|
cabd8500ea
|
||
|
|
e7d77547ac
|
||
|
|
70b9f9c647
|
||
|
|
77c5d15042
|
||
|
|
32ecb0b132
|
||
|
|
6d87ef496f | ||
|
|
d02a3d9666 | ||
|
|
cb64c91b19
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.3.0"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx",
|
"approx",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.3.0"
|
version = "0.4.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ enok beregn bygning.toml
|
|||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
Nordhusstykket 5 (Småhus, 140 m², byggeår 1948)
|
Eldre enebolig med kjeller (Småhus, 140 m², byggeår 1948)
|
||||||
Klimadata: Indre Østfold
|
Klimadata: Indre Østfold
|
||||||
|
|
||||||
Varmetapstall 3,00 W/(m²K)
|
Varmetapstall 3,00 W/(m²K)
|
||||||
@@ -68,6 +68,17 @@ bygning med store vindusflater fått urealistisk høye innetemperaturer om våre
|
|||||||
lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte
|
lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte
|
||||||
settpunkttemperaturen for kjøling, og luftingen koster ingen energi.
|
settpunkttemperaturen for kjøling, og luftingen koster ingen energi.
|
||||||
|
|
||||||
|
**Ventilasjon.** Varmegjenvinneren reguleres ned når den ellers ville varmet tilluften over
|
||||||
|
settpunktet. Et anlegg med `kjolebatteri = true` holder tilluften på normert temperatur i
|
||||||
|
driftstiden, slik DiBKs modellbygg for energirammene er regnet, og energien føres som
|
||||||
|
ventilasjonskjøling.
|
||||||
|
|
||||||
|
**Bygningskategorier.** Normerte laster og driftstider er lagt inn for småhus, boligblokk,
|
||||||
|
barnehage, kontorbygg, skolebygg, idrettsbygg og kulturbygg. Driftstidene har timer per døgn,
|
||||||
|
dager per uke og uker per år; lastene gjelder bare på driftsdager, og ukene uten drift legges
|
||||||
|
i ferier. For sykehus, sykehjem, hotell, forretningsbygg, universitet og lett industri er det
|
||||||
|
ikke funnet offentlige kilder, og de krever en egen fil i `--data KATALOG/normerte/`.
|
||||||
|
|
||||||
**Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell.
|
**Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell.
|
||||||
|
|
||||||
## Installasjon
|
## Installasjon
|
||||||
@@ -84,12 +95,15 @@ Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen
|
|||||||
| Kommando | Hva den gjør |
|
| Kommando | Hva den gjør |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
|
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
|
||||||
|
| `enok beregn modell.ifc` | Beregner rett fra en modell, uten at `bygning.toml` finnes fra før |
|
||||||
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
||||||
|
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
|
||||||
|
| `enok tillegg modell.ifc bygning.toml` | Skriver forskjellen mellom modellen og en håndredigert fil som tilleggsfil |
|
||||||
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
||||||
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
||||||
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
||||||
|
|
||||||
Nyttige valg til `beregn`: `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
|
Nyttige valg til `beregn`: `--tillegg FIL`, `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
|
||||||
`--uten-scenarier`, `--ut KATALOG`.
|
`--uten-scenarier`, `--ut KATALOG`.
|
||||||
|
|
||||||
### Utdata
|
### Utdata
|
||||||
@@ -111,7 +125,7 @@ antatt og hvor det kommer fra.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[bygning]
|
[bygning]
|
||||||
navn = "Nordhusstykket 5"
|
navn = "Eldre enebolig med kjeller"
|
||||||
kategori = "smaahus"
|
kategori = "smaahus"
|
||||||
byggeaar = 1948
|
byggeaar = 1948
|
||||||
oppvarmet_bra = 140.0
|
oppvarmet_bra = 140.0
|
||||||
@@ -154,6 +168,7 @@ over_terreng = 0.6 # høyden som står over terreng
|
|||||||
|
|
||||||
[ventilasjon]
|
[ventilasjon]
|
||||||
type = "naturlig" # naturlig | avtrekk | balansert
|
type = "naturlig" # naturlig | avtrekk | balansert
|
||||||
|
# kjolebatteri = true # balansert anlegg med kjølebatteri
|
||||||
|
|
||||||
[[oppvarming.kilde]]
|
[[oppvarming.kilde]]
|
||||||
type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, …
|
type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, …
|
||||||
@@ -165,8 +180,19 @@ navn = "Nye vinduer"
|
|||||||
endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
|
endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Rapporten kan følge husets eller firmaets egne dokumenter:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rapport]
|
||||||
|
logo = "logo.svg" # vises øverst på forsiden
|
||||||
|
farge = "#880000" # aksentfarge i overskrifter og tabeller
|
||||||
|
avsender = "Firma · Adresse · Org.nr. 000 000 000"
|
||||||
|
skrift_overskrift = "Futura Now Headline"
|
||||||
|
```
|
||||||
|
|
||||||
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
|
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
|
||||||
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
|
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
|
||||||
|
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
|
||||||
|
|
||||||
Ferdige eksempler ligger i `eksempler/`.
|
Ferdige eksempler ligger i `eksempler/`.
|
||||||
|
|
||||||
@@ -178,6 +204,73 @@ Ferdige eksempler ligger i `eksempler/`.
|
|||||||
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
|
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
|
||||||
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
|
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
|
||||||
|
|
||||||
|
### Modellen og en tilleggsfil
|
||||||
|
|
||||||
|
Modellen er kilden til alt den kan beskrive, og en modell alene gir en ferdig rapport: enok
|
||||||
|
fyller hullene med normerte verdier og typiske verdier for byggeåret, og rapporten sier hva
|
||||||
|
som er antatt. Det modellen ikke kan beskrive, som varmeanlegg, lekkasjetall og tiltak, og
|
||||||
|
rettelser der den mangler noe, legges i en tilleggsfil som flettes inn etter de samme reglene
|
||||||
|
som scenarioene:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
enok beregn modell.ifc # bare modellen
|
||||||
|
enok beregn modell.ifc --tillegg prosjekt.toml # modellen og tilleggsfilen
|
||||||
|
enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml # samme, som fil
|
||||||
|
```
|
||||||
|
|
||||||
|
`beregn` skriver bygningsbeskrivelsen modellen ga, til `bygning.toml` i utdatakatalogen, slik
|
||||||
|
at beregningen kan etterprøves.
|
||||||
|
|
||||||
|
En ny import skriver aldri over manuelle endringer, for de ligger ikke i den genererte filen.
|
||||||
|
Har du likevel en `bygning.toml` du har redigert for hånd, lager `enok tillegg` en tilleggsfil
|
||||||
|
av forskjellen mot modellen, så arbeidet følger med videre:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
enok tillegg modell.ifc bygning.toml --ut prosjekt.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Tillegget inneholder alt filen sier som modellen ikke sier: endrede U-verdier, drift,
|
||||||
|
varmeanlegg og scenarioer, og `slett = true` for oppføringer modellen har og filen ikke.
|
||||||
|
Kommandoen kontrollerer at tillegget gjenskaper filen det er laget av, og sier fra om noe
|
||||||
|
ikke lar seg uttrykke. `enok import` nekter dessuten å skrive over en fil enok ikke har laget
|
||||||
|
selv, med mindre `--overskriv` er oppgitt.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# prosjekt.toml
|
||||||
|
[import]
|
||||||
|
uoppvarmede_rom = ["Kott", "Bod"] # holdes utenfor oppvarmet BRA
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Yttervegg*" # alle ytterveggene, uansett himmelretning
|
||||||
|
masseklasse = "lett"
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Roof Slab" # skråtaket mangler U-verdi i modellen
|
||||||
|
u_verdi = 0.55
|
||||||
|
|
||||||
|
[[vindu]]
|
||||||
|
navn = "V-*"
|
||||||
|
g_verdi = 0.76
|
||||||
|
|
||||||
|
[[sone]]
|
||||||
|
navn = "Kjeller"
|
||||||
|
andel_distribusjonstap = 1.0
|
||||||
|
|
||||||
|
[[sone.flate]]
|
||||||
|
navn = "Kjellermur*"
|
||||||
|
over_terreng = 0.6
|
||||||
|
|
||||||
|
[[oppvarming.kilde]]
|
||||||
|
type = "el_kjel"
|
||||||
|
andel_rom = 1.0
|
||||||
|
andel_tappevann = 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Flatene får navn etter konstruksjonen i modellen, og vegger får himmelretningen etter navnet
|
||||||
|
(«Yttervegg, sør»), slik at tilleggsfilen kan vise til dem. Importen er deterministisk: samme
|
||||||
|
modell og tilleggsfil gir alltid samme `bygning.toml`. Et mønster som ikke treffer noe, nevnes
|
||||||
|
i merknadene, og et vanlig navn som ikke finnes i modellen, legges til som ny oppføring.
|
||||||
|
|
||||||
### Hva enok henter ut av en IFC-modell
|
### Hva enok henter ut av en IFC-modell
|
||||||
|
|
||||||
| Opplysning | Hvor den kommer fra |
|
| Opplysning | Hvor den kommer fra |
|
||||||
@@ -223,6 +316,23 @@ egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
|
|||||||
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
|
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
|
||||||
| `OppvarmetBRA` | `HeatedFloorArea` | `120` |
|
| `OppvarmetBRA` | `HeatedFloorArea` | `120` |
|
||||||
|
|
||||||
|
Mangler en bygningsdel U-verdi i modellen, sier importen hvilke det gjelder, og beregningen
|
||||||
|
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
|
||||||
|
eksportene ikke har U-verdi.
|
||||||
|
|
||||||
|
Importen tåler noen vanlige feil i eksporten:
|
||||||
|
|
||||||
|
- **Deler som ligger dobbelt.** Legger eksporten den samme veggen eller det samme dekket i
|
||||||
|
flere etasjer, telles den én gang. Av kopiene brukes den som hører til den etasjen høyden
|
||||||
|
tilsier, og vinduene i de andre finner fortsatt veggen sin.
|
||||||
|
- **Feil etasje.** Hva som er gulv mot grunnen, dekke over kjelleren eller etasjeskiller inne
|
||||||
|
i bygningen, avgjøres av høyden geometrien har, ikke av hvilken etasje delen er lagt i.
|
||||||
|
- **Terrengmodell utenfor bakkenivå.** Ligger terrenget over gulvet i første etasje, brukes
|
||||||
|
det ikke, og importen sier fra. Oppgi da `over_terreng` på kjellerveggene.
|
||||||
|
|
||||||
|
Importen sier også hvilke deler som ikke er regnet med i klimaskjermen, som innervegger og
|
||||||
|
etasjeskillere, slik at det er lett å se om noe mangler.
|
||||||
|
|
||||||
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
|
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
|
||||||
modellen alene.
|
modellen alene.
|
||||||
|
|
||||||
@@ -235,8 +345,8 @@ Dette i selve modellen er verdt å gjøre riktig, omtrent i rekkefølge etter hv
|
|||||||
- **Georeferer tegningen, eller sett prosjektstedet.** Da får modellen koordinater, og
|
- **Georeferer tegningen, eller sett prosjektstedet.** Da får modellen koordinater, og
|
||||||
beregningen velger selv nærmeste av de 358 klimastedene. Uten det står Vectorworks'
|
beregningen velger selv nærmeste av de 358 klimastedene. Uten det står Vectorworks'
|
||||||
standardverdi i Washington, og den blir oversett.
|
standardverdi i Washington, og den blir oversett.
|
||||||
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av
|
- **Merk yttervegger med `IsExternal`.** I en typisk eksport er bare en liten del av
|
||||||
hundre og fem vegger merket, og kjellermurene er ikke blant dem.
|
veggene merket, og kjellermurene er sjelden blant dem.
|
||||||
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
|
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
|
||||||
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
|
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
|
||||||
uoppvarmede kjelleren og regner oppvarmet areal riktig.
|
uoppvarmede kjelleren og regner oppvarmet areal riktig.
|
||||||
@@ -249,7 +359,7 @@ Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i
|
|||||||
|
|
||||||
```csv
|
```csv
|
||||||
type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||||
bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
bygning,Eldre enebolig,140,,,,smaahus,1948,
|
||||||
vegg,Yttervegg sør,26,1.0,180,90,ute,,
|
vegg,Yttervegg sør,26,1.0,180,90,ute,,
|
||||||
gulv,Mot kjeller,98,0.8,,180,uoppvarmet,,
|
gulv,Mot kjeller,98,0.8,,180,uoppvarmet,,
|
||||||
vindu,Stue sør,3.0,2.8,180,90,,0.76,2
|
vindu,Stue sør,3.0,2.8,180,90,,0.76,2
|
||||||
@@ -267,8 +377,9 @@ vertskjøreren `fish` på samme måte som de andre prosjektene:
|
|||||||
|
|
||||||
### Energirapport fra et byggeprosjekt
|
### Energirapport fra et byggeprosjekt
|
||||||
|
|
||||||
Et prosjektlager med sin egen IFC-modell og `bygning.toml` trenger ikke Rust. Det henter
|
Et prosjektlager med sin egen IFC-modell og tilleggsfil trenger ikke Rust. Det henter
|
||||||
binærfilen fra enoks siste utgivelse:
|
binærfilen fra enoks siste utgivelse. Én jobb lager `bygning.toml` av modellen, og en annen
|
||||||
|
beregner rapporten av den:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Energirapport
|
name: Energirapport
|
||||||
@@ -276,14 +387,14 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
jobs:
|
jobs:
|
||||||
rapport:
|
bygningsmodell:
|
||||||
runs-on: fish
|
runs-on: fish
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: fish {0}
|
shell: fish {0}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v6
|
- uses: https://gitea.com/actions/checkout@v6
|
||||||
- name: Hent enok og lag rapporten
|
- name: Hent enok og lag bygning.toml
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ github.token }}
|
TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
@@ -293,6 +404,27 @@ jobs:
|
|||||||
curl -sSL --fail -o enok --header "Authorization: token $TOKEN" \
|
curl -sSL --fail -o enok --header "Authorization: token $TOKEN" \
|
||||||
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
||||||
chmod +x enok
|
chmod +x enok
|
||||||
|
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
||||||
|
- uses: https://gitea.com/actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: bygningsmodell
|
||||||
|
path: |
|
||||||
|
bygning.toml
|
||||||
|
enok
|
||||||
|
rapport:
|
||||||
|
needs: bygningsmodell
|
||||||
|
runs-on: fish
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: fish {0}
|
||||||
|
steps:
|
||||||
|
- uses: https://gitea.com/actions/checkout@v6
|
||||||
|
- uses: https://gitea.com/actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: bygningsmodell
|
||||||
|
- name: Lag rapporten
|
||||||
|
run: |
|
||||||
|
chmod +x enok
|
||||||
./enok beregn bygning.toml --ut dist
|
./enok beregn bygning.toml --ut dist
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -310,12 +442,13 @@ Motoren er kontrollert mot uavhengige referanser:
|
|||||||
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
|
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
|
||||||
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
|
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
|
||||||
| Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) |
|
| Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) |
|
||||||
|
| Faste energiposter for yrkesbyggene | belysning, utstyr og varmtvann treffer energibudsjettene i kildene |
|
||||||
|
|
||||||
### Kjent avvik
|
### Kjent avvik
|
||||||
|
|
||||||
Standard Norges lanseringseksempel for NS 3031:2025 oppgir 46,2 kWh/(m²·år) romoppvarming og
|
Standard Norges lanseringseksempel for NS 3031:2025 oppgir 46,2 kWh/(m²·år) romoppvarming og
|
||||||
106,4 kWh/(m²·år) netto energibehov for TEK17-referansebygget beregnet med Simien 8. enok
|
106,4 kWh/(m²·år) netto energibehov for TEK17-referansebygget beregnet med Simien 8. enok
|
||||||
beregner 27,2 og 86,7 for den samme bygningen.
|
beregner 27,1 og 86,6 for den samme bygningen.
|
||||||
|
|
||||||
Varmetapsbudsjettet, de normerte energipostene, solinnstrålingen og energibalansen stemmer
|
Varmetapsbudsjettet, de normerte energipostene, solinnstrålingen og energibalansen stemmer
|
||||||
hver for seg, så avviket ligger i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025
|
hver for seg, så avviket ligger i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025
|
||||||
@@ -336,7 +469,7 @@ Alle normerte verdier ligger i `data/` og kan overstyres med `--data KATALOG`.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `skala.toml` | Energikarakterskalaen fra 1.1.2026, Enova SF via Multiconsult-notatet «Grunnlag for ny energikarakterskala», 19.12.2025 |
|
| `skala.toml` | Energikarakterskalaen fra 1.1.2026, Enova SF via Multiconsult-notatet «Grunnlag for ny energikarakterskala», 19.12.2025 |
|
||||||
| `vekting.toml` | Vektingsfaktorer etter energimerkeforskriften § 10a med NVEs veileder |
|
| `vekting.toml` | Vektingsfaktorer etter energimerkeforskriften § 10a med NVEs veileder |
|
||||||
| `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann er oppdatert til NS 3031:2025-verdien for småhus. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene |
|
| `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann for boliger er oppdatert til NS 3031:2025-verdien for småhus. Tillegg A er ikke offentlig, så yrkesbyggverdiene er hentet fra publiserte Simien-utskrifter og DiBK-utredninger. Hver fil oppgir kildene og hvilke verdier som er antatt. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene |
|
||||||
| `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 |
|
| `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 |
|
||||||
| `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode |
|
| `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode |
|
||||||
| `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 |
|
| `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 |
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Normerte inndata for barnehage.
|
||||||
|
#
|
||||||
|
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra offentlige
|
||||||
|
# energiberegninger som oppgir tabellverdiene for kategorien, og er kontrollert mot
|
||||||
|
# energibudsjettene i de samme rapportene:
|
||||||
|
# - Rambøll, «Kolstad barnehage – energikonsept» (Mercell, 2019): driftstid 10/5/52,
|
||||||
|
# utstyr 2 W/m², varmtvann 1,6 W/m², personer 6 W/m², «iht. NS 3031 tillegg A,
|
||||||
|
# tab. A.1–A.3, for bygningskategori barnehage». Budsjettet (utstyr 5,2 og varmtvann
|
||||||
|
# 10,0 kWh/m²) stemmer med 261 driftsdager.
|
||||||
|
# - Multiconsult for DiBK, «Grunnlag og forutsetninger for beregnede energirammer»
|
||||||
|
# (13.11.2015): personbelastning 6,0 W/m² i NS 3031.
|
||||||
|
# Belysning: rapporten over brukte passivhusverdien 5 W/m². NS 3031-verdien er ikke
|
||||||
|
# funnet i en offentlig kilde; 8 W/m² er antatt, som for kontor-, kultur- og idrettsbygg.
|
||||||
|
#
|
||||||
|
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på
|
||||||
|
# driftsdager, slik Simien oppgir den («effektbehov varmtvann på driftsdager»).
|
||||||
|
|
||||||
|
kategori = "barnehage"
|
||||||
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i offentlige energiberegninger (Rambøll 2019, Multiconsult/DiBK 2015); belysning antatt"
|
||||||
|
|
||||||
|
[settpunkt]
|
||||||
|
oppvarming = 21.0
|
||||||
|
oppvarming_natt = 19.0 # Rambøll oppgir årsmiddel 19,8 °C, som svarer til nattsenking
|
||||||
|
kjoling = 22.0
|
||||||
|
ventilasjon_tilluft = 19.0
|
||||||
|
|
||||||
|
[driftstid]
|
||||||
|
oppvarming = { start = 7, slutt = 17 } # 10 h
|
||||||
|
belysning = { start = 7, slutt = 17 }
|
||||||
|
utstyr = { start = 7, slutt = 17 }
|
||||||
|
varmtvann = { start = 0, slutt = 24 }
|
||||||
|
personer = { start = 7, slutt = 17 }
|
||||||
|
ventilasjon = { start = 7, slutt = 17 }
|
||||||
|
dager_per_uke = 5
|
||||||
|
uker_per_aar = 52 # 261 driftsdager
|
||||||
|
|
||||||
|
[belysning]
|
||||||
|
effekt = 8.0 # antatt → 20,9 kWh/(m²·år)
|
||||||
|
varmetilskudd = 8.0
|
||||||
|
|
||||||
|
[utstyr]
|
||||||
|
effekt = 2.0 # → 5,2 kWh/(m²·år)
|
||||||
|
varmetilskudd = 2.0
|
||||||
|
|
||||||
|
[varmtvann]
|
||||||
|
effekt = 1.6 # → 10,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 0.0
|
||||||
|
|
||||||
|
[personer]
|
||||||
|
varmetilskudd = 6.0
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
luftmengde_driftstid = 10.0 # m³/(h·m²), prosjektverdi i Rambøll-rapporten
|
||||||
|
luftmengde_utenfor_driftstid = 2.0
|
||||||
|
sfp = 1.5
|
||||||
|
temperaturvirkningsgrad = 0.80
|
||||||
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|
||||||
|
[termisk]
|
||||||
|
varmekapasitet_normalisert = 72.6
|
||||||
|
kuldebroer_normalisert = 0.07
|
||||||
|
|
||||||
|
[pumper]
|
||||||
|
effekt_vannbaaren = 0.5
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
# Normerte inndata for boligblokk.
|
# Normerte inndata for boligblokk.
|
||||||
# Standardverdier fra NS 3031:2014 Tillegg A (Tabell A.3/A.5) slik de også fremgår av Enovas
|
# NS 3031:2014 Tillegg A gir de samme verdiene for boligblokk som for småhus. Det bekreftes
|
||||||
# XML-eksempel for energimerkesystemet. NS 3031:2025 Tillegg A gir timesprofiler over døgnet;
|
# av Simien-utskriften for en boligblokk (Multiconsult 2013, «NS3031, tabell A.1–A.3»):
|
||||||
# eieren av standarden kan legge inn 24-verdiers profiler i feltene `profil_*` nedenfor, som da
|
# belysning 1,95 W/m², utstyr 3,00 W/m² med varmetilskudd 1,80, varmtvann 3,40 W/m² over
|
||||||
# overstyrer driftstidene. Alle effekter i W per m² oppvarmet BRA.
|
# døgnet (29,8 kWh/m²), personer 1,5 W/m², driftstid 16 h og personer/ventilasjon 24 h.
|
||||||
|
# Varmtvann følger småhusverdien fra NS 3031:2025 (25,1 kWh/m²); verdien for boligblokk i
|
||||||
|
# 2025-utgaven er ikke funnet i en offentlig kilde. Alle effekter i W per m² oppvarmet BRA.
|
||||||
|
|
||||||
kategori = "boligblokk"
|
kategori = "boligblokk"
|
||||||
kilde = "NS 3031:2014 Tillegg A (standardverdier); NS 3031:2025-profiler kan legges inn"
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i Simien-utskrift (Multiconsult 2013); luftmengde fra Multiconsult/DiBK 2014"
|
||||||
|
|
||||||
[settpunkt]
|
[settpunkt]
|
||||||
oppvarming = 21.0 # °C i driftstiden
|
oppvarming = 21.0 # °C i driftstiden
|
||||||
@@ -42,8 +44,8 @@ varmetilskudd = 0.0
|
|||||||
varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden
|
varmetilskudd = 1.5 # W/m² (sensibel varme) i driftstiden
|
||||||
|
|
||||||
[ventilasjon]
|
[ventilasjon]
|
||||||
luftmengde_driftstid = 1.2 # m³/(h·m²)
|
luftmengde_driftstid = 1.5 # m³/(h·m²); modellbygget for boligblokk (Multiconsult/DiBK 2014,
|
||||||
luftmengde_utenfor_driftstid = 1.2 # m³/(h·m²)
|
luftmengde_utenfor_driftstid = 1.5 # avsnitt 8.2), svarer til leiligheter på om lag 65 m²
|
||||||
sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon
|
sfp = 1.5 # kW/(m³/s), gjelder mekanisk ventilasjon
|
||||||
temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon
|
temperaturvirkningsgrad = 0.80 # varmegjenvinner, gjelder balansert ventilasjon
|
||||||
frostsikring_virkningsgrad = 0.80
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Normerte inndata for idrettsbygg.
|
||||||
|
#
|
||||||
|
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra to uavhengige,
|
||||||
|
# offentlige Simien-utskrifter for idrettshaller:
|
||||||
|
# - «Energiberegning og evaluering mot energikrav» (Mercell, 31.10.2018), sone
|
||||||
|
# Idrettshall: driftstid 12 h, belysning 8 W/m², utstyr 1 W/m², varmtvann 9,5 W/m²
|
||||||
|
# på driftsdager, personer 10 W/m², luftmengde 8 og 2 m³/(h·m²).
|
||||||
|
# - «Evaluering Energiregler 2016», Gimlehallen (BLOM Arkitekter, 2024): de samme
|
||||||
|
# verdiene for utstyr, varmtvann, personer og luftmengder (belysningen er prosjektverdi).
|
||||||
|
# Begge budsjettene (varmtvann 50,2 og utstyr 2,6 kWh/m²) svarer til 220 driftsdager,
|
||||||
|
# altså 5 dager i 44 uker. Begge utskriftene oppgir settpunkt 18,0 °C.
|
||||||
|
#
|
||||||
|
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
|
||||||
|
|
||||||
|
kategori = "idrettsbygg"
|
||||||
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i to offentlige Simien-utskrifter (2018, 2024)"
|
||||||
|
|
||||||
|
[settpunkt]
|
||||||
|
oppvarming = 18.0
|
||||||
|
oppvarming_natt = 18.0 # utskriftene oppgir 18,0 °C som årsmiddel
|
||||||
|
kjoling = 22.0
|
||||||
|
ventilasjon_tilluft = 18.0
|
||||||
|
|
||||||
|
[driftstid]
|
||||||
|
oppvarming = { start = 8, slutt = 20 } # 12 h
|
||||||
|
belysning = { start = 8, slutt = 20 }
|
||||||
|
utstyr = { start = 8, slutt = 20 }
|
||||||
|
varmtvann = { start = 0, slutt = 24 }
|
||||||
|
personer = { start = 8, slutt = 20 }
|
||||||
|
ventilasjon = { start = 8, slutt = 20 }
|
||||||
|
dager_per_uke = 5
|
||||||
|
uker_per_aar = 44 # 220 driftsdager
|
||||||
|
|
||||||
|
[belysning]
|
||||||
|
effekt = 8.0 # → 21,1 kWh/(m²·år)
|
||||||
|
varmetilskudd = 8.0
|
||||||
|
|
||||||
|
[utstyr]
|
||||||
|
effekt = 1.0 # → 2,6 kWh/(m²·år)
|
||||||
|
varmetilskudd = 1.0
|
||||||
|
|
||||||
|
[varmtvann]
|
||||||
|
effekt = 9.5 # → 50,2 kWh/(m²·år)
|
||||||
|
varmetilskudd = 0.0
|
||||||
|
|
||||||
|
[personer]
|
||||||
|
varmetilskudd = 10.0
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
luftmengde_driftstid = 8.0 # m³/(h·m²)
|
||||||
|
luftmengde_utenfor_driftstid = 2.0
|
||||||
|
sfp = 1.5
|
||||||
|
temperaturvirkningsgrad = 0.80
|
||||||
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|
||||||
|
[termisk]
|
||||||
|
varmekapasitet_normalisert = 72.6
|
||||||
|
kuldebroer_normalisert = 0.07
|
||||||
|
|
||||||
|
[pumper]
|
||||||
|
effekt_vannbaaren = 0.5
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Normerte inndata for kontorbygg.
|
||||||
|
#
|
||||||
|
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra offentlige kilder:
|
||||||
|
# - Multiconsult for DiBK, «Konsekvensvurdering av Energiregler 2015» (11.12.2014),
|
||||||
|
# avsnitt 7.1 og 8.2: belysning 8 W/m² og veiledende luftmengde 10 m³/(h·m²) etter
|
||||||
|
# NS 3031 tabell A1 og B1.
|
||||||
|
# - Simien-utskrift «Passivhusevaluering», kontorbygg (Mercell): driftstid 12 h,
|
||||||
|
# varmtvann 0,80 W/m² på driftsdager, personer 4 W/m². Personbelastningen og
|
||||||
|
# driftstiden er de samme som i NS 3031; belysning og utstyr er passivhusverdier.
|
||||||
|
# - Utstyr 11 W/m²: Simien-evaluering for et kontorbygg (Asplan Viak 2019, FutureBuilt).
|
||||||
|
# Bare søkeutdraget er lest; fulltekstdokumentet var ikke tilgjengelig.
|
||||||
|
# - NVE, «Praktisk veileder for energimerking» (2013): kontorbygg har driftstid 5 dager
|
||||||
|
# i 52 uker.
|
||||||
|
#
|
||||||
|
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
|
||||||
|
|
||||||
|
kategori = "kontorbygg"
|
||||||
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i offentlige kilder (Multiconsult/DiBK 2014, Simien-utskrifter); utstyr ikke verifisert i fulltekst"
|
||||||
|
|
||||||
|
[settpunkt]
|
||||||
|
oppvarming = 21.0
|
||||||
|
oppvarming_natt = 19.0
|
||||||
|
kjoling = 22.0
|
||||||
|
ventilasjon_tilluft = 19.0
|
||||||
|
|
||||||
|
[driftstid]
|
||||||
|
oppvarming = { start = 7, slutt = 19 } # 12 h
|
||||||
|
belysning = { start = 7, slutt = 19 }
|
||||||
|
utstyr = { start = 7, slutt = 19 }
|
||||||
|
varmtvann = { start = 0, slutt = 24 }
|
||||||
|
personer = { start = 7, slutt = 19 }
|
||||||
|
ventilasjon = { start = 7, slutt = 19 }
|
||||||
|
dager_per_uke = 5
|
||||||
|
uker_per_aar = 52 # 261 driftsdager
|
||||||
|
|
||||||
|
[belysning]
|
||||||
|
effekt = 8.0 # → 25,1 kWh/(m²·år)
|
||||||
|
varmetilskudd = 8.0
|
||||||
|
|
||||||
|
[utstyr]
|
||||||
|
effekt = 11.0 # → 34,5 kWh/(m²·år)
|
||||||
|
varmetilskudd = 11.0
|
||||||
|
|
||||||
|
[varmtvann]
|
||||||
|
effekt = 0.8 # → 5,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 0.0
|
||||||
|
|
||||||
|
[personer]
|
||||||
|
varmetilskudd = 4.0
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
luftmengde_driftstid = 10.0 # m³/(h·m²), NS 3031 tabell B1 (veiledende)
|
||||||
|
luftmengde_utenfor_driftstid = 2.0 # som i Simien-utskriftene for yrkesbygg
|
||||||
|
sfp = 1.5
|
||||||
|
temperaturvirkningsgrad = 0.80
|
||||||
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|
||||||
|
[termisk]
|
||||||
|
varmekapasitet_normalisert = 72.6
|
||||||
|
kuldebroer_normalisert = 0.07
|
||||||
|
|
||||||
|
[pumper]
|
||||||
|
effekt_vannbaaren = 0.5
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Normerte inndata for kulturbygg.
|
||||||
|
#
|
||||||
|
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra COWI,
|
||||||
|
# «Energikonsept Kystens Arv» (Mercell), tabell 7 «Standardverdier for internlaster etter
|
||||||
|
# NS 3031» og Simien-utskriften i samme rapport: belysning 8 W/m², utstyr 1 W/m²,
|
||||||
|
# varmtvann 1,6 W/m² på driftsdager og personer 3,2 W/m², luftmengde 10 og 2 m³/(h·m²).
|
||||||
|
# Rapporten skriver driftstid 12/5/52, men Simien-utskriften har 11 h, og energibehovene
|
||||||
|
# i tabell 7 (belysning 23, utstyr 2,9 og varmtvann 10 kWh/m²) stemmer med 11 h i
|
||||||
|
# 261 driftsdager.
|
||||||
|
#
|
||||||
|
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
|
||||||
|
|
||||||
|
kategori = "kulturbygg"
|
||||||
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i COWI, Energikonsept Kystens Arv"
|
||||||
|
|
||||||
|
[settpunkt]
|
||||||
|
oppvarming = 21.0
|
||||||
|
oppvarming_natt = 19.0 # utskriften oppgir årsmiddel 19,9 °C
|
||||||
|
kjoling = 22.0
|
||||||
|
ventilasjon_tilluft = 19.0
|
||||||
|
|
||||||
|
[driftstid]
|
||||||
|
oppvarming = { start = 10, slutt = 21 } # 11 h
|
||||||
|
belysning = { start = 10, slutt = 21 }
|
||||||
|
utstyr = { start = 10, slutt = 21 }
|
||||||
|
varmtvann = { start = 0, slutt = 24 }
|
||||||
|
personer = { start = 10, slutt = 21 }
|
||||||
|
ventilasjon = { start = 10, slutt = 21 }
|
||||||
|
dager_per_uke = 5
|
||||||
|
uker_per_aar = 52 # 261 driftsdager
|
||||||
|
|
||||||
|
[belysning]
|
||||||
|
effekt = 8.0 # → 23,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 8.0
|
||||||
|
|
||||||
|
[utstyr]
|
||||||
|
effekt = 1.0 # → 2,9 kWh/(m²·år)
|
||||||
|
varmetilskudd = 1.0
|
||||||
|
|
||||||
|
[varmtvann]
|
||||||
|
effekt = 1.6 # → 10,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 0.0
|
||||||
|
|
||||||
|
[personer]
|
||||||
|
varmetilskudd = 3.2
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
luftmengde_driftstid = 10.0 # m³/(h·m²)
|
||||||
|
luftmengde_utenfor_driftstid = 2.0
|
||||||
|
sfp = 1.5
|
||||||
|
temperaturvirkningsgrad = 0.80
|
||||||
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|
||||||
|
[termisk]
|
||||||
|
varmekapasitet_normalisert = 72.6
|
||||||
|
kuldebroer_normalisert = 0.07
|
||||||
|
|
||||||
|
[pumper]
|
||||||
|
effekt_vannbaaren = 0.5
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Normerte inndata for skolebygg.
|
||||||
|
#
|
||||||
|
# NS 3031 Tillegg A er ikke fritt tilgjengelig. Verdiene er hentet fra en offentlig
|
||||||
|
# Simien-utskrift, «Energiberegning og evaluering mot energikrav» for en skole
|
||||||
|
# (Mercell, 31.10.2018), sonene for arbeidsrom og klasserom: driftstid 10 h, belysning
|
||||||
|
# 10 W/m², utstyr 6 W/m², varmtvann 1,9 W/m² på driftsdager og personer 12 W/m².
|
||||||
|
# Budsjettet i rapporten (varmtvann 10,0 kWh/m²) svarer til 220 driftsdager, altså
|
||||||
|
# 5 dager i 44 uker.
|
||||||
|
#
|
||||||
|
# Alle effekter i W per m² oppvarmet BRA. Varmtvann er effekt over hele døgnet på driftsdager.
|
||||||
|
|
||||||
|
kategori = "skolebygg"
|
||||||
|
kilde = "NS 3031:2014 Tillegg A slik gjengitt i en offentlig Simien-utskrift (2018)"
|
||||||
|
|
||||||
|
[settpunkt]
|
||||||
|
oppvarming = 21.0
|
||||||
|
oppvarming_natt = 19.0 # utskriften oppgir årsmiddel 19,8 °C
|
||||||
|
kjoling = 22.0
|
||||||
|
ventilasjon_tilluft = 19.0
|
||||||
|
|
||||||
|
[driftstid]
|
||||||
|
oppvarming = { start = 8, slutt = 18 } # 10 h
|
||||||
|
belysning = { start = 8, slutt = 18 }
|
||||||
|
utstyr = { start = 8, slutt = 18 }
|
||||||
|
varmtvann = { start = 0, slutt = 24 }
|
||||||
|
personer = { start = 8, slutt = 18 }
|
||||||
|
ventilasjon = { start = 8, slutt = 18 }
|
||||||
|
dager_per_uke = 5
|
||||||
|
uker_per_aar = 44 # 220 driftsdager
|
||||||
|
|
||||||
|
[belysning]
|
||||||
|
effekt = 10.0 # → 22,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 10.0
|
||||||
|
|
||||||
|
[utstyr]
|
||||||
|
effekt = 6.0 # → 13,2 kWh/(m²·år)
|
||||||
|
varmetilskudd = 6.0
|
||||||
|
|
||||||
|
[varmtvann]
|
||||||
|
effekt = 1.9 # → 10,0 kWh/(m²·år)
|
||||||
|
varmetilskudd = 0.0
|
||||||
|
|
||||||
|
[personer]
|
||||||
|
varmetilskudd = 12.0
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
luftmengde_driftstid = 10.0 # m³/(h·m²)
|
||||||
|
luftmengde_utenfor_driftstid = 2.0
|
||||||
|
sfp = 1.5
|
||||||
|
temperaturvirkningsgrad = 0.80
|
||||||
|
frostsikring_virkningsgrad = 0.80
|
||||||
|
|
||||||
|
[termisk]
|
||||||
|
varmekapasitet_normalisert = 72.6
|
||||||
|
kuldebroer_normalisert = 0.07
|
||||||
|
|
||||||
|
[pumper]
|
||||||
|
effekt_vannbaaren = 0.5
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur.
|
# Eksempel: enebolig i tre fra 1948, 1,5 etasje på betong grunnmur.
|
||||||
# Bygningsdata er hentet fra termografirapporten av 10.3.2016 og originale byggetegninger.
|
# Bygningsdataene er typiske for et utett hus fra perioden.
|
||||||
# Kjelleren er uoppvarmet, men varmes indirekte av uisolerte varmerør.
|
# Kjelleren er uoppvarmet, men varmes indirekte av uisolerte varmerør.
|
||||||
|
|
||||||
[bygning]
|
[bygning]
|
||||||
navn = "Nordhusstykket 5"
|
navn = "Eldre enebolig med kjeller"
|
||||||
kategori = "smaahus"
|
kategori = "smaahus"
|
||||||
byggeaar = 1948
|
byggeaar = 1948
|
||||||
oppvarmet_bra = 140.0
|
oppvarmet_bra = 140.0
|
||||||
oppvarmet_volum = 336.0
|
oppvarmet_volum = 336.0
|
||||||
adresse = "Nordhusstykket 5, 1825 Tomter"
|
|
||||||
oppdragsgiver = "Ellen Brødholt"
|
|
||||||
beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 98 m², betong grunnmur"
|
beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 98 m², betong grunnmur"
|
||||||
|
|
||||||
[klima]
|
[klima]
|
||||||
+45
-2
@@ -14,6 +14,11 @@ const MATERIALER: &str = include_str!("../data/materialer.toml");
|
|||||||
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
|
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
|
||||||
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
|
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
|
||||||
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
|
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
|
||||||
|
const NORMERTE_BARNEHAGE: &str = include_str!("../data/normerte/barnehage.toml");
|
||||||
|
const NORMERTE_KONTORBYGG: &str = include_str!("../data/normerte/kontorbygg.toml");
|
||||||
|
const NORMERTE_SKOLEBYGG: &str = include_str!("../data/normerte/skolebygg.toml");
|
||||||
|
const NORMERTE_IDRETTSBYGG: &str = include_str!("../data/normerte/idrettsbygg.toml");
|
||||||
|
const NORMERTE_KULTURBYGG: &str = include_str!("../data/normerte/kulturbygg.toml");
|
||||||
|
|
||||||
/// Katalog som kan overstyre de innebygde datafilene.
|
/// Katalog som kan overstyre de innebygde datafilene.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@@ -63,8 +68,13 @@ impl Datakilde {
|
|||||||
let (fil, innebygd) = match kategori {
|
let (fil, innebygd) = match kategori {
|
||||||
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
|
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
|
||||||
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
|
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
|
||||||
|
Kategori::Barnehage => ("normerte/barnehage.toml", NORMERTE_BARNEHAGE),
|
||||||
|
Kategori::Kontorbygg => ("normerte/kontorbygg.toml", NORMERTE_KONTORBYGG),
|
||||||
|
Kategori::Skolebygg => ("normerte/skolebygg.toml", NORMERTE_SKOLEBYGG),
|
||||||
|
Kategori::Idrettsbygg => ("normerte/idrettsbygg.toml", NORMERTE_IDRETTSBYGG),
|
||||||
|
Kategori::Kulturbygg => ("normerte/kulturbygg.toml", NORMERTE_KULTURBYGG),
|
||||||
annen => {
|
annen => {
|
||||||
// Andre kategorier krever egen datafil i data-katalogen.
|
// Kategorier uten offentlig tilgjengelige normerte verdier krever egen datafil.
|
||||||
let fil = format!("normerte/{}.toml", annen.nokkel());
|
let fil = format!("normerte/{}.toml", annen.nokkel());
|
||||||
let p = self
|
let p = self
|
||||||
.katalog
|
.katalog
|
||||||
@@ -257,9 +267,12 @@ pub struct Settpunkt {
|
|||||||
pub oppvarming_natt: f64,
|
pub oppvarming_natt: f64,
|
||||||
pub kjoling: f64,
|
pub kjoling: f64,
|
||||||
pub ventilasjon_tilluft: f64,
|
pub ventilasjon_tilluft: f64,
|
||||||
|
/// Tilluftstemperatur når anlegget har kjølebatteri. Utelatt: lik `ventilasjon_tilluft`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub ventilasjon_tilluft_kjoling: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, serde::Serialize, Clone, Debug)]
|
||||||
pub struct Driftstider {
|
pub struct Driftstider {
|
||||||
pub oppvarming: Tidsrom,
|
pub oppvarming: Tidsrom,
|
||||||
pub belysning: Tidsrom,
|
pub belysning: Tidsrom,
|
||||||
@@ -267,13 +280,43 @@ pub struct Driftstider {
|
|||||||
pub varmtvann: Tidsrom,
|
pub varmtvann: Tidsrom,
|
||||||
pub personer: Tidsrom,
|
pub personer: Tidsrom,
|
||||||
pub ventilasjon: Tidsrom,
|
pub ventilasjon: Tidsrom,
|
||||||
|
/// Driftsdager per uke, regnet fra mandag (NS 3031 Tabell A.3).
|
||||||
#[serde(default = "sju")]
|
#[serde(default = "sju")]
|
||||||
pub dager_per_uke: u8,
|
pub dager_per_uke: u8,
|
||||||
|
/// Driftsuker per år (NS 3031 Tabell A.3). Ukene uten drift legges i ferier, se [`FERIEUKER`].
|
||||||
|
#[serde(default = "femtito")]
|
||||||
|
pub uker_per_aar: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sju() -> u8 {
|
fn sju() -> u8 {
|
||||||
7
|
7
|
||||||
}
|
}
|
||||||
|
fn femtito() -> u8 {
|
||||||
|
52
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uker uten drift, i den rekkefølgen de tas ut når en kategori har færre enn 52 driftsuker:
|
||||||
|
/// fellesferien først, deretter jul, vinterferie, påske og høstferie, og så uke for uke
|
||||||
|
/// utover sommeren og nyttår.
|
||||||
|
pub const FERIEUKER: [u8; 16] = [28, 29, 30, 31, 27, 32, 52, 8, 14, 40, 26, 33, 1, 25, 34, 9];
|
||||||
|
|
||||||
|
impl Driftstider {
|
||||||
|
/// Om døgn nummer `dag` (0 = 1. januar) er en driftsdag.
|
||||||
|
///
|
||||||
|
/// Året regnes å begynne på en mandag. Døgn 364 hører til uke 52.
|
||||||
|
pub fn driftsdag(&self, dag: usize) -> bool {
|
||||||
|
if dag % 7 >= self.dager_per_uke.min(7) as usize {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let uke = (dag / 7 + 1).min(52) as u8;
|
||||||
|
let fri = (52u8.saturating_sub(self.uker_per_aar) as usize).min(FERIEUKER.len());
|
||||||
|
!FERIEUKER[..fri].contains(&uke)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn driftsdager_per_aar(&self) -> usize {
|
||||||
|
(0..365).filter(|&d| self.driftsdag(d)).count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
|
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
|
||||||
pub struct Tidsrom {
|
pub struct Tidsrom {
|
||||||
|
|||||||
@@ -481,13 +481,13 @@ fn bygg(
|
|||||||
let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon);
|
let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon);
|
||||||
aapningsareal += areal;
|
aapningsareal += areal;
|
||||||
let vt = vindustyper.get(&a.vindustype);
|
let vt = vindustyper.get(&a.vindustype);
|
||||||
let u = vt.and_then(|v| v.u_verdi).unwrap_or_else(|| {
|
let u = vt.and_then(|v| v.u_verdi);
|
||||||
|
if u.is_none() {
|
||||||
merknader.push(format!(
|
merknader.push(format!(
|
||||||
"Åpningen «{}» mangler U-verdi i gbXML; satt til 2,4 W/(m²K).",
|
"Åpningen «{}» mangler U-verdi i gbXML; verdien utledes av byggeåret.",
|
||||||
a.navn
|
a.navn
|
||||||
));
|
));
|
||||||
2.4
|
}
|
||||||
});
|
|
||||||
let dor = a.typ.to_lowercase().contains("door");
|
let dor = a.typ.to_lowercase().contains("door");
|
||||||
vinduer.push(Vindu {
|
vinduer.push(Vindu {
|
||||||
navn: vt
|
navn: vt
|
||||||
@@ -596,6 +596,7 @@ fn bygg(
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport { bygning, merknader })
|
Ok(Importrapport { bygning, merknader })
|
||||||
}
|
}
|
||||||
@@ -687,7 +688,7 @@ mod tester {
|
|||||||
// Vinduet.
|
// Vinduet.
|
||||||
assert_eq!(b.vindu.len(), 1);
|
assert_eq!(b.vindu.len(), 1);
|
||||||
assert_eq!(b.vindu[0].areal, 3.0);
|
assert_eq!(b.vindu[0].areal, 3.0);
|
||||||
assert_eq!(b.vindu[0].u_verdi, 1.2);
|
assert_eq!(b.vindu[0].u_verdi, Some(1.2));
|
||||||
assert_eq!(b.vindu[0].g_verdi, Some(0.5));
|
assert_eq!(b.vindu[0].g_verdi, Some(0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+326
-43
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
use crate::inndata::ifcgeom;
|
use crate::inndata::ifcgeom;
|
||||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||||
|
use crate::inndata::toml_inn::Importvalg;
|
||||||
use crate::klima::geo;
|
use crate::klima::geo;
|
||||||
use crate::modell::*;
|
use crate::modell::*;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -25,21 +26,33 @@ pub struct Importrapport {
|
|||||||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Konstruksjonsnavn og avrundet plassering, som skiller én bygningsdel fra en annen.
|
||||||
|
type Dublettnokkel = (String, i64, i64, i64, i64);
|
||||||
|
|
||||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||||
|
|
||||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||||
|
les_med(sti, &Importvalg::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn les_med(sti: &Path, valg: &Importvalg) -> Result<Importrapport> {
|
||||||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||||
let tekst = String::from_utf8_lossy(&raa);
|
let tekst = String::from_utf8_lossy(&raa);
|
||||||
fra_tekst(
|
fra_tekst_med(
|
||||||
&tekst,
|
&tekst,
|
||||||
sti.file_stem()
|
sti.file_stem()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
.unwrap_or("Bygning"),
|
.unwrap_or("Bygning"),
|
||||||
|
valg,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||||
|
fra_tekst_med(tekst, standardnavn, &Importvalg::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Result<Importrapport> {
|
||||||
let fil = step::les(tekst);
|
let fil = step::les(tekst);
|
||||||
let mut merknader = Vec::new();
|
let mut merknader = Vec::new();
|
||||||
|
|
||||||
@@ -88,6 +101,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
let mut rom: Vec<(String, f64)> = Vec::new();
|
let mut rom: Vec<(String, f64)> = Vec::new();
|
||||||
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
|
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
|
||||||
let mut mistenkelige: Vec<String> = Vec::new();
|
let mut mistenkelige: Vec<String> = Vec::new();
|
||||||
|
let mut uoppvarmede: Vec<String> = Vec::new();
|
||||||
for (r, e) in fil.av_type("IFCSPACE") {
|
for (r, e) in fil.av_type("IFCSPACE") {
|
||||||
let navn = romnavn(e);
|
let navn = romnavn(e);
|
||||||
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
|
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
|
||||||
@@ -111,6 +125,14 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
kjellervolum += romvolum;
|
kjellervolum += romvolum;
|
||||||
} else {
|
} else {
|
||||||
let lavt = navn.to_lowercase();
|
let lavt = navn.to_lowercase();
|
||||||
|
if valg
|
||||||
|
.uoppvarmede_rom
|
||||||
|
.iter()
|
||||||
|
.any(|u| lavt.contains(&u.to_lowercase()))
|
||||||
|
{
|
||||||
|
uoppvarmede.push(format!("{navn} {areal:.1} m²"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
|
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
|
||||||
.iter()
|
.iter()
|
||||||
.any(|o| lavt.contains(o))
|
.any(|o| lavt.contains(o))
|
||||||
@@ -136,6 +158,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
mistenkelige.join(", ")
|
mistenkelige.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if !uoppvarmede.is_empty() {
|
||||||
|
merknader.push(format!(
|
||||||
|
"Disse rommene er uoppvarmet etter tilleggsfilen og holdt utenfor oppvarmet BRA: {}.",
|
||||||
|
uoppvarmede.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||||
@@ -157,9 +185,59 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
plass,
|
plass,
|
||||||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||||||
normal: flate.map(|f| f.normal),
|
normal: flate.map(|f| f.normal),
|
||||||
|
z: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.hoyde).unwrap_or(0.0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Noen eksporter legger den samme bygningsdelen i flere etasjer. To deler med samme
|
||||||
|
// konstruksjon, samme areal og samme plassering er den samme veggen eller det samme
|
||||||
|
// dekket. Kopiene blir stående, slik at vinduene i dem finner veggen sin, men de telles
|
||||||
|
// ikke med i klimaskjermen. Av kopiene beholdes den som ligger i den etasjen høyden
|
||||||
|
// tilsier, slik at en kjellervegg havner i kjelleren.
|
||||||
|
let konstruksjon = |navn: &str| navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||||||
|
let mut grupper: Vec<(Dublettnokkel, Vec<u64>)> = Vec::new();
|
||||||
|
for d in &deler {
|
||||||
|
let Some((x, y, _, _, _)) = d.plass else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let cm = |v: f64| (v * 50.0).round() as i64;
|
||||||
|
let nokkel = (
|
||||||
|
konstruksjon(&d.navn),
|
||||||
|
cm(x),
|
||||||
|
cm(y),
|
||||||
|
cm(d.z),
|
||||||
|
(d.areal * 100.0).round() as i64,
|
||||||
|
);
|
||||||
|
match grupper.iter_mut().find(|(k, _)| *k == nokkel) {
|
||||||
|
Some((_, medlemmer)) => medlemmer.push(d.nr),
|
||||||
|
None => grupper.push((nokkel, vec![d.nr])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut dublett_av: HashMap<u64, u64> = HashMap::new();
|
||||||
|
for (_, medlemmer) in grupper.iter().filter(|(_, m)| m.len() > 1) {
|
||||||
|
let avstand_til_etasjen = |nr: u64| {
|
||||||
|
let d = deler.iter().find(|d| d.nr == nr)?;
|
||||||
|
let hoyde = d.etasje.and_then(|i| etasjer.get(i)).map(|e| e.hoyde)?;
|
||||||
|
Some((d.z - hoyde).abs())
|
||||||
|
};
|
||||||
|
let beholdt = *medlemmer
|
||||||
|
.iter()
|
||||||
|
.min_by(|a, b| {
|
||||||
|
let (fa, fb) = (avstand_til_etasjen(**a), avstand_til_etasjen(**b));
|
||||||
|
match (fa, fb) {
|
||||||
|
(Some(x), Some(y)) => x.total_cmp(&y).then(a.cmp(b)),
|
||||||
|
(Some(_), None) => std::cmp::Ordering::Less,
|
||||||
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
|
(None, None) => a.cmp(b),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(&medlemmer[0]);
|
||||||
|
for nr in medlemmer.iter().filter(|nr| **nr != beholdt) {
|
||||||
|
dublett_av.insert(*nr, beholdt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let dubletter = dublett_av.len();
|
||||||
|
|
||||||
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||||||
let ytre_deler: Vec<&Del> = deler
|
let ytre_deler: Vec<&Del> = deler
|
||||||
.iter()
|
.iter()
|
||||||
@@ -209,8 +287,16 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
||||||
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
||||||
let terreng = terrenghoyder(&fil, skala);
|
let terreng = terrenghoyder(&fil, skala);
|
||||||
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
// Ligger terrengmodellen over gulvet i første etasje, er den ikke lagt i bakkenivå, og da
|
||||||
|
// sier den ingenting om hvor mye av grunnmuren som er nedgravd.
|
||||||
|
let terreng_over_gulvet = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||||
(Some((referanse, terrengnivaa)), Some(forste)) => {
|
(Some((referanse, terrengnivaa)), Some(forste)) => {
|
||||||
|
terrengnivaa > referanse + forste.hoyde + 0.5
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||||
|
(Some((referanse, terrengnivaa)), Some(forste)) if !terreng_over_gulvet => {
|
||||||
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
||||||
}
|
}
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
@@ -220,20 +306,20 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
let mut kjellerflater: Vec<Flate> = Vec::new();
|
let mut kjellerflater: Vec<Flate> = Vec::new();
|
||||||
let mut flater: Vec<Flate> = Vec::new();
|
let mut flater: Vec<Flate> = Vec::new();
|
||||||
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
||||||
let mut uten_uverdi = 0usize;
|
let mut uten_uverdi: Vec<String> = Vec::new();
|
||||||
let mut uten_geometri = 0usize;
|
let mut uten_geometri = 0usize;
|
||||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||||
|
|
||||||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||||||
let etasjekontekst = Etasjekontekst {
|
let etasjekontekst = Etasjekontekst {
|
||||||
nederste: (!etasjer.is_empty()).then_some(0),
|
|
||||||
kjeller: kjelleretasje,
|
kjeller: kjelleretasje,
|
||||||
forste_over,
|
forste_over,
|
||||||
|
forste_over_hoyde: forste_over.and_then(|i| etasjer.get(i)).map(|e| e.hoyde),
|
||||||
};
|
};
|
||||||
let laveste = deler
|
let laveste = deler
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||||
.filter_map(|d| d.plass.map(|p| p.2))
|
.map(|d| d.z)
|
||||||
.fold(f64::INFINITY, f64::min);
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
|
||||||
for d in &deler {
|
for d in &deler {
|
||||||
@@ -246,23 +332,32 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
uten_geometri += 1;
|
uten_geometri += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if dublett_av.contains_key(&d.nr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
||||||
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
||||||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// En etasjeskiller i den laveste etasjen over terreng er grensen mot kjelleren.
|
// En etasjeskiller like over kjelleren er grensen mot den uoppvarmede sonen. Høyden
|
||||||
|
// avgjør, ikke etasjetilknytningen, som kan være feil i eksporten.
|
||||||
|
let ved_kjellertaket = etasjekontekst
|
||||||
|
.forste_over_hoyde
|
||||||
|
.map(|h| (d.z - h).abs() < 0.6)
|
||||||
|
.unwrap_or(false);
|
||||||
let (mot, sone) = if kjelleretasje.is_some()
|
let (mot, sone) = if kjelleretasje.is_some()
|
||||||
&& !i_kjeller
|
&& !i_kjeller
|
||||||
&& d.etasje == forste_over
|
&& ved_kjellertaket
|
||||||
&& typ == FlateType::Gulv
|
&& typ == FlateType::Gulv
|
||||||
|
&& mot != Mot::Grunn
|
||||||
{
|
{
|
||||||
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
||||||
} else {
|
} else {
|
||||||
(mot, None)
|
(mot, None)
|
||||||
};
|
};
|
||||||
if d.u_verdi.is_none() {
|
if d.u_verdi.is_none() {
|
||||||
uten_uverdi += 1;
|
uten_uverdi.push(d.navn.clone());
|
||||||
}
|
}
|
||||||
let mal = if i_kjeller {
|
let mal = if i_kjeller {
|
||||||
&mut kjellerflater
|
&mut kjellerflater
|
||||||
@@ -299,6 +394,9 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
|
|
||||||
// ── vinduer og dører ────────────────────────────────────────────
|
// ── vinduer og dører ────────────────────────────────────────────
|
||||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||||
|
let mut sette_vinduer: std::collections::HashSet<(String, (i64, i64, i64), i64)> =
|
||||||
|
Default::default();
|
||||||
|
let mut vindusdubletter = 0usize;
|
||||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||||
let dor = e.typ == "IFCDOOR";
|
let dor = e.typ == "IFCDOOR";
|
||||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||||
@@ -315,7 +413,9 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
dor_vert.get(&r)
|
dor_vert.get(&r)
|
||||||
} else {
|
} else {
|
||||||
vindu_vert.get(&r)
|
vindu_vert.get(&r)
|
||||||
};
|
}
|
||||||
|
.map(|v| dublett_av.get(v).copied().unwrap_or(*v));
|
||||||
|
let vert = vert.as_ref();
|
||||||
// Vinduer i innervegger hører ikke til klimaskjermen.
|
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||||||
let i_ytre_vegg = vert
|
let i_ytre_vegg = vert
|
||||||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||||||
@@ -329,18 +429,35 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Det samme vinduet i to kopier av den samme veggen er ett vindu.
|
||||||
|
let sted = e
|
||||||
|
.arg(5)
|
||||||
|
.and_then(|a| a.som_ref())
|
||||||
|
.and_then(|p| global_plassering(&fil, p, skala))
|
||||||
|
.map(|(x, y, z, _, _)| {
|
||||||
|
let cm = |v: f64| (v * 50.0).round() as i64;
|
||||||
|
(cm(x), cm(y), cm(z))
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let nokkel = (
|
||||||
|
konstruksjon(&navn_av(e, r, if dor { "Dør" } else { "Vindu" })),
|
||||||
|
sted,
|
||||||
|
(areal * 100.0).round() as i64,
|
||||||
|
);
|
||||||
|
if !sette_vinduer.insert(nokkel) {
|
||||||
|
vindusdubletter += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let asimut = vert
|
let asimut = vert
|
||||||
.and_then(|v| asimut_for_vegg.get(v))
|
.and_then(|v| asimut_for_vegg.get(v))
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
|
// Uten U-verdi i modellen lar vi feltet stå tomt, slik at beregningen bruker en
|
||||||
uten_uverdi += 1;
|
// typisk verdi for byggeåret i stedet for en fast standardverdi.
|
||||||
if dor {
|
let u = u_verdi(&egenskaper, r);
|
||||||
2.0
|
if u.is_none() {
|
||||||
} else {
|
uten_uverdi.push(navn_av(e, r, if dor { "Dør" } else { "Vindu" }));
|
||||||
2.4
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
vinduer.push(Vindu {
|
vinduer.push(Vindu {
|
||||||
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
|
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
|
||||||
areal,
|
areal,
|
||||||
@@ -398,6 +515,10 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tilleggsfiler og scenarioer viser til flatene med navn, så navnene må være entydige.
|
||||||
|
unike_navn(&mut flater);
|
||||||
|
unike_navn(&mut kjellerflater);
|
||||||
|
|
||||||
// ── merknader ───────────────────────────────────────────────────
|
// ── merknader ───────────────────────────────────────────────────
|
||||||
if bra <= 0.0 {
|
if bra <= 0.0 {
|
||||||
bra = flater
|
bra = flater
|
||||||
@@ -416,9 +537,69 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if uten_uverdi > 0 {
|
if !utelatt.is_empty() {
|
||||||
|
// Gruppert per konstruksjon, slik at listen er til å lese.
|
||||||
|
let mut grupper: Vec<(String, usize, f64)> = Vec::new();
|
||||||
|
for (navn, areal, _) in &utelatt {
|
||||||
|
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||||||
|
match grupper.iter_mut().find(|(n, _, _)| *n == kort) {
|
||||||
|
Some((_, antall, sum)) => {
|
||||||
|
*antall += 1;
|
||||||
|
*sum += areal;
|
||||||
|
}
|
||||||
|
None => grupper.push((kort, 1, *areal)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grupper.sort_by(|a, b| b.2.total_cmp(&a.2));
|
||||||
|
let liste: Vec<String> = grupper
|
||||||
|
.iter()
|
||||||
|
.take(10)
|
||||||
|
.map(|(navn, antall, sum)| {
|
||||||
|
if *antall > 1 {
|
||||||
|
format!("{navn} ×{antall} ({sum:.0} m²)")
|
||||||
|
} else {
|
||||||
|
format!("{navn} ({sum:.0} m²)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
merknader.push(format!(
|
merknader.push(format!(
|
||||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
|
"{} bygningsdeler er ikke regnet med i klimaskjermen, som innervegger og dekker inne i bygningen: {}{}.",
|
||||||
|
utelatt.len(),
|
||||||
|
liste.join(", "),
|
||||||
|
if grupper.len() > liste.len() { " …" } else { "" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if dubletter + vindusdubletter > 0 {
|
||||||
|
merknader.push(format!(
|
||||||
|
"{} bygningsdeler ligger dobbelt i modellen, samme konstruksjon på samme sted, og er telt én gang. Det skjer når eksporten legger de samme delene i flere etasjer.",
|
||||||
|
dubletter + vindusdubletter
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !uten_uverdi.is_empty() {
|
||||||
|
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
|
||||||
|
// konstruksjon nevnes én gang, med antallet.
|
||||||
|
let mut antall: Vec<(String, usize)> = Vec::new();
|
||||||
|
for navn in &uten_uverdi {
|
||||||
|
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||||||
|
match antall.iter_mut().find(|(n, _)| *n == kort) {
|
||||||
|
Some((_, a)) => *a += 1,
|
||||||
|
None => antall.push((kort, 1)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let liste: Vec<String> = antall
|
||||||
|
.iter()
|
||||||
|
.map(|(n, a)| {
|
||||||
|
if *a > 1 {
|
||||||
|
format!("{n} ×{a}")
|
||||||
|
} else {
|
||||||
|
n.clone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
merknader.push(format!(
|
||||||
|
"{} bygningsdeler mangler U-verdi i IFC-filen: {}. Legg inn U-verdier i modellen, eller la enok bruke typiske verdier for byggeåret.",
|
||||||
|
uten_uverdi.len(),
|
||||||
|
liste.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
merknader.push(
|
merknader.push(
|
||||||
@@ -512,7 +693,13 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
rom.join(", ")
|
rom.join(", ")
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
let grunnmur = if terreng.is_some() {
|
let grunnmur = if terreng_over_gulvet {
|
||||||
|
format!(
|
||||||
|
"Terrengmodellen ligger over gulvet i første etasje, så den er ikke brukt. \
|
||||||
|
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Legg \
|
||||||
|
terrengmodellen i bakkenivå, eller sett `over_terreng` i meter på veggene."
|
||||||
|
)
|
||||||
|
} else if terreng.is_some() {
|
||||||
format!(
|
format!(
|
||||||
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
||||||
{:.2} m under.",
|
{:.2} m under.",
|
||||||
@@ -571,6 +758,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
vinduer.sort_by(|a, b| a.navn.cmp(&b.navn));
|
||||||
|
|
||||||
let bygning = Bygning {
|
let bygning = Bygning {
|
||||||
bygning: BygningInfo {
|
bygning: BygningInfo {
|
||||||
navn,
|
navn,
|
||||||
@@ -644,6 +833,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport {
|
Ok(Importrapport {
|
||||||
bygning,
|
bygning,
|
||||||
@@ -668,9 +858,13 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
|||||||
for f in flater.drain(..) {
|
for f in flater.drain(..) {
|
||||||
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
||||||
Some(g) => {
|
Some(g) => {
|
||||||
// Arealvektet himmelretning, slik at summen peker riktig vei.
|
// Arealvektet himmelretning og helning, slik at summen peker riktig vei og ikke
|
||||||
|
// avhenger av rekkefølgen elementene kommer i.
|
||||||
let sum = g.areal + f.areal;
|
let sum = g.areal + f.areal;
|
||||||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||||||
|
if let (Some(a), Some(b)) = (g.helning, f.helning) {
|
||||||
|
g.helning = Some((a * g.areal + b * f.areal) / sum);
|
||||||
|
}
|
||||||
g.areal = sum;
|
g.areal = sum;
|
||||||
}
|
}
|
||||||
None => samlet.push(f),
|
None => samlet.push(f),
|
||||||
@@ -689,6 +883,36 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
|||||||
*flater = samlet;
|
*flater = samlet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gjør flatenavnene entydige. Vegger får himmelretningen etter konstruksjonsnavnet, og andre
|
||||||
|
/// flater som fortsatt deler navn, nummereres.
|
||||||
|
fn unike_navn(flater: &mut [Flate]) {
|
||||||
|
// Fast rekkefølge, slik at samme modell alltid gir samme fil og samme nummerering.
|
||||||
|
flater.sort_by(|a, b| {
|
||||||
|
(a.typ as u8, &a.navn)
|
||||||
|
.cmp(&(b.typ as u8, &b.navn))
|
||||||
|
.then(a.orientering.total_cmp(&b.orientering))
|
||||||
|
.then(b.areal.total_cmp(&a.areal))
|
||||||
|
});
|
||||||
|
for f in flater.iter_mut().filter(|f| f.typ == FlateType::Vegg) {
|
||||||
|
f.navn = format!("{}, {}", f.navn, himmelretning(f.orientering));
|
||||||
|
}
|
||||||
|
let mut antall: HashMap<String, usize> = HashMap::new();
|
||||||
|
for f in flater.iter_mut() {
|
||||||
|
let n = antall.entry(f.navn.clone()).or_insert(0);
|
||||||
|
*n += 1;
|
||||||
|
if *n > 1 {
|
||||||
|
f.navn = format!("{} ({n})", f.navn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn himmelretning(grader: f64) -> &'static str {
|
||||||
|
const RETNINGER: [&str; 8] = [
|
||||||
|
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||||||
|
];
|
||||||
|
RETNINGER[((grader / 45.0).round() as i64).rem_euclid(8) as usize]
|
||||||
|
}
|
||||||
|
|
||||||
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
|
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Etasje {
|
pub struct Etasje {
|
||||||
@@ -1025,6 +1249,8 @@ struct Del {
|
|||||||
plass: Option<(f64, f64, f64, f64, f64)>,
|
plass: Option<(f64, f64, f64, f64, f64)>,
|
||||||
areal: f64,
|
areal: f64,
|
||||||
normal: Option<[f64; 3]>,
|
normal: Option<[f64; 3]>,
|
||||||
|
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
||||||
|
z: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
||||||
@@ -1150,7 +1376,10 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
|||||||
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
||||||
|
|
||||||
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
||||||
if vannrett > 0.2 {
|
// Et skrått tak har også en vannrett komponent, men skal alltid ha normalen oppover;
|
||||||
|
// ellers kan undersiden bli hovedflaten og gi helning 180° minus takvinkelen.
|
||||||
|
let skraa_plate = PLATETYPER.contains(&d.typ.as_str()) && g[2].abs() > 0.1;
|
||||||
|
if vannrett > 0.2 && !skraa_plate {
|
||||||
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
||||||
if let Some((x, y, _, _, _)) = d.plass {
|
if let Some((x, y, _, _, _)) = d.plass {
|
||||||
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
||||||
@@ -1169,12 +1398,13 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
|||||||
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
||||||
/// Etasjeinndelingen som klassifiseringen trenger.
|
/// Etasjeinndelingen som klassifiseringen trenger.
|
||||||
struct Etasjekontekst {
|
struct Etasjekontekst {
|
||||||
/// Nederste etasje i modellen.
|
|
||||||
nederste: Option<usize>,
|
|
||||||
/// Etasjen under terreng, dersom modellen har en.
|
/// Etasjen under terreng, dersom modellen har en.
|
||||||
kjeller: Option<usize>,
|
kjeller: Option<usize>,
|
||||||
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
||||||
forste_over: Option<usize>,
|
forste_over: Option<usize>,
|
||||||
|
/// Høyden på den etasjen, m. Geometrien veier tyngre enn etasjetilknytningen, som kan
|
||||||
|
/// være feil i eksporten.
|
||||||
|
forste_over_hoyde: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn klassifiser(
|
fn klassifiser(
|
||||||
@@ -1209,21 +1439,29 @@ fn klassifiser(
|
|||||||
if d.ytre == Some(true) {
|
if d.ytre == Some(true) {
|
||||||
return Some((FlateType::Gulv, Mot::Ute));
|
return Some((FlateType::Gulv, Mot::Ute));
|
||||||
}
|
}
|
||||||
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
|
// Mot grunnen i det nederste planet, eller når navnet sier det. Høyden av geometrien
|
||||||
let nederst = match (d.etasje, e.nederste) {
|
// avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
||||||
(Some(a), Some(b)) => a == b,
|
let paa_grunnen = [
|
||||||
_ => d
|
"kjellergulv",
|
||||||
.plass
|
"on grade",
|
||||||
.map(|p| (p.2 - laveste).abs() < 0.5)
|
"på grunn",
|
||||||
.unwrap_or(false),
|
"paa grunn",
|
||||||
};
|
"såle",
|
||||||
if nederst || navn.contains("kjellergulv") {
|
"sale",
|
||||||
|
];
|
||||||
|
if (d.z - laveste).abs() < 0.5 || paa_grunnen.iter().any(|o| navn.contains(o)) {
|
||||||
return Some((FlateType::Gulv, Mot::Grunn));
|
return Some((FlateType::Gulv, Mot::Grunn));
|
||||||
}
|
}
|
||||||
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
|
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
|
||||||
if e.kjeller.is_some() && d.etasje.is_some() && d.etasje == e.forste_over {
|
if e.kjeller.is_some() {
|
||||||
|
let ved_forste_over = e
|
||||||
|
.forste_over_hoyde
|
||||||
|
.map(|h| (d.z - h).abs() < 0.6)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if ved_forste_over || (d.etasje.is_some() && d.etasje == e.forste_over) {
|
||||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1590,6 +1828,11 @@ mod tester {
|
|||||||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
||||||
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||||||
fn prove() -> String {
|
fn prove() -> String {
|
||||||
|
prove_med(&[(100, "Sorvegg", -5.0), (120, "Nordvegg", 5.0)])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Som `prove`, men med de veggene testen ber om: (entitetsnummer, navn, y i meter).
|
||||||
|
fn prove_med(vegger: &[(u64, &str, f64)]) -> String {
|
||||||
let mut s = String::from(
|
let mut s = String::from(
|
||||||
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
||||||
);
|
);
|
||||||
@@ -1639,8 +1882,9 @@ mod tester {
|
|||||||
p + 8
|
p + 8
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
vegg(100, "Sorvegg", -5.0);
|
for (nr, navn, y) in vegger {
|
||||||
vegg(120, "Nordvegg", 5.0);
|
vegg(*nr, navn, *y);
|
||||||
|
}
|
||||||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -1686,6 +1930,45 @@ mod tester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn samme_vegg_i_flere_etasjer_telles_en_gang() {
|
||||||
|
// Den samme veggen på nøyaktig samme sted to ganger, slik en eksport gjør når den
|
||||||
|
// legger de samme delene i flere etasjer.
|
||||||
|
let tekst = prove_med(&[
|
||||||
|
(100, "Sorvegg", -5.0),
|
||||||
|
(120, "Nordvegg", 5.0),
|
||||||
|
(140, "Sorvegg", -5.0),
|
||||||
|
]);
|
||||||
|
let r = fra_tekst(&tekst, "prove").unwrap();
|
||||||
|
assert_eq!(r.bygning.flate.len(), 2, "flater: {:?}", r.bygning.flate);
|
||||||
|
assert!(
|
||||||
|
r.merknader.iter().any(|m| m.contains("dobbelt i modellen")),
|
||||||
|
"{:?}",
|
||||||
|
r.merknader
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vegger_faar_himmelretning_i_navnet() {
|
||||||
|
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||||
|
let mut navn: Vec<&str> = r.bygning.flate.iter().map(|f| f.navn.as_str()).collect();
|
||||||
|
navn.sort();
|
||||||
|
assert_eq!(navn, vec!["Nordvegg, nord", "Sorvegg, sør"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uoppvarmede_rom_fra_tilleggsfilen_holdes_utenfor_bra() {
|
||||||
|
let valg = Importvalg {
|
||||||
|
uoppvarmede_rom: vec!["STUE".into()],
|
||||||
|
};
|
||||||
|
let r = fra_tekst_med(&prove(), "prove", &valg).unwrap();
|
||||||
|
assert_ne!(r.bygning.bygning.oppvarmet_bra, 100.0);
|
||||||
|
assert!(r
|
||||||
|
.merknader
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.contains("uoppvarmet etter tilleggsfilen")));
|
||||||
|
}
|
||||||
|
|
||||||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||||||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1744,21 +2027,21 @@ mod tester {
|
|||||||
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() {
|
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() {
|
||||||
let t = prove().replace(
|
let t = prove().replace(
|
||||||
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
||||||
"#501= IFCPOSTALADDRESS($,$,$,$,('Hob\\X\\F8lveien 6'),$,'Tomter',$,'1825','Norge');\n\
|
"#501= IFCPOSTALADDRESS($,$,$,$,('Firmaveien 1'),$,'Firmaby',$,'9999','Norge');\n\
|
||||||
#502= IFCORGANIZATION('976015711','Klingenberg Bygg',$,$,(#501));\n\
|
#502= IFCORGANIZATION('999999999','Eksempel AS',$,$,(#501));\n\
|
||||||
#503= IFCPERSON($,'Lynghaug','Fredrik',$,$,$,$,$);\n\
|
#503= IFCPERSON($,'Nordmann','Kari',$,$,$,$,$);\n\
|
||||||
#504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\
|
#504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\
|
||||||
#500= IFCPOSTALADDRESS($,$,$,$,('Nordhusstykket 5'),$,'Tomter',$,'1825','Norge');\n\
|
#500= IFCPOSTALADDRESS($,$,$,$,('Eksempelveien 12'),$,'Eksempelby',$,'1234','Norge');\n\
|
||||||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
||||||
);
|
);
|
||||||
let r = fra_tekst(&t, "prove").unwrap();
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
r.bygning.bygning.adresse.as_deref(),
|
r.bygning.bygning.adresse.as_deref(),
|
||||||
Some("Nordhusstykket 5, 1825 Tomter")
|
Some("Eksempelveien 12, 1234 Eksempelby")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
r.bygning.bygning.energiraadgiver.as_deref(),
|
r.bygning.bygning.energiraadgiver.as_deref(),
|
||||||
Some("Fredrik Lynghaug, Klingenberg Bygg")
|
Some("Kari Nordmann, Eksempel AS")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,11 +2055,11 @@ mod tester {
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);",
|
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);",
|
||||||
"#3= IFCPROJECT('p',$,'Nordhusstykket 5','Energianalyse',$,'Nordhusstykket 5','Eksisterende',(#40),#2);",
|
"#3= IFCPROJECT('p',$,'Eksempelhuset','Energianalyse',$,'Eksempelhuset','Eksisterende',(#40),#2);",
|
||||||
);
|
);
|
||||||
let r = fra_tekst(&t, "prove").unwrap();
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
assert_eq!(r.bygning.bygning.adresse, None);
|
assert_eq!(r.bygning.bygning.adresse, None);
|
||||||
assert_eq!(r.bygning.bygning.navn, "Nordhusstykket 5");
|
assert_eq!(r.bygning.bygning.navn, "Eksempelhuset");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+17
-1
@@ -27,6 +27,9 @@ pub struct Hovedflate {
|
|||||||
pub normal: [f64; 3],
|
pub normal: [f64; 3],
|
||||||
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
||||||
plan: [f64; 4],
|
plan: [f64; 4],
|
||||||
|
/// Midlere høyde over delens eget nullpunkt, m. Sier hvilken etasje delen ligger i når
|
||||||
|
/// plasseringen i filen ikke gjør det.
|
||||||
|
pub hoyde: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nett {
|
impl Nett {
|
||||||
@@ -95,10 +98,16 @@ impl Nett {
|
|||||||
let (normal, sum) = grupper
|
let (normal, sum) = grupper
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||||
|
let hoyde = if self.punkter.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.punkter.iter().map(|p| p[2]).sum::<f64>() / self.punkter.len() as f64 * skala
|
||||||
|
};
|
||||||
Some(Hovedflate {
|
Some(Hovedflate {
|
||||||
areal: sum / 2.0,
|
areal: sum / 2.0,
|
||||||
normal,
|
normal,
|
||||||
plan: self.utstrekning(normal, skala),
|
plan: self.utstrekning(normal, skala),
|
||||||
|
hoyde,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +182,16 @@ pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
|||||||
grupper.push(f);
|
grupper.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let areal = grupper.iter().map(|g| g.areal).sum();
|
let areal: f64 = grupper.iter().map(|g| g.areal).sum();
|
||||||
|
// Arealvektet høyde, slik at et sammensatt dekke havner der mesteparten av det ligger.
|
||||||
|
let hoyde = if areal > 0.0 {
|
||||||
|
grupper.iter().map(|g| g.hoyde * g.areal).sum::<f64>() / areal
|
||||||
|
} else {
|
||||||
|
grupper[0].hoyde
|
||||||
|
};
|
||||||
Some(Hovedflate {
|
Some(Hovedflate {
|
||||||
areal,
|
areal,
|
||||||
|
hoyde,
|
||||||
..grupper[0]
|
..grupper[0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -7,4 +7,6 @@ pub mod regneark;
|
|||||||
pub mod step;
|
pub mod step;
|
||||||
pub mod toml_inn;
|
pub mod toml_inn;
|
||||||
|
|
||||||
pub use toml_inn::{les_bygning, slaa_sammen_scenario};
|
pub use toml_inn::{
|
||||||
|
differanse, flett_tillegg, les_bygning, les_tillegg, slaa_sammen_scenario, Importvalg, Tillegg,
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||||
//! bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
//! bygning,Eldre enebolig,140,,,,smaahus,1948,
|
||||||
//! vegg,Yttervegg sør,32,1.0,180,90,ute,,
|
//! vegg,Yttervegg sør,32,1.0,180,90,ute,,
|
||||||
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
|
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
|
||||||
//! ```
|
//! ```
|
||||||
@@ -84,6 +84,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
|
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
|
||||||
"skolebygg" | "skole" => Kategori::Skolebygg,
|
"skolebygg" | "skole" => Kategori::Skolebygg,
|
||||||
"barnehage" => Kategori::Barnehage,
|
"barnehage" => Kategori::Barnehage,
|
||||||
|
"idrettsbygg" | "idrett" => Kategori::Idrettsbygg,
|
||||||
|
"kulturbygg" | "kultur" => Kategori::Kulturbygg,
|
||||||
annen => {
|
annen => {
|
||||||
merknader
|
merknader
|
||||||
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
|
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
|
||||||
@@ -151,7 +153,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
antall: tall(felt(8)).unwrap_or(1.0),
|
antall: tall(felt(8)).unwrap_or(1.0),
|
||||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||||
helning: tall(felt(5)).unwrap_or(90.0),
|
helning: tall(felt(5)).unwrap_or(90.0),
|
||||||
u_verdi: u,
|
u_verdi: Some(u),
|
||||||
g_verdi: tall(felt(7)),
|
g_verdi: tall(felt(7)),
|
||||||
karmandel: None,
|
karmandel: None,
|
||||||
solskjerming: None,
|
solskjerming: None,
|
||||||
@@ -213,6 +215,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport { bygning, merknader })
|
Ok(Importrapport { bygning, merknader })
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -3,7 +3,7 @@
|
|||||||
//! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen
|
//! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen
|
||||||
//! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet.
|
//! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Arg {
|
pub enum Arg {
|
||||||
@@ -58,7 +58,8 @@ impl Entitet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Fil {
|
pub struct Fil {
|
||||||
pub entiteter: HashMap<u64, Entitet>,
|
/// Sortert på entitetsnummer, slik at gjennomløp alltid kommer i samme rekkefølge.
|
||||||
|
pub entiteter: BTreeMap<u64, Entitet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fil {
|
impl Fil {
|
||||||
@@ -83,7 +84,7 @@ impl Fil {
|
|||||||
|
|
||||||
/// Deler en STEP-fil i entiteter.
|
/// Deler en STEP-fil i entiteter.
|
||||||
pub fn les(tekst: &str) -> Fil {
|
pub fn les(tekst: &str) -> Fil {
|
||||||
let mut entiteter = HashMap::new();
|
let mut entiteter = BTreeMap::new();
|
||||||
let mut setning = String::new();
|
let mut setning = String::new();
|
||||||
let mut i_streng = false;
|
let mut i_streng = false;
|
||||||
let mut i_data = false;
|
let mut i_data = false;
|
||||||
|
|||||||
+250
-6
@@ -1,8 +1,9 @@
|
|||||||
//! Leser `bygning.toml` og fletter inn scenarioendringer.
|
//! Leser `bygning.toml` og fletter inn scenarioendringer og tilleggsfiler.
|
||||||
|
|
||||||
use crate::modell::{Bygning, Scenario};
|
use crate::modell::{Bygning, Scenario};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
|
||||||
@@ -11,19 +12,165 @@ pub fn les_bygning(sti: &Path) -> Result<Bygning> {
|
|||||||
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Valg for selve importen, fra tabellen `[import]` i en tilleggsfil.
|
||||||
|
#[derive(Deserialize, Default, Clone, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Importvalg {
|
||||||
|
/// Rom som står uoppvarmet og holdes utenfor oppvarmet BRA. Et rom treffes når navnet
|
||||||
|
/// inneholder en av tekstene, uten hensyn til store og små bokstaver.
|
||||||
|
#[serde(default)]
|
||||||
|
pub uoppvarmede_rom: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// En tilleggsfil til en importert modell: det modellen ikke beskriver, som drift, varmeanlegg
|
||||||
|
/// og scenarioer, og rettelser til det den beskriver.
|
||||||
|
pub struct Tillegg {
|
||||||
|
pub endringer: Value,
|
||||||
|
pub import: Importvalg,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn les_tillegg(sti: &Path) -> Result<Tillegg> {
|
||||||
|
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||||
|
let mut endringer: Value =
|
||||||
|
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))?;
|
||||||
|
let import = match endringer.as_table_mut().and_then(|t| t.remove("import")) {
|
||||||
|
Some(v) => v
|
||||||
|
.try_into()
|
||||||
|
.with_context(|| format!("tolker [import] i {}", sti.display()))?,
|
||||||
|
None => Importvalg::default(),
|
||||||
|
};
|
||||||
|
Ok(Tillegg { endringer, import })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fletter en tilleggsfil inn i en importert bygning, etter de samme reglene som scenarioene.
|
||||||
|
///
|
||||||
|
/// Returnerer også navnemønstrene med `*` som ikke traff noen oppføring, slik at en tilleggsfil
|
||||||
|
/// som har gått ut av takt med modellen blir synlig.
|
||||||
|
pub fn flett_tillegg(b: &Bygning, tillegg: &Value) -> Result<(Bygning, Vec<String>)> {
|
||||||
|
let mut grunn = Value::try_from(b).context("serialiserer den importerte bygningen")?;
|
||||||
|
let mut bom = Vec::new();
|
||||||
|
flett(&mut grunn, tillegg, &mut bom);
|
||||||
|
let bygning = grunn.try_into().context(
|
||||||
|
"tilleggsfilen gir en ugyldig bygningsbeskrivelse. Et navn som ikke finnes i modellen, \
|
||||||
|
legges til som ny oppføring og må da være komplett",
|
||||||
|
)?;
|
||||||
|
Ok((bygning, bom))
|
||||||
|
}
|
||||||
|
|
||||||
/// Lager en ny bygning der scenarioets endringer er flettet inn.
|
/// Lager en ny bygning der scenarioets endringer er flettet inn.
|
||||||
///
|
///
|
||||||
/// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches
|
/// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches
|
||||||
/// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges
|
/// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges
|
||||||
/// til, og `slett = true` fjerner et element. Andre verdier erstattes.
|
/// til, og `slett = true` fjerner et element. Et navn med `*` er et mønster som treffer alle
|
||||||
|
/// oppføringer det passer på, for eksempel `"Yttervegg*"`. Andre verdier erstattes.
|
||||||
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
||||||
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
||||||
flett(&mut grunn, &s.endringer);
|
flett(&mut grunn, &s.endringer, &mut Vec::new());
|
||||||
grunn
|
grunn
|
||||||
.try_into()
|
.try_into()
|
||||||
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forskjellen mellom to bygningsbeskrivelser, som en tilleggsfil.
|
||||||
|
///
|
||||||
|
/// Resultatet er det minste som må flettes inn i `fra` for å få `til`: endrede og nye felter,
|
||||||
|
/// nye elementer i lister, og `slett = true` for elementer som bare finnes i `fra`. Fletting
|
||||||
|
/// kan ikke fjerne et enkeltfelt, så felter som bare finnes i `fra`, listes som merknader.
|
||||||
|
pub fn differanse(fra: &Value, til: &Value) -> (Value, Vec<String>) {
|
||||||
|
let mut merknader = Vec::new();
|
||||||
|
let v = diff_verdi(fra, til, "", &mut merknader).unwrap_or(Value::Table(Default::default()));
|
||||||
|
(v, merknader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_verdi(fra: &Value, til: &Value, sti: &str, merknader: &mut Vec<String>) -> Option<Value> {
|
||||||
|
if fra == til {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match (fra, til) {
|
||||||
|
(Value::Table(f), Value::Table(t)) => {
|
||||||
|
let mut ut = toml::map::Map::new();
|
||||||
|
for (k, v) in t {
|
||||||
|
let bane = if sti.is_empty() {
|
||||||
|
k.clone()
|
||||||
|
} else {
|
||||||
|
format!("{sti}.{k}")
|
||||||
|
};
|
||||||
|
match f.get(k) {
|
||||||
|
Some(gammel) => {
|
||||||
|
if let Some(d) = diff_verdi(gammel, v, &bane, merknader) {
|
||||||
|
ut.insert(k.clone(), d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ut.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k in f.keys() {
|
||||||
|
if !t.contains_key(k) {
|
||||||
|
let bane = if sti.is_empty() {
|
||||||
|
k.clone()
|
||||||
|
} else {
|
||||||
|
format!("{sti}.{k}")
|
||||||
|
};
|
||||||
|
merknader.push(format!(
|
||||||
|
"«{bane}» finnes i modellen, men ikke i bygningsbeskrivelsen. En tilleggsfil kan ikke fjerne et felt."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(!ut.is_empty()).then_some(Value::Table(ut))
|
||||||
|
}
|
||||||
|
(Value::Array(f), Value::Array(t)) if t.iter().all(|e| navnenokkel(e).is_some()) => {
|
||||||
|
let mut ut: Vec<Value> = Vec::new();
|
||||||
|
for element in t {
|
||||||
|
let navn = navnenokkel(element).unwrap_or_default();
|
||||||
|
match f.iter().find(|e| navnenokkel(e).as_deref() == Some(&navn)) {
|
||||||
|
Some(gammel) => {
|
||||||
|
let bane = format!("{sti}.{navn}");
|
||||||
|
if let Some(Value::Table(mut endring)) =
|
||||||
|
diff_verdi(gammel, element, &bane, merknader)
|
||||||
|
{
|
||||||
|
endring.insert("navn".into(), Value::String(navn));
|
||||||
|
ut.push(Value::Table(endring));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ut.push(element.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for gammel in f {
|
||||||
|
let navn = navnenokkel(gammel).unwrap_or_default();
|
||||||
|
if !t.iter().any(|e| navnenokkel(e).as_deref() == Some(&navn)) {
|
||||||
|
let mut slett = toml::map::Map::new();
|
||||||
|
slett.insert("navn".into(), Value::String(navn));
|
||||||
|
slett.insert("slett".into(), Value::Boolean(true));
|
||||||
|
ut.push(Value::Table(slett));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(!ut.is_empty()).then_some(Value::Array(ut))
|
||||||
|
}
|
||||||
|
_ => Some(til.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst.
|
||||||
|
fn passer(moenster: &str, navn: &str) -> bool {
|
||||||
|
let deler: Vec<&str> = moenster.split('*').collect();
|
||||||
|
let (forst, sist) = (deler[0], deler[deler.len() - 1]);
|
||||||
|
if deler.len() == 1 {
|
||||||
|
return moenster == navn;
|
||||||
|
}
|
||||||
|
let Some(mut rest) = navn.strip_prefix(forst) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for del in &deler[1..deler.len() - 1] {
|
||||||
|
match rest.find(del) {
|
||||||
|
Some(i) => rest = &rest[i + del.len()..],
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rest.ends_with(sist)
|
||||||
|
}
|
||||||
|
|
||||||
fn navnenokkel(v: &Value) -> Option<String> {
|
fn navnenokkel(v: &Value) -> Option<String> {
|
||||||
let t = v.as_table()?;
|
let t = v.as_table()?;
|
||||||
t.get("navn")
|
t.get("navn")
|
||||||
@@ -39,7 +186,7 @@ fn skal_slettes(v: &Value) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flett(grunn: &mut Value, over: &Value) {
|
fn flett(grunn: &mut Value, over: &Value, bom: &mut Vec<String>) {
|
||||||
match (grunn, over) {
|
match (grunn, over) {
|
||||||
(Value::Table(g), Value::Table(o)) => {
|
(Value::Table(g), Value::Table(o)) => {
|
||||||
for (k, v) in o {
|
for (k, v) in o {
|
||||||
@@ -47,7 +194,7 @@ fn flett(grunn: &mut Value, over: &Value) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match g.get_mut(k) {
|
match g.get_mut(k) {
|
||||||
Some(eksisterende) => flett(eksisterende, v),
|
Some(eksisterende) => flett(eksisterende, v, bom),
|
||||||
None => {
|
None => {
|
||||||
g.insert(k.clone(), v.clone());
|
g.insert(k.clone(), v.clone());
|
||||||
}
|
}
|
||||||
@@ -57,6 +204,25 @@ fn flett(grunn: &mut Value, over: &Value) {
|
|||||||
(Value::Array(g), Value::Array(o)) => {
|
(Value::Array(g), Value::Array(o)) => {
|
||||||
for element in o {
|
for element in o {
|
||||||
match navnenokkel(element) {
|
match navnenokkel(element) {
|
||||||
|
Some(moenster) if moenster.contains('*') => {
|
||||||
|
let treffer =
|
||||||
|
|e: &Value| navnenokkel(e).is_some_and(|n| passer(&moenster, &n));
|
||||||
|
let antall = g.iter().filter(|e| treffer(e)).count();
|
||||||
|
if antall == 0 {
|
||||||
|
bom.push(moenster.clone());
|
||||||
|
} else if skal_slettes(element) {
|
||||||
|
g.retain(|e| !treffer(e));
|
||||||
|
} else {
|
||||||
|
// Mønsteret skal ikke erstatte navnene det treffer.
|
||||||
|
let mut endring = element.clone();
|
||||||
|
if let Some(t) = endring.as_table_mut() {
|
||||||
|
t.remove("navn");
|
||||||
|
}
|
||||||
|
for e in g.iter_mut().filter(|e| treffer(e)) {
|
||||||
|
flett(e, &endring, bom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(navn) => {
|
Some(navn) => {
|
||||||
let treff = g
|
let treff = g
|
||||||
.iter()
|
.iter()
|
||||||
@@ -65,7 +231,7 @@ fn flett(grunn: &mut Value, over: &Value) {
|
|||||||
Some(i) if skal_slettes(element) => {
|
Some(i) if skal_slettes(element) => {
|
||||||
g.remove(i);
|
g.remove(i);
|
||||||
}
|
}
|
||||||
Some(i) => flett(&mut g[i], element),
|
Some(i) => flett(&mut g[i], element, bom),
|
||||||
None if skal_slettes(element) => {}
|
None if skal_slettes(element) => {}
|
||||||
None => g.push(element.clone()),
|
None => g.push(element.clone()),
|
||||||
}
|
}
|
||||||
@@ -133,6 +299,84 @@ u_verdi = 0.8
|
|||||||
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
|
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moenster_i_navn_treffer_flere_oppforinger() {
|
||||||
|
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||||
|
let mut grunn = Value::try_from(&b).unwrap();
|
||||||
|
let mut bom = Vec::new();
|
||||||
|
let over: Value = toml::from_str(
|
||||||
|
r#"flate = [{ navn = "*", masseklasse = "lett" }, { navn = "Yttervegg*", u_verdi = 0.3 }, { navn = "Kjeller*", u_verdi = 0.2 }]"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
flett(&mut grunn, &over, &mut bom);
|
||||||
|
let ny: Bygning = grunn.try_into().unwrap();
|
||||||
|
assert_eq!(ny.flate.len(), 2);
|
||||||
|
assert_eq!(ny.flate[0].navn, "Yttervegg");
|
||||||
|
assert_eq!(ny.flate[0].u_verdi, Some(0.3));
|
||||||
|
assert_eq!(ny.flate[1].u_verdi, Some(0.8));
|
||||||
|
assert!(ny.flate.iter().all(|f| f.masseklasse.is_some()));
|
||||||
|
assert_eq!(bom, vec!["Kjeller*".to_string()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn differansen_gjenskaper_den_manuelle_filen() {
|
||||||
|
let importert: Bygning = toml::from_str(GRUNN).unwrap();
|
||||||
|
let manuell: Bygning = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[bygning]
|
||||||
|
navn = "Testhus"
|
||||||
|
kategori = "smaahus"
|
||||||
|
byggeaar = 1948
|
||||||
|
oppvarmet_bra = 100.0
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Yttervegg"
|
||||||
|
type = "vegg"
|
||||||
|
areal = 80.0
|
||||||
|
u_verdi = 0.22
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Kjellervegg"
|
||||||
|
type = "vegg"
|
||||||
|
areal = 30.0
|
||||||
|
u_verdi = 3.13
|
||||||
|
|
||||||
|
[[scenario]]
|
||||||
|
navn = "Nye vinduer"
|
||||||
|
endringer.flate = [{ navn = "Yttervegg", u_verdi = 0.18 }]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let (patch, merknader) = differanse(
|
||||||
|
&Value::try_from(&importert).unwrap(),
|
||||||
|
&Value::try_from(&manuell).unwrap(),
|
||||||
|
);
|
||||||
|
// Taket finnes bare i importen, og skal slettes av tillegget.
|
||||||
|
let tekst = toml::to_string(&patch).unwrap();
|
||||||
|
assert!(tekst.contains("slett"), "{tekst}");
|
||||||
|
assert!(merknader.is_empty(), "{merknader:?}");
|
||||||
|
|
||||||
|
let (gjenskapt, bom) = flett_tillegg(&importert, &patch).unwrap();
|
||||||
|
assert!(bom.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
Value::try_from(&gjenskapt).unwrap(),
|
||||||
|
Value::try_from(&manuell).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moenster_passer() {
|
||||||
|
assert!(passer("V-2*", "V-2 #3306"));
|
||||||
|
assert!(passer("*sør", "Yttervegg, sør"));
|
||||||
|
assert!(passer(
|
||||||
|
"Kjellermur*uisolert*",
|
||||||
|
"Kjellermur 30 cm uisolert, øst"
|
||||||
|
));
|
||||||
|
assert!(!passer("V-2*", "VD-2 #11162"));
|
||||||
|
assert!(passer("Tak", "Tak"));
|
||||||
|
assert!(!passer("Tak", "Taket"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_endrer_skalarfelt() {
|
fn scenario_endrer_skalarfelt() {
|
||||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||||
|
|||||||
+197
-24
@@ -31,6 +31,8 @@ enum Kommando {
|
|||||||
Beregn(Beregn),
|
Beregn(Beregn),
|
||||||
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
|
/// Importerer en bygningsmodell fra gbXML, IFC eller regneark og skriver bygning.toml.
|
||||||
Import(Import),
|
Import(Import),
|
||||||
|
/// Lager en tilleggsfil av forskjellen mellom en modell og en ferdig bygningsbeskrivelse.
|
||||||
|
Tillegg(TilleggAv),
|
||||||
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
|
/// Viser tilgjengelige klimasteder og nøkkeltall for ett sted.
|
||||||
Klima(Klima),
|
Klima(Klima),
|
||||||
/// Viser energikarakterskalaen for en bygningskategori.
|
/// Viser energikarakterskalaen for en bygningskategori.
|
||||||
@@ -56,15 +58,36 @@ struct Beregn {
|
|||||||
/// Beregner bare hovedalternativet, uten scenarioene i filen.
|
/// Beregner bare hovedalternativet, uten scenarioene i filen.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uten_scenarier: bool,
|
uten_scenarier: bool,
|
||||||
|
/// Tilleggsfil som flettes inn: det modellen ikke beskriver, og rettelser.
|
||||||
|
#[arg(short, long, value_name = "FIL")]
|
||||||
|
tillegg: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
struct Import {
|
struct Import {
|
||||||
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
|
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
|
||||||
fil: PathBuf,
|
fil: PathBuf,
|
||||||
|
/// Tilleggsfil som flettes inn i den importerte bygningen: det modellen ikke beskriver
|
||||||
|
/// (drift, varmeanlegg, scenarioer) og rettelser til det den beskriver.
|
||||||
|
#[arg(short, long, value_name = "FIL")]
|
||||||
|
tillegg: Option<PathBuf>,
|
||||||
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
||||||
#[arg(short, long, value_name = "FIL")]
|
#[arg(short, long, value_name = "FIL")]
|
||||||
ut: Option<PathBuf>,
|
ut: Option<PathBuf>,
|
||||||
|
/// Skriver over en utdatafil enok ikke har laget selv.
|
||||||
|
#[arg(long)]
|
||||||
|
overskriv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
struct TilleggAv {
|
||||||
|
/// BIM-modellen: .ifc, .gbxml eller .csv.
|
||||||
|
modell: PathBuf,
|
||||||
|
/// Bygningsbeskrivelsen med de manuelle endringene, som tillegget skal gjenskape.
|
||||||
|
bygning: PathBuf,
|
||||||
|
/// Utdatafil. Standard: prosjekt.toml ved siden av bygningsbeskrivelsen.
|
||||||
|
#[arg(short, long, value_name = "FIL")]
|
||||||
|
ut: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
@@ -93,19 +116,83 @@ fn main() -> Result<()> {
|
|||||||
match cli.kommando {
|
match cli.kommando {
|
||||||
Kommando::Beregn(a) => beregn(a, &kilde),
|
Kommando::Beregn(a) => beregn(a, &kilde),
|
||||||
Kommando::Import(a) => import(a),
|
Kommando::Import(a) => import(a),
|
||||||
|
Kommando::Tillegg(a) => tillegg(a),
|
||||||
Kommando::Klima(a) => klima(a),
|
Kommando::Klima(a) => klima(a),
|
||||||
Kommando::Skala(a) => skala(a, &kilde),
|
Kommando::Skala(a) => skala(a, &kilde),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Leser en bygningsbeskrivelse eller en BIM-modell, med en valgfri tilleggsfil flettet inn.
|
||||||
|
///
|
||||||
|
/// En `.toml`-fil er bygningsbeskrivelsen selv; andre filtyper importeres. Returnerer også
|
||||||
|
/// merknadene fra importen, og om filen var en modell.
|
||||||
|
fn les_modell(
|
||||||
|
fil: &Path,
|
||||||
|
tillegg: Option<&Path>,
|
||||||
|
) -> Result<(enok::modell::Bygning, Vec<String>, bool)> {
|
||||||
|
let endelse = fil
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
let tillegg = tillegg.map(inndata::les_tillegg).transpose()?;
|
||||||
|
let valg = tillegg
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| t.import.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (bygning, mut merknader, modell) = match endelse.as_str() {
|
||||||
|
"toml" => (inndata::les_bygning(fil)?, Vec::new(), false),
|
||||||
|
"gbxml" | "xml" => {
|
||||||
|
let r = inndata::gbxml::les(fil)?;
|
||||||
|
(r.bygning, r.merknader, true)
|
||||||
|
}
|
||||||
|
"ifc" => {
|
||||||
|
let r = inndata::ifc::les_med(fil, &valg)?;
|
||||||
|
(r.bygning, r.merknader, true)
|
||||||
|
}
|
||||||
|
"csv" => {
|
||||||
|
let r = inndata::regneark::les(fil)?;
|
||||||
|
(r.bygning, r.merknader, true)
|
||||||
|
}
|
||||||
|
annen => bail!(
|
||||||
|
"ukjent filtype «{annen}». enok leser bygning.toml og importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
|
||||||
|
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
|
||||||
|
}
|
||||||
|
let Some(t) = tillegg else {
|
||||||
|
return Ok((bygning, merknader, modell));
|
||||||
|
};
|
||||||
|
let (bygning, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
|
||||||
|
for m in bom {
|
||||||
|
merknader.push(format!(
|
||||||
|
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok((bygning, merknader, modell))
|
||||||
|
}
|
||||||
|
|
||||||
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
|
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
|
||||||
let bygning = inndata::les_bygning(&a.fil)?;
|
let (bygning, merknader, modell) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
||||||
|
for m in &merknader {
|
||||||
|
eprintln!(" · {m}");
|
||||||
|
}
|
||||||
for m in enok::modell::kontroll::kontroller(&bygning) {
|
for m in enok::modell::kontroll::kontroller(&bygning) {
|
||||||
eprintln!(" ! {}: {}", m.felt, m.melding);
|
eprintln!(" ! {}: {}", m.felt, m.melding);
|
||||||
}
|
}
|
||||||
let ut =
|
let ut =
|
||||||
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
|
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
|
||||||
std::fs::create_dir_all(&ut)?;
|
std::fs::create_dir_all(&ut)?;
|
||||||
|
if modell {
|
||||||
|
// Bygningsbeskrivelsen modellen ga, skrives med, slik at beregningen kan etterprøves.
|
||||||
|
let sti = ut.join("bygning.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&sti,
|
||||||
|
skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?,
|
||||||
|
)?;
|
||||||
|
eprintln!("Bygningsbeskrivelsen fra modellen: {}", sti.display());
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!("Henter klimadata …");
|
eprintln!("Henter klimadata …");
|
||||||
let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?;
|
let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?;
|
||||||
@@ -210,39 +297,125 @@ fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)])
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GENERERT: &str = "# Generert av enok";
|
||||||
|
|
||||||
fn import(a: Import) -> Result<()> {
|
fn import(a: Import) -> Result<()> {
|
||||||
let endelse = a
|
let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
|
||||||
.fil
|
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
||||||
.extension()
|
// En fil som ikke er skrevet av enok, kan være redigert for hånd. Da er det bedre å
|
||||||
.and_then(|e| e.to_str())
|
// stoppe enn å skrive over arbeidet.
|
||||||
.unwrap_or("")
|
if !a.overskriv && ut.exists() {
|
||||||
.to_lowercase();
|
let gammel = std::fs::read_to_string(&ut).unwrap_or_default();
|
||||||
let (bygning, merknader) = match endelse.as_str() {
|
if !gammel.starts_with(GENERERT) {
|
||||||
"gbxml" | "xml" => {
|
bail!(
|
||||||
let r = inndata::gbxml::les(&a.fil)?;
|
"{} er ikke laget av enok og kan være redigert for hånd. \n\
|
||||||
(r.bygning, r.merknader)
|
Ta vare på endringene som en tilleggsfil med «enok tillegg {} {} --ut prosjekt.toml», \n\
|
||||||
|
og importer med «--tillegg prosjekt.toml». Bruk --overskriv for å skrive over.",
|
||||||
|
ut.display(),
|
||||||
|
a.fil.display(),
|
||||||
|
ut.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
"ifc" => {
|
|
||||||
let r = inndata::ifc::les(&a.fil)?;
|
|
||||||
(r.bygning, r.merknader)
|
|
||||||
}
|
}
|
||||||
"csv" => {
|
let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
|
||||||
let r = inndata::regneark::les(&a.fil)?;
|
std::fs::write(&ut, innhold)?;
|
||||||
(r.bygning, r.merknader)
|
println!("Skrev {}", ut.display());
|
||||||
|
for m in &merknader {
|
||||||
|
println!(" • {m}");
|
||||||
}
|
}
|
||||||
annen => bail!(
|
Ok(())
|
||||||
"ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
|
}
|
||||||
|
|
||||||
|
/// Bygningsbeskrivelsen som TOML, med et hode som sier hvor den kommer fra.
|
||||||
|
fn skriv_bygning(
|
||||||
|
bygning: &enok::modell::Bygning,
|
||||||
|
merknader: &[String],
|
||||||
|
fil: &Path,
|
||||||
|
tillegg: Option<&Path>,
|
||||||
|
) -> Result<String> {
|
||||||
|
let tekst = toml::to_string_pretty(bygning).context("serialiserer bygningen til TOML")?;
|
||||||
|
let mut innhold = match tillegg {
|
||||||
|
Some(t) => format!(
|
||||||
|
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
|
||||||
|
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
|
||||||
|
# Merknader fra importen:\n",
|
||||||
|
fil.display(),
|
||||||
|
t.display()
|
||||||
|
),
|
||||||
|
None => format!(
|
||||||
|
"# Generert av enok fra «{}».\n\
|
||||||
|
# Merknader fra importen:\n",
|
||||||
|
fil.display()
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
for m in merknader {
|
||||||
let tekst = toml::to_string_pretty(&bygning).context("serialiserer bygningen til TOML")?;
|
|
||||||
let mut innhold =
|
|
||||||
String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n");
|
|
||||||
for m in &merknader {
|
|
||||||
innhold.push_str(&format!("# {m}\n"));
|
innhold.push_str(&format!("# {m}\n"));
|
||||||
}
|
}
|
||||||
innhold.push('\n');
|
innhold.push('\n');
|
||||||
innhold.push_str(&tekst);
|
innhold.push_str(&tekst);
|
||||||
|
Ok(innhold)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skriver forskjellen mellom modellen og en ferdig bygningsbeskrivelse som en tilleggsfil.
|
||||||
|
fn tillegg(a: TilleggAv) -> Result<()> {
|
||||||
|
let (importert, _, modell) = les_modell(&a.modell, None)?;
|
||||||
|
if !modell {
|
||||||
|
bail!("{} er ikke en BIM-modell", a.modell.display());
|
||||||
|
}
|
||||||
|
let manuell = inndata::les_bygning(&a.bygning)?;
|
||||||
|
let fra = toml::Value::try_from(&importert).context("serialiserer den importerte bygningen")?;
|
||||||
|
let til = toml::Value::try_from(&manuell).context("serialiserer bygningsbeskrivelsen")?;
|
||||||
|
let (patch, detaljer) = inndata::differanse(&fra, &til);
|
||||||
|
|
||||||
|
// Felter modellen har og filen ikke, kan et tillegg ikke fjerne. De er som regel
|
||||||
|
// standardverdier importen fyller ut, så de samles i én merknad i stedet for én hver.
|
||||||
|
let (ufjernbare, mut merknader): (Vec<String>, Vec<String>) = detaljer
|
||||||
|
.into_iter()
|
||||||
|
.partition(|m| m.contains("finnes i modellen"));
|
||||||
|
if !ufjernbare.is_empty() {
|
||||||
|
let felter: Vec<&str> = ufjernbare
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| m.split('«').nth(1)?.split('»').next())
|
||||||
|
.collect();
|
||||||
|
merknader.push(format!(
|
||||||
|
"{} felter finnes i modellen, men ikke i bygningsbeskrivelsen, og blir stående: {}{}. En tilleggsfil kan ikke fjerne et felt.",
|
||||||
|
felter.len(),
|
||||||
|
felter.iter().take(3).copied().collect::<Vec<_>>().join(", "),
|
||||||
|
if felter.len() > 3 { " …" } else { "" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontroller at tillegget faktisk gjenskaper filen det er laget av.
|
||||||
|
let (gjenskapt, bom) = inndata::flett_tillegg(&importert, &patch)?;
|
||||||
|
merknader.extend(
|
||||||
|
bom.into_iter()
|
||||||
|
.map(|m| format!("Navnemønsteret «{m}» traff ingenting.")),
|
||||||
|
);
|
||||||
|
if toml::Value::try_from(&gjenskapt).ok().as_ref() != Some(&til) {
|
||||||
|
merknader.push(
|
||||||
|
"Tillegget gjenskaper ikke bygningsbeskrivelsen helt. Gå gjennom filen før den brukes."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ut =
|
||||||
|
a.ut.unwrap_or_else(|| a.bygning.with_file_name("prosjekt.toml"));
|
||||||
|
let mut innhold = format!(
|
||||||
|
"# Tillegg til «{}», laget av forskjellen mot «{}».\n\
|
||||||
|
# Alt som ikke står i modellen, ligger her, og flettes inn ved hver import:\n\
|
||||||
|
# enok beregn \"{}\" --tillegg {}\n",
|
||||||
|
a.modell.display(),
|
||||||
|
a.bygning.display(),
|
||||||
|
a.modell.display(),
|
||||||
|
ut.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("prosjekt.toml")
|
||||||
|
);
|
||||||
|
for m in &merknader {
|
||||||
|
innhold.push_str(&format!("# {m}\n"));
|
||||||
|
}
|
||||||
|
innhold.push('\n');
|
||||||
|
innhold.push_str(&toml::to_string_pretty(&patch).context("serialiserer tillegget")?);
|
||||||
std::fs::write(&ut, innhold)?;
|
std::fs::write(&ut, innhold)?;
|
||||||
println!("Skrev {}", ut.display());
|
println!("Skrev {}", ut.display());
|
||||||
for m in &merknader {
|
for m in &merknader {
|
||||||
|
|||||||
+64
-14
@@ -130,6 +130,8 @@ pub struct LostVentilasjon {
|
|||||||
pub sfp: f64,
|
pub sfp: f64,
|
||||||
pub varmebatteri: bool,
|
pub varmebatteri: bool,
|
||||||
pub tilluft_settpunkt: f64,
|
pub tilluft_settpunkt: f64,
|
||||||
|
/// Kjølebatteri: tilluften holdes på denne temperaturen i driftstiden.
|
||||||
|
pub tilluft_kjoling: Option<f64>,
|
||||||
pub driftstid: crate::data::Tidsrom,
|
pub driftstid: crate::data::Tidsrom,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +144,15 @@ pub struct LostLaster {
|
|||||||
pub personer: [f64; 24],
|
pub personer: [f64; 24],
|
||||||
pub varmtvann: [f64; 24],
|
pub varmtvann: [f64; 24],
|
||||||
pub pumper: [f64; 24],
|
pub pumper: [f64; 24],
|
||||||
pub oppvarming_driftstid: crate::data::Tidsrom,
|
/// Driftstider med driftsdager og driftsuker. Profilene over gjelder bare på driftsdager.
|
||||||
|
pub driftstider: crate::data::Driftstider,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LostLaster {
|
||||||
|
/// Om time `t` i året faller på en driftsdag.
|
||||||
|
pub fn driftsdag(&self, t: usize) -> bool {
|
||||||
|
self.driftstider.driftsdag(t / 24)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const R_SI: f64 = 0.13;
|
const R_SI: f64 = 0.13;
|
||||||
@@ -208,7 +218,7 @@ impl Lost {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
for v in &b.vindu {
|
for v in &b.vindu {
|
||||||
flater.push(los_vindu(v, &mut f));
|
flater.push(los_vindu(v, periode, &mut f));
|
||||||
}
|
}
|
||||||
if flater.is_empty() {
|
if flater.is_empty() {
|
||||||
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
|
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
|
||||||
@@ -326,7 +336,10 @@ impl Lost {
|
|||||||
let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0);
|
let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0);
|
||||||
|
|
||||||
// Vinduslufting brukes når bygningen ikke har kjøleanlegg.
|
// Vinduslufting brukes når bygningen ikke har kjøleanlegg.
|
||||||
let lufting = if b.lufting.ingen || settpunkt_kjoling.is_some() {
|
let lufting = if b.lufting.ingen
|
||||||
|
|| settpunkt_kjoling.is_some()
|
||||||
|
|| ventilasjon.tilluft_kjoling.is_some()
|
||||||
|
{
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
// Uten oppgitt grense brukes den normerte settpunkttemperaturen for kjøling.
|
// Uten oppgitt grense brukes den normerte settpunkttemperaturen for kjøling.
|
||||||
@@ -633,8 +646,25 @@ fn los_flate(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
fn los_vindu(
|
||||||
|
v: &Vindu,
|
||||||
|
periode: Option<&crate::data::Periode>,
|
||||||
|
logg: &mut Vec<Forutsetning>,
|
||||||
|
) -> LostFlate {
|
||||||
let areal = v.areal * v.antall;
|
let areal = v.areal * v.antall;
|
||||||
|
// U-verdi: oppgitt, eller typisk for byggeperioden. Et vindu uten oppgitt U-verdi er
|
||||||
|
// vanligvis originalvinduet, og da sier byggeåret mer enn en fast standardverdi.
|
||||||
|
let u = v.u_verdi.unwrap_or_else(|| {
|
||||||
|
let antatt = periode.map(|p| p.vindu).unwrap_or(2.8);
|
||||||
|
logg.push(Forutsetning {
|
||||||
|
felt: format!("vindu.{}.u_verdi", v.navn),
|
||||||
|
verdi: format!("{antatt:.2} W/(m²K)"),
|
||||||
|
kilde: periode
|
||||||
|
.map(|p| format!("typisk for {}", p.navn))
|
||||||
|
.unwrap_or_else(|| "antatt".into()),
|
||||||
|
});
|
||||||
|
antatt
|
||||||
|
});
|
||||||
let karm = v.karmandel.unwrap_or_else(|| {
|
let karm = v.karmandel.unwrap_or_else(|| {
|
||||||
logg.push(Forutsetning {
|
logg.push(Forutsetning {
|
||||||
felt: format!("vindu.{}.karmandel", v.navn),
|
felt: format!("vindu.{}.karmandel", v.navn),
|
||||||
@@ -647,11 +677,11 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
|||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
v.g_verdi.unwrap_or_else(|| {
|
v.g_verdi.unwrap_or_else(|| {
|
||||||
let antatt = if v.u_verdi <= 1.2 { 0.50 } else { 0.75 };
|
let antatt = if u <= 1.2 { 0.50 } else { 0.75 };
|
||||||
logg.push(Forutsetning {
|
logg.push(Forutsetning {
|
||||||
felt: format!("vindu.{}.g_verdi", v.navn),
|
felt: format!("vindu.{}.g_verdi", v.navn),
|
||||||
verdi: format!("{antatt:.2}"),
|
verdi: format!("{antatt:.2}"),
|
||||||
kilde: if v.u_verdi <= 1.2 {
|
kilde: if u <= 1.2 {
|
||||||
"antatt 3-lags energiglass"
|
"antatt 3-lags energiglass"
|
||||||
} else {
|
} else {
|
||||||
"antatt 2-lags glass"
|
"antatt 2-lags glass"
|
||||||
@@ -675,8 +705,8 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
|||||||
navn: v.navn.clone(),
|
navn: v.navn.clone(),
|
||||||
typ: FlateType::Vegg,
|
typ: FlateType::Vegg,
|
||||||
areal,
|
areal,
|
||||||
u_verdi: v.u_verdi,
|
u_verdi: u,
|
||||||
u_konstruksjon: v.u_verdi,
|
u_konstruksjon: u,
|
||||||
helning: v.helning,
|
helning: v.helning,
|
||||||
orientering: v.orientering,
|
orientering: v.orientering,
|
||||||
kappa: 0.0,
|
kappa: 0.0,
|
||||||
@@ -724,6 +754,22 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
|
|||||||
let varmebatteri = v
|
let varmebatteri = v
|
||||||
.varmebatteri
|
.varmebatteri
|
||||||
.unwrap_or(matches!(v.typ, VentilasjonType::Balansert));
|
.unwrap_or(matches!(v.typ, VentilasjonType::Balansert));
|
||||||
|
// Kjølebatteriet holder konstant tilluftstemperatur i driftstiden, slik DiBKs modellbygg
|
||||||
|
// for energirammene er regnet (Multiconsult 2014, avsnitt 7.3).
|
||||||
|
let tilluft_kjoling = (v.kjolebatteri == Some(true)).then(|| {
|
||||||
|
v.tilluft_kjoling.unwrap_or_else(|| {
|
||||||
|
let t = n
|
||||||
|
.settpunkt
|
||||||
|
.ventilasjon_tilluft_kjoling
|
||||||
|
.unwrap_or(n.settpunkt.ventilasjon_tilluft);
|
||||||
|
logg.push(Forutsetning {
|
||||||
|
felt: "ventilasjon.tilluft_kjoling".into(),
|
||||||
|
verdi: format!("{t:.0} °C"),
|
||||||
|
kilde: format!("normert tilluftstemperatur, {}", n.kilde),
|
||||||
|
});
|
||||||
|
t
|
||||||
|
})
|
||||||
|
});
|
||||||
LostVentilasjon {
|
LostVentilasjon {
|
||||||
typ: v.typ,
|
typ: v.typ,
|
||||||
luftmengde,
|
luftmengde,
|
||||||
@@ -732,6 +778,7 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
|
|||||||
sfp,
|
sfp,
|
||||||
varmebatteri,
|
varmebatteri,
|
||||||
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
|
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
|
||||||
|
tilluft_kjoling,
|
||||||
driftstid: n.driftstid.ventilasjon,
|
driftstid: n.driftstid.ventilasjon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,9 +801,11 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
|
|||||||
logg.push(Forutsetning {
|
logg.push(Forutsetning {
|
||||||
felt: "belysning".into(),
|
felt: "belysning".into(),
|
||||||
verdi: format!(
|
verdi: format!(
|
||||||
"{:.2} W/m² i {:.0} h/døgn",
|
"{:.2} W/m² i {:.0} h/døgn, {} dager/uke, {} uker/år",
|
||||||
n.belysning.effekt,
|
n.belysning.effekt,
|
||||||
n.driftstid.belysning.timer()
|
n.driftstid.belysning.timer(),
|
||||||
|
n.driftstid.dager_per_uke,
|
||||||
|
n.driftstid.uker_per_aar
|
||||||
),
|
),
|
||||||
kilde: format!("normert, {}", n.kilde),
|
kilde: format!("normert, {}", n.kilde),
|
||||||
});
|
});
|
||||||
@@ -772,16 +821,17 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
|
|||||||
.drift
|
.drift
|
||||||
.personer_varmetilskudd
|
.personer_varmetilskudd
|
||||||
.unwrap_or(n.personer.varmetilskudd);
|
.unwrap_or(n.personer.varmetilskudd);
|
||||||
|
let driftsdager = n.driftstid.driftsdager_per_aar() as f64;
|
||||||
let tappevann = b.tappevann.energibehov.map(|kwh| {
|
let tappevann = b.tappevann.energibehov.map(|kwh| {
|
||||||
// kWh/(m²·år) → W/m² i driftstiden.
|
// kWh/(m²·år) → W/m² i driftstiden på driftsdager.
|
||||||
kwh * 1000.0 / (365.0 * n.driftstid.varmtvann.timer())
|
kwh * 1000.0 / (driftsdager * n.driftstid.varmtvann.timer())
|
||||||
});
|
});
|
||||||
let vv = tappevann.unwrap_or_else(|| {
|
let vv = tappevann.unwrap_or_else(|| {
|
||||||
logg.push(Forutsetning {
|
logg.push(Forutsetning {
|
||||||
felt: "tappevann.energibehov".into(),
|
felt: "tappevann.energibehov".into(),
|
||||||
verdi: format!(
|
verdi: format!(
|
||||||
"{:.1} kWh/(m²·år)",
|
"{:.1} kWh/(m²·år)",
|
||||||
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * 365.0 / 1000.0
|
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * driftsdager / 1000.0
|
||||||
),
|
),
|
||||||
kilde: format!("normert, {}", n.kilde),
|
kilde: format!("normert, {}", n.kilde),
|
||||||
});
|
});
|
||||||
@@ -801,6 +851,6 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
|
|||||||
personer: profil(personer, n.driftstid.personer, n.profil_personer),
|
personer: profil(personer, n.driftstid.personer, n.profil_personer),
|
||||||
varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann),
|
varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann),
|
||||||
pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None),
|
pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None),
|
||||||
oppvarming_driftstid: n.driftstid.oppvarming,
|
driftstider: n.driftstid.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-1
@@ -82,6 +82,26 @@ pub struct Bygning {
|
|||||||
pub drift: Drift,
|
pub drift: Drift,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub scenario: Vec<Scenario>,
|
pub scenario: Vec<Scenario>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub rapport: Rapport,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utseendet på rapporten, slik at den kan følge firmaets øvrige dokumenter.
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Rapport {
|
||||||
|
/// Logo på forsiden. Filen kopieres til utdatakatalogen ved siden av rapporten.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub logo: Option<PathBuf>,
|
||||||
|
/// Aksentfarge, for eksempel «#880000». Utelatt: enoks egen.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub farge: Option<String>,
|
||||||
|
/// Avsenderlinje nederst på hver side, for eksempel firma og organisasjonsnummer.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub avsender: Option<String>,
|
||||||
|
/// Skrift for overskrifter. Utelatt: enoks egen.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub skrift_overskrift: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
@@ -268,7 +288,9 @@ pub struct Vindu {
|
|||||||
pub orientering: f64,
|
pub orientering: f64,
|
||||||
#[serde(default = "nitti")]
|
#[serde(default = "nitti")]
|
||||||
pub helning: f64,
|
pub helning: f64,
|
||||||
pub u_verdi: f64,
|
/// U-verdi inkl. karm, W/(m²K). Utelatt: typisk for byggeåret.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub u_verdi: Option<f64>,
|
||||||
/// Solfaktor for glasset (g-verdi). Utelatt: 0,75 for 2-lags, 0,5 for 3-lags energiglass.
|
/// Solfaktor for glasset (g-verdi). Utelatt: 0,75 for 2-lags, 0,5 for 3-lags energiglass.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub g_verdi: Option<f64>,
|
pub g_verdi: Option<f64>,
|
||||||
@@ -397,6 +419,12 @@ pub struct Ventilasjon {
|
|||||||
/// Har anlegget varmebatteri som ettervarmer tilluften?
|
/// Har anlegget varmebatteri som ettervarmer tilluften?
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub varmebatteri: Option<bool>,
|
pub varmebatteri: Option<bool>,
|
||||||
|
/// Har anlegget kjølebatteri? Tilluften holdes da på `tilluft_kjoling` i driftstiden.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub kjolebatteri: Option<bool>,
|
||||||
|
/// Tilluftstemperatur med kjølebatteri, °C. Utelatt: normert.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tilluft_kjoling: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
|||||||
+22
-11
@@ -188,17 +188,20 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
|||||||
}
|
}
|
||||||
for t in timer {
|
for t in timer {
|
||||||
let klokke = t % 24;
|
let klokke = t % 24;
|
||||||
|
let driftsdag = l.laster.driftsdag(t);
|
||||||
let t_ute = v.t_ute[t];
|
let t_ute = v.t_ute[t];
|
||||||
let t_himmel = sol::himmeltemperatur(v, t);
|
let t_himmel = sol::himmeltemperatur(v, t);
|
||||||
|
|
||||||
// ── laster ──────────────────────────────────────────────
|
// ── laster ──────────────────────────────────────────────
|
||||||
let bel = l.laster.belysning[klokke] * l.bra;
|
// Lastprofilene gjelder driftsdager; på andre dager er bygningen ute av bruk.
|
||||||
let utstyr_el = l.laster.utstyr_effekt[klokke] * l.bra;
|
let skala = if driftsdag { l.bra } else { 0.0 };
|
||||||
let utstyr_varme = l.laster.utstyr_varme[klokke] * l.bra;
|
let bel = l.laster.belysning[klokke] * skala;
|
||||||
let personer = l.laster.personer[klokke] * l.bra;
|
let utstyr_el = l.laster.utstyr_effekt[klokke] * skala;
|
||||||
let vv = l.laster.varmtvann[klokke] * l.bra;
|
let utstyr_varme = l.laster.utstyr_varme[klokke] * skala;
|
||||||
let pumper = l.laster.pumper[klokke] * l.bra;
|
let personer = l.laster.personer[klokke] * skala;
|
||||||
let vifter = ventilasjon::vifteeffekt(l, klokke);
|
let vv = l.laster.varmtvann[klokke] * skala;
|
||||||
|
let pumper = l.laster.pumper[klokke] * skala;
|
||||||
|
let vifter = ventilasjon::vifteeffekt(l, t);
|
||||||
// Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten).
|
// Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten).
|
||||||
let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter;
|
let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter;
|
||||||
|
|
||||||
@@ -211,21 +214,28 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── ventilasjon ─────────────────────────────────────────
|
// ── ventilasjon ─────────────────────────────────────────
|
||||||
let q_vent = ventilasjon::luftmengde(l, klokke);
|
let q_vent = ventilasjon::luftmengde(l, t);
|
||||||
let h_ve = q_vent * RHO_C_LUFT;
|
let h_ve = q_vent * RHO_C_LUFT;
|
||||||
let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige);
|
let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige);
|
||||||
let (t_sup, vent_varme) =
|
let kjol_tilluft = l
|
||||||
|
.ventilasjon
|
||||||
|
.tilluft_kjoling
|
||||||
|
.filter(|&k| ventilasjon::i_drift(l, t) && t_sup_hx > k);
|
||||||
|
let (t_sup, vent_varme, vent_kjol) =
|
||||||
if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt {
|
if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt {
|
||||||
(
|
(
|
||||||
l.ventilasjon.tilluft_settpunkt,
|
l.ventilasjon.tilluft_settpunkt,
|
||||||
h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx),
|
h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx),
|
||||||
|
0.0,
|
||||||
)
|
)
|
||||||
|
} else if let Some(k) = kjol_tilluft {
|
||||||
|
(k, 0.0, h_ve * (t_sup_hx - k))
|
||||||
} else {
|
} else {
|
||||||
(t_sup_hx, 0.0)
|
(t_sup_hx, 0.0, 0.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── settpunkt ───────────────────────────────────────────
|
// ── settpunkt ───────────────────────────────────────────
|
||||||
let sett_varme = if l.laster.oppvarming_driftstid.aktiv(klokke) {
|
let sett_varme = if driftsdag && l.laster.driftstider.oppvarming.aktiv(klokke) {
|
||||||
l.settpunkt_oppvarming
|
l.settpunkt_oppvarming
|
||||||
} else {
|
} else {
|
||||||
l.settpunkt_natt
|
l.settpunkt_natt
|
||||||
@@ -339,6 +349,7 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
|
|||||||
ts.romoppvarming[t] = fi.max(0.0);
|
ts.romoppvarming[t] = fi.max(0.0);
|
||||||
ts.romkjoling[t] = (-fi).max(0.0);
|
ts.romkjoling[t] = (-fi).max(0.0);
|
||||||
ts.ventilasjonsvarme[t] = vent_varme;
|
ts.ventilasjonsvarme[t] = vent_varme;
|
||||||
|
ts.ventilasjonskjoling[t] = vent_kjol;
|
||||||
ts.varmtvann[t] = vv;
|
ts.varmtvann[t] = vv;
|
||||||
ts.vifter[t] = vifter;
|
ts.vifter[t] = vifter;
|
||||||
ts.pumper[t] = pumper;
|
ts.pumper[t] = pumper;
|
||||||
|
|||||||
@@ -33,9 +33,14 @@ pub fn infiltrasjonsluftmengde(l: &Lost) -> f64 {
|
|||||||
n_inf * v
|
n_inf * v
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ventilasjonsluftmengde for en klokketime, m³/h.
|
/// Om ventilasjonen går med driftsluftmengde i time `t` av året.
|
||||||
pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
|
pub fn i_drift(l: &Lost, t: usize) -> bool {
|
||||||
let spesifikk = if l.ventilasjon.driftstid.aktiv(klokketime) {
|
l.laster.driftsdag(t) && l.ventilasjon.driftstid.aktiv(t % 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ventilasjonsluftmengde i time `t` av året, m³/h.
|
||||||
|
pub fn luftmengde(l: &Lost, t: usize) -> f64 {
|
||||||
|
let spesifikk = if i_drift(l, t) {
|
||||||
l.ventilasjon.luftmengde
|
l.ventilasjon.luftmengde
|
||||||
} else {
|
} else {
|
||||||
l.ventilasjon.luftmengde_natt
|
l.ventilasjon.luftmengde_natt
|
||||||
@@ -44,18 +49,29 @@ pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C.
|
/// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C.
|
||||||
|
///
|
||||||
|
/// Gjenvinneren reguleres ned når den ellers ville varmet tilluften over settpunktet, slik
|
||||||
|
/// en roterende gjenvinner med turtallsregulering eller et omløpsspjeld gjør. Når det er
|
||||||
|
/// varmere ute enn inne, kjøler gjenvinneren tilluften og går for fullt.
|
||||||
pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 {
|
pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 {
|
||||||
match l.ventilasjon.typ {
|
match l.ventilasjon.typ {
|
||||||
VentilasjonType::Balansert => t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute),
|
VentilasjonType::Balansert => {
|
||||||
|
let gjenvunnet = t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute);
|
||||||
|
if gjenvunnet > t_ute {
|
||||||
|
gjenvunnet.min(l.ventilasjon.tilluft_settpunkt.max(t_ute))
|
||||||
|
} else {
|
||||||
|
gjenvunnet
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => t_ute,
|
_ => t_ute,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vifteeffekt for en klokketime, W.
|
/// Vifteeffekt i time `t` av året, W.
|
||||||
pub fn vifteeffekt(l: &Lost, klokketime: usize) -> f64 {
|
pub fn vifteeffekt(l: &Lost, t: usize) -> f64 {
|
||||||
if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) {
|
if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let q = luftmengde(l, klokketime) / 3600.0; // m³/s
|
let q = luftmengde(l, t) / 3600.0; // m³/s
|
||||||
l.ventilasjon.sfp * 1000.0 * q
|
l.ventilasjon.sfp * 1000.0 * q
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ pub fn kilde(r: &Resultat) -> String {
|
|||||||
l.settpunkt_kjoling.unwrap_or(0.0)
|
l.settpunkt_kjoling.unwrap_or(0.0)
|
||||||
);
|
);
|
||||||
let _ = writeln!(s, " <Driftstider>");
|
let _ = writeln!(s, " <Driftstider>");
|
||||||
let d = &l.laster.oppvarming_driftstid;
|
let d = &l.laster.driftstider.oppvarming;
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
s,
|
s,
|
||||||
" <Ventilasjon>{:.0}</Ventilasjon>",
|
" <Ventilasjon>{:.0}</Ventilasjon>",
|
||||||
|
|||||||
+88
-8
@@ -66,6 +66,12 @@ fn rad(celler: &[String]) -> String {
|
|||||||
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
||||||
std::fs::create_dir_all(katalog)?;
|
std::fs::create_dir_all(katalog)?;
|
||||||
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
||||||
|
// Logoen følger med, slik at rapporten kan kompileres for seg selv senere. Mangler
|
||||||
|
// filen, lages rapporten uten den.
|
||||||
|
if let (Some(logo), Some(navn)) = (&r.bygning.rapport.logo, logo_navn(r)) {
|
||||||
|
std::fs::copy(logo, katalog.join(navn))
|
||||||
|
.with_context(|| format!("kopierer logoen {}", logo.display()))?;
|
||||||
|
}
|
||||||
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
|
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-varighet.svg"), svg::varighetskurve(r))?;
|
||||||
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
||||||
@@ -100,15 +106,32 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
|
|
||||||
s.push_str("#import \"stil.typ\": *\n\n");
|
s.push_str("#import \"stil.typ\": *\n\n");
|
||||||
s.push_str(&format!(
|
let rap = &r.bygning.rapport;
|
||||||
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n",
|
let mut valg = format!(
|
||||||
e(&l.navn),
|
"tittel: \"{} — energirapport\", bunntekst: \"{}\"",
|
||||||
e(&bunntekst(r))
|
streng(&l.navn),
|
||||||
));
|
streng(&bunntekst(r))
|
||||||
|
);
|
||||||
|
if let Some(a) = &rap.avsender {
|
||||||
|
valg.push_str(&format!(", avsender: \"{}\"", streng(a)));
|
||||||
|
}
|
||||||
|
if let Some(f) = &rap.farge {
|
||||||
|
valg.push_str(&format!(", farge: rgb(\"{}\")", streng(f)));
|
||||||
|
}
|
||||||
|
if let Some(sk) = &rap.skrift_overskrift {
|
||||||
|
valg.push_str(&format!(", skrift: \"{}\"", streng(sk)));
|
||||||
|
}
|
||||||
|
s.push_str(&format!("#show: oppsett.with({valg})\n\n"));
|
||||||
|
|
||||||
// ── forside ─────────────────────────────────────────────────────
|
// ── forside ─────────────────────────────────────────────────────
|
||||||
|
if let Some(navn) = logo_navn(r) {
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\
|
"#align(right, image(\"{}\", height: 1.7cm))\n\n#v(-0.9cm)\n",
|
||||||
|
streng(&navn)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
s.push_str(&format!(
|
||||||
|
"#v(1.2cm)\n#forsidetittel[Energirapport]\n\n\
|
||||||
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
||||||
e(&l.navn)
|
e(&l.navn)
|
||||||
));
|
));
|
||||||
@@ -473,7 +496,8 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
if l.forutsetninger.is_empty() {
|
if l.forutsetninger.is_empty() {
|
||||||
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
||||||
} else {
|
} else {
|
||||||
s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
// Faste bredder, ellers spiser de to første kolonnene plassen kilden trenger.
|
||||||
|
s.push_str("#tabell(\n kolonner: (1.15fr, 0.85fr, 1.6fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
||||||
for f in &l.forutsetninger {
|
for f in &l.forutsetninger {
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
" {}\n",
|
" {}\n",
|
||||||
@@ -530,14 +554,33 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet med enok {} etter NS 3031:2025 (timesmetode, \
|
"\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. \
|
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",
|
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
||||||
|
i_dag(),
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
));
|
));
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tekst i en Typst-strengliteral: bare omvendt skråstrek og anførselstegn skal ut.
|
||||||
|
fn streng(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filnavnet på logoen, når den er oppgitt og finnes.
|
||||||
|
fn logo_navn(r: &Resultat) -> Option<String> {
|
||||||
|
let logo = r.bygning.rapport.logo.as_ref()?;
|
||||||
|
if !logo.exists() {
|
||||||
|
eprintln!(
|
||||||
|
" ! rapport.logo: fant ikke {}; rapporten lages uten logo.",
|
||||||
|
logo.display()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(logo.file_name()?.to_str()?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn bunntekst(r: &Resultat) -> String {
|
fn bunntekst(r: &Resultat) -> String {
|
||||||
let b = &r.bygning.bygning;
|
let b = &r.bygning.bygning;
|
||||||
let mut deler = Vec::new();
|
let mut deler = Vec::new();
|
||||||
@@ -550,9 +593,34 @@ fn bunntekst(r: &Resultat) -> String {
|
|||||||
if let Some(e) = &b.energiraadgiver {
|
if let Some(e) = &b.energiraadgiver {
|
||||||
deler.push(e.clone());
|
deler.push(e.clone());
|
||||||
}
|
}
|
||||||
|
deler.push(format!("enok {} · {}", env!("CARGO_PKG_VERSION"), i_dag()));
|
||||||
deler.join(" · ")
|
deler.join(" · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dagens dato som ISO-8601 i UTC.
|
||||||
|
fn i_dag() -> String {
|
||||||
|
let sekunder = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let (aar, maaned, dag) = dato_av_dagnummer(sekunder.div_euclid(86_400));
|
||||||
|
format!("{aar:04}-{maaned:02}-{dag:02}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kalenderdato av antall døgn siden 1970-01-01, etter Howard Hinnants algoritme.
|
||||||
|
fn dato_av_dagnummer(dager: i64) -> (i64, i64, i64) {
|
||||||
|
let z = dager + 719_468;
|
||||||
|
let era = z.div_euclid(146_097);
|
||||||
|
let doe = z.rem_euclid(146_097);
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let dag = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let maaned = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let aar = yoe + era * 400 + i64::from(maaned <= 2);
|
||||||
|
(aar, maaned, dag)
|
||||||
|
}
|
||||||
|
|
||||||
fn himmelretning(orientering: f64, helning: f64) -> String {
|
fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||||
if helning < 20.0 {
|
if helning < 20.0 {
|
||||||
return "horisontal".into();
|
return "horisontal".into();
|
||||||
@@ -566,3 +634,15 @@ fn himmelretning(orientering: f64, helning: f64) -> String {
|
|||||||
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
||||||
retninger[i].into()
|
retninger[i].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tester {
|
||||||
|
use super::dato_av_dagnummer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dagnummer_gir_riktig_dato() {
|
||||||
|
assert_eq!(dato_av_dagnummer(0), (1970, 1, 1));
|
||||||
|
assert_eq!(dato_av_dagnummer(19_723), (2024, 1, 1));
|
||||||
|
assert_eq!(dato_av_dagnummer(20_351), (2025, 9, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -310,7 +310,8 @@ fn baerer_for(t: VarmekildeType, lukket: bool) -> Energibaerer {
|
|||||||
|
|
||||||
/// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står.
|
/// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står.
|
||||||
pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 {
|
pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 {
|
||||||
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra;
|
let driftsandel = l.laster.driftstider.driftsdager_per_aar() as f64 / 365.0;
|
||||||
|
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra * driftsandel;
|
||||||
netto_w * s.tappevann_tap_andel
|
netto_w * s.tappevann_tap_andel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! Kontroll av de normerte inndataene for yrkesbyggkategoriene: driftsdager, faste energiposter
|
||||||
|
//! og kjølebatteri.
|
||||||
|
|
||||||
|
use enok::data::Datakilde;
|
||||||
|
use enok::kjor;
|
||||||
|
use enok::modell::{Bygning, Kategori};
|
||||||
|
|
||||||
|
fn prøvebygg(kategori: &str, kjolebatteri: bool) -> Bygning {
|
||||||
|
let s = format!(
|
||||||
|
r#"
|
||||||
|
[bygning]
|
||||||
|
navn = "Prøvebygg"
|
||||||
|
kategori = "{kategori}"
|
||||||
|
byggeaar = 2020
|
||||||
|
oppvarmet_bra = 1200.0
|
||||||
|
oppvarmet_volum = 3840.0
|
||||||
|
|
||||||
|
[klima]
|
||||||
|
kommune = "Oslo"
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Vegger"
|
||||||
|
type = "vegg"
|
||||||
|
areal = 560.0
|
||||||
|
u_verdi = 0.18
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Tak"
|
||||||
|
type = "tak"
|
||||||
|
areal = 600.0
|
||||||
|
u_verdi = 0.13
|
||||||
|
|
||||||
|
[[flate]]
|
||||||
|
navn = "Gulv"
|
||||||
|
type = "gulv"
|
||||||
|
areal = 600.0
|
||||||
|
u_verdi = 0.10
|
||||||
|
|
||||||
|
[[vindu]]
|
||||||
|
navn = "Sør"
|
||||||
|
areal = 150.0
|
||||||
|
orientering = 180
|
||||||
|
u_verdi = 0.8
|
||||||
|
g_verdi = 0.5
|
||||||
|
|
||||||
|
[infiltrasjon]
|
||||||
|
n50 = 0.6
|
||||||
|
|
||||||
|
[ventilasjon]
|
||||||
|
type = "balansert"
|
||||||
|
gjenvinning = 0.85
|
||||||
|
kjolebatteri = {kjolebatteri}
|
||||||
|
|
||||||
|
[[oppvarming.kilde]]
|
||||||
|
type = "direkte_el"
|
||||||
|
andel_rom = 1.0
|
||||||
|
andel_tappevann = 1.0
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
toml::from_str(&s).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post(r: &kjor::Resultat, nr: &str) -> f64 {
|
||||||
|
let p = r.budsjett.poster.iter().find(|p| p.nr == nr).unwrap();
|
||||||
|
p.netto / r.lost.bra
|
||||||
|
}
|
||||||
|
|
||||||
|
fn naer(faktisk: f64, forventet: f64, hva: &str) {
|
||||||
|
assert!(
|
||||||
|
(faktisk - forventet).abs() <= 0.1,
|
||||||
|
"{hva}: beregnet {faktisk:.2}, forventet {forventet:.2}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn driftsdager_per_aar() {
|
||||||
|
let kilde = Datakilde::default();
|
||||||
|
for (kategori, dager) in [
|
||||||
|
(Kategori::Smaahus, 365),
|
||||||
|
(Kategori::Boligblokk, 365),
|
||||||
|
(Kategori::Barnehage, 261),
|
||||||
|
(Kategori::Kontorbygg, 261),
|
||||||
|
(Kategori::Kulturbygg, 261),
|
||||||
|
(Kategori::Skolebygg, 220),
|
||||||
|
(Kategori::Idrettsbygg, 220),
|
||||||
|
] {
|
||||||
|
let n = kilde.normerte(kategori).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n.driftstid.driftsdager_per_aar(),
|
||||||
|
dager,
|
||||||
|
"{}",
|
||||||
|
kategori.nokkel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fellesferien og helgene er uten drift i skolebygg.
|
||||||
|
#[test]
|
||||||
|
fn skolebygg_stengt_i_helg_og_ferie() {
|
||||||
|
let n = Datakilde::default().normerte(Kategori::Skolebygg).unwrap();
|
||||||
|
let d = &n.driftstid;
|
||||||
|
assert!(d.driftsdag(0), "1. januar er mandag i uke 1");
|
||||||
|
assert!(!d.driftsdag(5), "lørdag");
|
||||||
|
assert!(!d.driftsdag(6), "søndag");
|
||||||
|
assert!(!d.driftsdag(7 * 28), "mandag i uke 29");
|
||||||
|
assert!(d.driftsdag(7 * 34), "mandag i uke 35");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Belysning, utstyr og varmtvann skal gi energibehovene som følger av tabellverdiene.
|
||||||
|
#[test]
|
||||||
|
fn faste_poster_som_tabellverdiene() {
|
||||||
|
let kilde = Datakilde::default();
|
||||||
|
for (kategori, belysning, utstyr, varmtvann) in [
|
||||||
|
("barnehage", 20.9, 5.2, 10.0),
|
||||||
|
("kontorbygg", 25.1, 34.5, 5.0),
|
||||||
|
("skolebygg", 22.0, 13.2, 10.0),
|
||||||
|
("idrettsbygg", 21.1, 2.6, 50.2),
|
||||||
|
("kulturbygg", 23.0, 2.9, 10.0),
|
||||||
|
] {
|
||||||
|
let r = kjor::kjor(&prøvebygg(kategori, false), &kilde).unwrap();
|
||||||
|
naer(post(&r, "5"), belysning, &format!("{kategori} belysning"));
|
||||||
|
naer(post(&r, "6"), utstyr, &format!("{kategori} utstyr"));
|
||||||
|
naer(post(&r, "2"), varmtvann, &format!("{kategori} varmtvann"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kjølebatteriet gir ventilasjonskjøling; uten det er posten null.
|
||||||
|
#[test]
|
||||||
|
fn kjolebatteri_gir_ventilasjonskjoling() {
|
||||||
|
let kilde = Datakilde::default();
|
||||||
|
let uten = kjor::kjor(&prøvebygg("kontorbygg", false), &kilde).unwrap();
|
||||||
|
let med = kjor::kjor(&prøvebygg("kontorbygg", true), &kilde).unwrap();
|
||||||
|
assert_eq!(post(&uten, "3b"), 0.0);
|
||||||
|
assert!(
|
||||||
|
post(&med, "3b") > 0.5,
|
||||||
|
"ventilasjonskjøling {:.2}",
|
||||||
|
post(&med, "3b")
|
||||||
|
);
|
||||||
|
// Kjøling skjer bare i driftstiden på driftsdager.
|
||||||
|
let ts = &med.timeserie;
|
||||||
|
let lørdag_middag = 5 * 24 + 12 + 24 * 7 * 26;
|
||||||
|
assert_eq!(ts.ventilasjonskjoling[lørdag_middag], 0.0);
|
||||||
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@ use enok::klima::geo::Posisjon;
|
|||||||
use enok::klima::tmy::naermeste_kommune;
|
use enok::klima::tmy::naermeste_kommune;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn naermeste_klimasted_for_tomter_er_indre_ostfold() {
|
fn naermeste_klimasted_i_indre_ostfold() {
|
||||||
let (kommune, km) = naermeste_kommune(&Posisjon {
|
let (kommune, km) = naermeste_kommune(&Posisjon {
|
||||||
bredde: 59.63,
|
bredde: 59.63,
|
||||||
lengde: 11.01,
|
lengde: 11.01,
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ fn regresjonsvakt_for_romoppvarming() {
|
|||||||
let r = kjor_referansebygg();
|
let r = kjor_referansebygg();
|
||||||
naer(
|
naer(
|
||||||
post(&r, "1a"),
|
post(&r, "1a"),
|
||||||
27.2,
|
27.1,
|
||||||
1.5,
|
1.5,
|
||||||
"romoppvarming (regresjon, publisert verdi 46,2)",
|
"romoppvarming (regresjon, publisert verdi 46,2)",
|
||||||
);
|
);
|
||||||
naer(
|
naer(
|
||||||
r.budsjett.netto_spesifikk(r.lost.bra),
|
r.budsjett.netto_spesifikk(r.lost.bra),
|
||||||
86.7,
|
86.6,
|
||||||
2.0,
|
2.0,
|
||||||
"netto energibehov (regresjon, publisert verdi 106,4)",
|
"netto energibehov (regresjon, publisert verdi 106,4)",
|
||||||
);
|
);
|
||||||
|
|||||||
+45
-8
@@ -7,11 +7,32 @@
|
|||||||
#let lys = rgb("#f4f1ee")
|
#let lys = rgb("#f4f1ee")
|
||||||
#let kantlinje = rgb("#ddd4cc")
|
#let kantlinje = rgb("#ddd4cc")
|
||||||
|
|
||||||
|
// Aksentfarge og overskriftsskrift kan settes av rapporten, slik at den kan følge
|
||||||
|
// firmaets øvrige dokumenter. Verdiene under er enoks egne.
|
||||||
|
#let aksent-valgt = state("aksent-valgt", aksent)
|
||||||
|
#let skrift-valgt = state("skrift-valgt", none)
|
||||||
|
|
||||||
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||||
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||||
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
|
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
|
||||||
|
|
||||||
#let oppsett(tittel: "", undertittel: "", bunntekst: "", doc) = {
|
/// Skriften rapporten har bedt om, ellers enoks egen.
|
||||||
|
#let overskriftsskrift() = {
|
||||||
|
let valgt = skrift-valgt.get()
|
||||||
|
if valgt == none { font-heading } else { (valgt,) + font-heading }
|
||||||
|
}
|
||||||
|
|
||||||
|
#let oppsett(
|
||||||
|
tittel: "",
|
||||||
|
undertittel: "",
|
||||||
|
bunntekst: "",
|
||||||
|
avsender: "",
|
||||||
|
farge: none,
|
||||||
|
skrift: none,
|
||||||
|
doc,
|
||||||
|
) = {
|
||||||
|
aksent-valgt.update(if farge == none { aksent } else { farge })
|
||||||
|
skrift-valgt.update(skrift)
|
||||||
set page(
|
set page(
|
||||||
paper: "a4",
|
paper: "a4",
|
||||||
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
||||||
@@ -20,32 +41,36 @@
|
|||||||
grid(
|
grid(
|
||||||
columns: (1fr, auto),
|
columns: (1fr, auto),
|
||||||
align: (left + horizon, right + horizon),
|
align: (left + horizon, right + horizon),
|
||||||
text(font: font-heading, size: 8pt, fill: mid)[#tittel],
|
text(font: overskriftsskrift(), size: 8pt, fill: mid)[#tittel],
|
||||||
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
|
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
|
||||||
)
|
)
|
||||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
footer: context {
|
footer: context {
|
||||||
if counter(page).get().first() > 1 and bunntekst != "" {
|
if counter(page).get().first() > 1 and (bunntekst != "" or avsender != "") {
|
||||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||||
v(3pt)
|
v(3pt)
|
||||||
|
if avsender != "" {
|
||||||
|
text(font: font-body, size: 7.5pt, fill: mid)[#avsender]
|
||||||
|
linebreak()
|
||||||
|
}
|
||||||
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
|
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
||||||
set par(justify: true, leading: 0.62em)
|
set par(justify: true, leading: 0.62em)
|
||||||
show heading: it => {
|
show heading: it => context {
|
||||||
set text(font: font-heading, fill: aksent)
|
set text(font: overskriftsskrift(), fill: aksent-valgt.get())
|
||||||
block(above: 1.2em, below: 0.6em, it)
|
block(above: 1.2em, below: 0.6em, it)
|
||||||
}
|
}
|
||||||
show heading.where(level: 1): it => {
|
show heading.where(level: 1): it => context {
|
||||||
set text(size: 15pt, weight: "bold")
|
set text(size: 15pt, weight: "bold")
|
||||||
block(above: 1.6em, below: 0.7em)[
|
block(above: 1.6em, below: 0.7em)[
|
||||||
#it.body
|
#it.body
|
||||||
#v(-0.45em)
|
#v(-0.45em)
|
||||||
#line(length: 100%, stroke: 1pt + aksent)
|
#line(length: 100%, stroke: 1pt + aksent-valgt.get())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
|
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
|
||||||
@@ -55,7 +80,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tabell med stripete rader og linje under overskriften.
|
// Tabell med stripete rader og linje under overskriften.
|
||||||
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = {
|
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = context {
|
||||||
|
let aksent = aksent-valgt.get()
|
||||||
let j = if justering == none { kolonner.map(_ => right) } else { justering }
|
let j = if justering == none { kolonner.map(_ => right) } else { justering }
|
||||||
block(breakable: true, table(
|
block(breakable: true, table(
|
||||||
columns: kolonner,
|
columns: kolonner,
|
||||||
@@ -71,6 +97,16 @@
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tittelen på forsiden, i samme skrift og farge som overskriftene.
|
||||||
|
#let forsidetittel(tekst) = context {
|
||||||
|
text(
|
||||||
|
font: overskriftsskrift(),
|
||||||
|
size: 26pt,
|
||||||
|
weight: "bold",
|
||||||
|
fill: aksent-valgt.get(),
|
||||||
|
)[#tekst]
|
||||||
|
}
|
||||||
|
|
||||||
// Stor karakterrute på forsiden.
|
// Stor karakterrute på forsiden.
|
||||||
#let karakterrute(bokstav, verdi, farge) = {
|
#let karakterrute(bokstav, verdi, farge) = {
|
||||||
box(
|
box(
|
||||||
@@ -99,3 +135,4 @@
|
|||||||
stroke: (left: 2pt + aksent-lys),
|
stroke: (left: 2pt + aksent-lys),
|
||||||
text(size: 9pt)[#innhold],
|
text(size: 9pt)[#innhold],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user