3 Commits
Author SHA1 Message Date
blandClaude Opus 5 fba5a7936d Velg klimasted ut fra georefereringen, og rett adresse fra IFC
Release enok / bygg (release) Successful in 1m20s
test / test (push) Successful in 1m26s
En ny eksport av Nordhusstykket 5 viste hva mer modellen kan bære, og avdekket feil.

Posisjon og klimasted
- IfcMapConversion med IfcProjectedCRS gjøres om til breddegrad og lengdegrad.
  EUREF89 og WGS 84 UTM, NN2000-varianter og Web Mercator forstås. Omregningen er
  kontrollert mot PROJ.
- Vectorworks' standardplassering i Washington, som står når ingenting er satt,
  kjennes igjen og brukes ikke.
- Ny `posisjon` under [klima]. Uten kommune velger beregningen nærmeste av de 358
  klimastedene i TMY-arkivet, og sier i rapporten hvor langt unna det ligger.

Opplysninger fra modellen
- Adressen hentes fra IfcBuilding eller IfcSite. Før ble den første postadressen i
  filen brukt, og det var firmaets adresse i forfatteropplysningene.
- Energirådgiver fra person og organisasjon i forfatteropplysningene.
- Byggets navn fra prosjektet når bygningen heter «Default Building».
- Referansehøyde og terrenghøyde på bygningen gir hvor mye av grunnmuren som står
  over bakken.
- IsLandmarked gir en merknad om at vernede bygninger er unntatt energiattestplikten.

En eksport uten bygningsdeler stopper nå med en feilmelding som peker på
lagtilknytningen i eksportdialogen, i stedet for å skrive en tom bygning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-15 15:38:04 +02:00
blandClaude Opus 5 be1514bd83 Fjern rapportbyggingen fra enok
test / test (push) Successful in 1m26s
Energirapporter lages i hvert prosjektlager, ikke i verktøyets eget lager.
rapport.yml og bygninger/ er fjernet. Mappen inneholdt en kundes IFC-modell
og bygningsbeskrivelse, som hører hjemme i prosjektlageret.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-15 15:26:20 +02:00
blandClaude Opus 5 81544d73e6 Hent mye mer ut av IFC-modellen
Energirapporter / rapporter (release) Successful in 57s
Release enok / bygg (release) Successful in 1m8s
test / test (push) Successful in 1m16s
Modellen for Nordhusstykket 5 viste seg å inneholde langt mer enn importen brukte.

Etasjer
- IfcBuildingStorey med høyde og navn, og tilknytningen av rom og bygningsdeler, sier
  hva som er hva. Etasjen «Kjeller» ligger 2,35 m under referansenivået.
- En etasje under terreng settes nå opp som en egen uoppvarmet sone med sine egne
  vegger og gulv, og dekket over blir grensen mot den oppvarmede delen.
- Oppvarmet BRA er rommene i etasjene over terreng. Rom som heter kott, bod, loft
  eller garasje blir flagget, siden de sjelden er oppvarmet.
- Et dekke kan bare ligge mot grunnen i nederste etasje. Før ble etasjeskillere høyere
  opp regnet som gulv mot grunn.

Egenskaper
- Byggeår, bygningskategori, kommune, lekkasjetall, ventilasjonstype, varmegjenvinning,
  varmekilde og oppvarmet BRA leses fra et hvilket som helst egenskapssett på bygningen,
  tomta eller prosjektet, på norsk eller engelsk. Da kan et postformat i BIM-verktøyet
  bære de opplysningene IFC ikke har noen fast plass til.
- Modellen viste seg allerede å ha YearOfConstruction 1950. Bare den ene verdien flytter
  den automatiske beregningen fra karakter E til G, siden lekkasjetallet følger byggeåret.
- Masseklasse utledes av materialnavnene, og posisjonen leses av IfcSite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-14 22:36:54 +02:00
16 changed files with 1344 additions and 555 deletions
-76
View File
@@ -1,76 +0,0 @@
name: Energirapporter
# Lager energirapport for hver bygning i `bygninger/` og legger PDF, JSON og CSV ved
# utgivelsen. Se `bygninger/LES-MEG.md` for hvilke filtyper som kan ligge der.
on:
release:
types: [published]
workflow_dispatch:
jobs:
rapporter:
runs-on: fish
defaults:
run:
shell: fish {0}
steps:
- name: Hent koden
uses: https://gitea.com/actions/checkout@v6
- name: Sørg for en Rust-verktøykjede
run: |
# Kjørebrukeren har rustup, men ingen standardkjede er satt. Uten dette svarer
# cargo med at den ikke vet hvilken versjon den skal bruke.
if not cargo --version >/dev/null 2>&1
rustup default stable
end
cargo --version
- name: Bygg enok
run: cargo build --release --locked
- name: Importer BIM-filer som ikke har en TOML fra før
run: |
for f in (find bygninger -type f \( -iname '*.ifc' -o -iname '*.gbxml' -o -iname '*.csv' \) 2>/dev/null)
set toml (string replace -r '\.[^.]+$' '.toml' $f)
if not test -f $toml
echo "Importerer $f"
./target/release/enok import $f --ut $toml
end
end
- name: Beregn og lag rapporter
run: |
mkdir -p dist
set feil 0
for t in (find bygninger -type f -name '*.toml' 2>/dev/null)
set navn (basename $t .toml)
echo "::group::$navn"
if ./target/release/enok beregn $t --ut ut/$navn
cp ut/$navn/rapport.pdf dist/$navn-energirapport.pdf
cp ut/$navn/resultat.json dist/$navn-resultat.json
cp ut/$navn/maaned.csv dist/$navn-maaned.csv
gzip -c ut/$navn/timeserie.csv > dist/$navn-timeserie.csv.gz
else
echo "Beregningen av $navn feilet"
set feil 1
end
echo "::endgroup::"
end
ls -la dist
exit $feil
- name: Legg ved utgivelsen
if: github.event_name == 'release'
env:
TOKEN: ${{ github.token }}
run: |
set REL_ID (curl --silent --fail --header "Authorization: token $TOKEN" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$GITHUB_REF_NAME" | jq -r '.id')
for f in (find dist -type f)
echo "Laster opp "(basename $f)
curl --silent --show-error --fail --header "Authorization: token $TOKEN" \
--form "attachment=@$f" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$REL_ID/assets?name="(basename $f)
end
Generated
+1 -1
View File
@@ -222,7 +222,7 @@ dependencies = [
[[package]] [[package]]
name = "enok" name = "enok"
version = "0.1.1" version = "0.3.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "enok" name = "enok"
version = "0.1.1" version = "0.3.0"
edition = "2021" edition = "2021"
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
license = "MIT" license = "MIT"
+70 -14
View File
@@ -9,7 +9,7 @@ varmekilder og drift — og et typisk klimaår, og gir 8760 timesverdier, et ful
fra netto energibehov til vektet levert energi, en energikarakter og en ferdig rapport i PDF. fra netto energibehov til vektet levert energi, en energikarakter og en ferdig rapport i PDF.
``` ```
enok beregn bygninger/nordhusstykket-5.toml enok beregn bygning.toml
``` ```
``` ```
@@ -117,7 +117,7 @@ byggeaar = 1948
oppvarmet_bra = 140.0 oppvarmet_bra = 140.0
[klima] [klima]
kommune = "Indre Østfold" kommune = "Indre Østfold" # eller posisjon = [59.63, 11.01] for nærmeste klimasted
[[flate]] [[flate]]
navn = "Yttervegg sør" navn = "Yttervegg sør"
@@ -178,12 +178,72 @@ 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 |
Himmelretningene i IFC utledes av elementenes plassering: veggens lokale akse gir planet, og ### Hva enok henter ut av en IFC-modell
ytre normal velges som den som peker bort fra bygningens tyngdepunkt, korrigert for prosjektets
sanne nord. Kontroller dem mot tegningen etter import.
Ingen av formatene beskriver ventilasjon, lekkasjetall eller varmekilder. Importen skriver | Opplysning | Hvor den kommer fra |
merknader øverst i TOML-filen om hva som må fylles ut. | --- | --- |
| Areal for vegger, tak og dekker | Geometrien. Trekantene grupperes etter retning, og den største gruppen er delens hovedflate. Vinduer som er skåret ut av veggen kommer riktig fram, siden hullene mangler trekanter. Materialsjikt som ligger oppå hverandre telles én gang |
| Himmelretning og helning | Flatenormalen, dreid med elementets plassering og prosjektets sanne nord |
| U-verdi | `ThermalTransmittance` i egenskapssettene, for både konstruksjoner og vinduer |
| Ytre eller innvendig | `IsExternal` |
| Masseklasse | Materialnavnene. Betong, tegl og mur gir tung konstruksjon, bindingsverk og isolasjon lett |
| Etasjer og hva som ligger i dem | `IfcBuildingStorey` med høyde, og tilknytningen av rom og bygningsdeler |
| Uoppvarmet kjeller | En etasje under referansenivået blir satt opp som egen sone, med sine egne vegger og gulv, og dekket over som grense mot den oppvarmede delen |
| Oppvarmet BRA | Rommene i etasjene over terreng. Rom som heter kott, bod, loft eller garasje blir flagget |
| Vindus- og dørareal | `OverallHeight` og `OverallWidth`, og hvilken vegg de sitter i |
| Posisjon | `IfcMapConversion` med `IfcProjectedCRS`, ellers `IfcSite`. Uten oppgitt kommune velger beregningen nærmeste klimasted. EUREF89 UTM og Web Mercator forstås |
| Grunnmur over terreng | `ElevationOfRefHeight` og `ElevationOfTerrain` på bygningen |
| Adresse | `BuildingAddress` på bygningen, eller `SiteAddress` på tomta. Adressen i forfatteropplysningene er firmaets og brukes ikke |
| Energirådgiver | Person og organisasjon i forfatteropplysningene |
| Byggets navn | Bygningens navn, eller prosjektets når bygningen heter «Default Building» |
| Vernet bygning | `IsLandmarked`. Vernede bygninger er unntatt plikten til energiattest |
Himmelretningene og arealene bør likevel kontrolleres mot tegningen. Importen skriver merknader
øverst i TOML-filen om hva den måtte anta.
### Egenskaper enok leser
IFC beskriver geometri og konstruksjon, men har ingen fast plass til byggeår, tetthet,
ventilasjon eller hvordan huset varmes opp. Det er nettopp de opplysningene som skiller en god
energiberegning fra en middelmådig: for et hus fra 1948 er forskjellen mellom lekkasjetall 0,6
og 8 langt større enn alt annet i modellen.
De kan likevel følge med. I Vectorworks lager du et postformat, fester det på bygningen og
knytter feltene til et IFC-egenskapssett. enok leser da disse navnene fra et hvilket som helst
egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
| Felt | Godtar også | Eksempel |
| --- | --- | --- |
| `Byggeaar` | `Byggeår`, `YearOfConstruction` | `1948` |
| `Bygningskategori` | `Kategori`, `BuildingCategory` | `Småhus` |
| `Kommune` | `Municipality`, `Klimasted` | `Indre Østfold` |
| `Lekkasjetall` | `n50`, `AirPermeability` | `8.0` |
| `Ventilasjonstype` | `Ventilasjon`, `VentilationType` | `Balansert` |
| `Varmegjenvinning` | `Gjenvinning`, `HeatRecoveryEfficiency` | `80` |
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
| `OppvarmetBRA` | `HeatedFloorArea` | `120` |
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
modellen alene.
Dette i selve modellen er verdt å gjøre riktig, omtrent i rekkefølge etter hva det betyr:
- **Ta med designlagene i eksporten.** I «Export IFC Project» må lagene være knyttet til
etasjer under «Layer Mapping». Mangler de, blir filen bare prosjektstruktur uten vegger,
dekker, vinduer eller rom. enok stopper da med en feilmelding i stedet for å skrive en tom
bygning.
- **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'
standardverdi i Washington, og den blir oversett.
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av
hundre og fem vegger merket, og kjellermurene er ikke blant dem.
- **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
uoppvarmede kjelleren og regner oppvarmet areal riktig.
- **Sett referansehøyde og terrenghøyde på bygningen.** Da regner enok ut hvor mye av
grunnmuren som står over bakken. Uten dem regnes hele muren som nedgravd.
- **Legg byggets adresse på bygningen.** Adressen i forfatteropplysningene er firmaets, og
brukes ikke som byggets adresse.
Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet: Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet:
@@ -197,22 +257,18 @@ vindu,Stue sør,3.0,2.8,180,90,,0.76,2
## Rapporter fra byggeserveren ## Rapporter fra byggeserveren
Arbeidsflytene i `.gitea/workflows/` kjører på vertskjøreren `fish` og utløses når en Rapportene lages i hvert prosjektlager, ikke i enok. enoks egne arbeidsflyter kjører på
utgivelse publiseres, på samme måte som de andre prosjektene. vertskjøreren `fish` på samme måte som de andre prosjektene:
| Fil | Hva den gjør | | Fil | Hva den gjør |
| --- | --- | | --- | --- |
| `test.yml` | Formatering, clippy og tester på hver push | | `test.yml` | Formatering, clippy og tester på hver push |
| `release.yml` | Bygger enok og legger binærfilen ved utgivelsen | | `release.yml` | Bygger enok og legger binærfilen ved utgivelsen |
| `rapport.yml` | Lager energirapport for hver bygning i `bygninger/` og legger PDF, JSON og CSV ved utgivelsen |
IFC-, gbXML- og CSV-filer i `bygninger/` importeres automatisk til TOML dersom det ikke
allerede finnes en TOML-fil med samme navn. Se `bygninger/LES-MEG.md`.
### 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 `bygning.toml` trenger ikke Rust. Det henter
binærfilen fra enoks utgivelse: binærfilen fra enoks siste utgivelse:
```yaml ```yaml
name: Energirapport name: Energirapport
-18
View File
@@ -1,18 +0,0 @@
# Bygninger
Legg én fil per bygning her. Byggeserveren regner ut energirapporten og legger PDF, JSON og
CSV ved utgivelsen når du setter en tagg som begynner med `v` eller `rapport-`.
| Filtype | Kommer fra |
| -------- | --------------------------------------------------- |
| `.toml` | Bygningsbeskrivelse skrevet for hånd, eller importert |
| `.ifc` | Vectorworks: *Fil → Eksporter → Export IFC Project* |
| `.gbxml` | BIM-verktøy som eksporterer gbXML |
| `.csv` | Vectorworks: *Fil → Eksporter → Export Worksheet* |
IFC-, gbXML- og CSV-filer blir importert til TOML automatisk dersom det ikke allerede finnes
en TOML-fil med samme navn. Importen får med geometri, U-verdier og himmelretninger, men ikke
ventilasjon, lekkasjetall eller varmekilder. Åpne den importerte TOML-filen, fyll ut det som
mangler, og sjekk den inn — da brukes den i stedet for importen neste gang.
Eksempler på ferdige bygningsbeskrivelser ligger i `../eksempler/`.
File diff suppressed because one or more lines are too long
-392
View File
@@ -1,392 +0,0 @@
# Nordhusstykket 5, 1825 Tomter — enebolig i tre fra 1948, 1,5 etasje på betong grunnmur.
#
# Geometri, U-verdier og himmelretninger er importert fra byggets IFC-modell
# (Vectorworks Architect 2026, «Nordhusstykket 5.ifc»). Arealene er regnet av geometrien,
# og oppføringene er slått sammen per konstruksjon og himmelretning.
# Vurderingene av isolasjon og lekkasjer er hentet fra termografirapporten av 10.3.2016.
#
# Verdier som ikke står i modellen og må kontrolleres på stedet er merket KONTROLLER.
[bygning]
navn = "Nordhusstykket 5"
kategori = "smaahus"
byggeaar = 1948
# Oppvarmet BRA er summen av rommene i 1. og 2. etasje i IFC-modellen:
# stue 39,6 + kjøkken 22,6 + entré 10,6 + vindfang 1,2 = 74,0 m² i 1. etasje,
# tre soverom 33,9 + gang/trapperom 11,2 + toalett 0,9 = 46,0 m² i 2. etasje.
# Kjeller (74,9 m²) og kott (19,6 m²) er uoppvarmede og regnes som egne soner.
oppvarmet_bra = 120.0
oppvarmet_volum = 288.0
adresse = "Nordhusstykket 5, 1825 Tomter"
oppdragsgiver = "Ellen Brødholt"
beskrivelse = "Enebolig i tre, 1,5 etasje, bebygget areal 99,6 m², uoppvarmet kjeller på betong grunnmur"
[klima]
kommune = "Indre Østfold"
[grunn]
ledningsevne = 2.0
# ── klimaskjerm for den oppvarmede delen ───────────────────────────────
# Ytterveggene er stenderverk med panel ute og porøs huntonitt inne. IFC-modellen oppgir
# U-verdi 0,93 W/(m²K), som stemmer med termografirapportens vurdering av at veggen har
# noe, men lite isolasjon.
[[flate]]
navn = "Yttervegg nord"
type = "vegg"
areal = 41.8
orientering = 0
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg øst"
type = "vegg"
areal = 54.2
orientering = 90
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg sør"
type = "vegg"
areal = 55.7
orientering = 180
u_verdi = 0.93
masseklasse = "lett"
[[flate]]
navn = "Yttervegg vest"
type = "vegg"
areal = 59.5
orientering = 270
u_verdi = 0.93
masseklasse = "lett"
# KONTROLLER: IFC-modellen oppgir ingen U-verdi for takflatene. Termografien viser
# luftlekkasje til loftet og ut gjennom pipen, og lite isolasjon i skråtaket.
[[flate]]
navn = "Skråtak"
type = "tak"
areal = 130.1
helning = 40
u_verdi = 0.55
masseklasse = "lett"
kommentar = "Areal fra IFC; U-verdi anslått etter termografirapporten"
[[flate]]
navn = "Himling mot kaldt loft"
type = "tak"
areal = 32.6
helning = 0
u_verdi = 0.45
mot = "uoppvarmet"
b_faktor = 0.9
masseklasse = "lett"
# Etasjeskilleren mot kjelleren. IFC-modellen har 150,4 m² «Etasjeskille», som dekker både
# gulvet over kjelleren og skillet mellom 1. og 2. etasje. Halvparten ligger over kjelleren,
# og stemmer med kjellergulvets 78,9 m².
[[flate]]
navn = "Etasjeskiller mot kjeller"
type = "gulv"
areal = 75.0
u_verdi = 0.31
mot = "uoppvarmet"
sone = "Kjeller"
masseklasse = "middels"
# ── vinduer og dører ───────────────────────────────────────────────────
# Areal og himmelretning fra IFC. KONTROLLER: modellen oppgir ingen U-verdi for vinduene.
# Termografirapporten beskriver dem som dårlig isolerte, og 2,8 W/(m²K) svarer til
# eldre tolagsglass. Med originale enkle glass ville verdien vært nærmere 5.
[[vindu]]
navn = "Vinduer nord"
areal = 5.58
orientering = 0
u_verdi = 2.80
g_verdi = 0.76
[[vindu]]
navn = "Vinduer øst"
areal = 2.86
orientering = 90
u_verdi = 2.80
g_verdi = 0.76
[[vindu]]
navn = "Vinduer sør"
areal = 3.96
orientering = 180
u_verdi = 2.80
g_verdi = 0.76
[[vindu]]
navn = "Vinduer vest"
areal = 1.05
orientering = 270
u_verdi = 2.80
g_verdi = 0.76
[[vindu]]
navn = "Inngangsdør med vindfang"
areal = 1.46
orientering = 270
u_verdi = 2.00
dor = true
[[vindu]]
navn = "Verandadør øst"
areal = 2.94
orientering = 90
u_verdi = 2.00
dor = true
[[vindu]]
navn = "Balkongdør sør"
areal = 1.91
orientering = 180
u_verdi = 2.00
dor = true
# Utkragede bjelker som bærer balkongen er en tydelig kuldebro i termografien.
[kuldebroer]
normalisert = 0.15
# KONTROLLER: lekkasjetallet er ikke målt. Termografien viser luftlekkasje til loft og
# gjennom pipen, som er typisk for et utett hus fra 1948.
[infiltrasjon]
n50 = 8.0
skjerming = "moderat"
[ventilasjon]
type = "naturlig"
luftmengde = 1.2
[tappevann]
system = "aapent"
# ── uoppvarmet kjeller ─────────────────────────────────────────────────
# Kjelleren brukes ikke til opphold, men varmes av uisolerte varmerør og av kjelen som står
# i det tekniske rommet. Temperaturen regnes ut time for time av kjellerens varmebalanse,
# slik at rørtapet både teller som tap fra varmeanlegget og som varme som holder
# etasjeskilleren varm. Det var dette termografirapporten pekte på som det største funnet.
[[sone]]
navn = "Kjeller"
volum = 165.0
luftveksling = 0.4
# Alt distribusjonstapet fra det vannbårne anlegget avgis her, og berederen står her.
andel_distribusjonstap = 1.0
andel_tappevannstap = 1.0
# KONTROLLER: anslått kappetap fra kjelen.
fast_effekt = 100.0
kommentar = "Rom 21,1 + gang 11,1 + vaskerom 9,9 + 11,1 + boder 10,4 + 9,3 + teknisk rom 2,2 = 74,9 m²"
# Grunnmuren. KONTROLLER: høyden over terreng er anslått til 0,6 m av fasadebildene i
# termografirapporten, og kjellerhøyden til 2,2 m. Disse to tallene avgjør hvor mye av
# muren som taper varme rett til uteluft, og er derfor verdt å måle på stedet.
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, nord"
type = "vegg"
areal = 21.2
orientering = 0
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, øst"
type = "vegg"
areal = 9.7
orientering = 90
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, sør"
type = "vegg"
areal = 22.7
orientering = 180
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur 30 cm uisolert, vest"
type = "vegg"
areal = 15.0
orientering = 270
u_verdi = 3.13
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur 25 cm uisolert"
type = "vegg"
areal = 40.7
orientering = 90
u_verdi = 3.39
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellermur med treullsement"
type = "vegg"
areal = 2.2
orientering = 90
u_verdi = 0.46
mot = "grunn"
dybde = 1.6
over_terreng = 0.6
[[sone.flate]]
navn = "Kjellergulv"
type = "gulv"
areal = 78.9
u_verdi = 3.85
mot = "grunn"
omkrets = 40.0
dybde = 2.2
# Kjellervinduene. Termografien viser stort varmetap gjennom kjellervinduet.
[[sone.flate]]
navn = "Kjellervinduer"
type = "vegg"
areal = 1.8
orientering = 180
u_verdi = 4.50
mot = "ute"
kommentar = "KONTROLLER: antall og størrelse; enkle glass i støpt karm"
# ── varmeforsyning ─────────────────────────────────────────────────────
# Vannbårent radiatoranlegg med elektrokjel i kjelleren, supplert med vedovn.
[[oppvarming.kilde]]
type = "vedovn"
andel_rom = 0.25
lukket = false
kommentar = "Åpent ildsted; energimerkeforskriften regner inntil 10 % av romoppvarmingen"
[[oppvarming.kilde]]
type = "el_kjel"
andel_rom = 0.75
andel_tappevann = 1.0
# Uisolerte varmerør gjennom den uoppvarmede kjelleren er hovedgrunnen til at kjelleren
# er varm. Termografirapporten anbefaler å isolere dem.
[distribusjon]
type = "radiator"
turtemperatur = 60
tap_andel = 0.25
roer_uoppvarmet = 1.0
# ── tiltak ─────────────────────────────────────────────────────────────
[[scenario]]
navn = "Isolerte varmerør"
beskrivelse = "Isolering av varmerørene gjennom kjelleren, som termografirapporten anbefaler. Rørtapet faller, men kjelleren blir kaldere, så noe av gevinsten spises opp av økt varmetap gjennom etasjeskilleren."
endringer.distribusjon = { tap_andel = 0.06 }
[[scenario]]
navn = "Isolerte kjellervegger"
beskrivelse = "Innvendig isolering av grunnmuren til U-verdi 0,35 W/(m²K)."
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
[[scenario]]
navn = "Isolerte rør og kjellervegger"
beskrivelse = "Begge kjellertiltakene samlet, slik termografirapporten anbefaler dem."
endringer.distribusjon = { tap_andel = 0.06 }
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
[[scenario]]
navn = "Nye vinduer"
beskrivelse = "Utskifting til trelags energiglass med U-verdi 0,8 W/(m²K)."
endringer.vindu = [
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Balkongdør sør", u_verdi = 1.20 },
{ navn = "Verandadør øst", u_verdi = 1.20 },
]
[[scenario]]
navn = "Etterisolert klimaskjerm"
beskrivelse = "Etterisolering av yttervegger til 20 cm og loft til 30 cm, med tetting av luftlekkasjer."
endringer.flate = [
{ navn = "Yttervegg nord", u_verdi = 0.22 },
{ navn = "Yttervegg øst", u_verdi = 0.22 },
{ navn = "Yttervegg sør", u_verdi = 0.22 },
{ navn = "Yttervegg vest", u_verdi = 0.22 },
{ navn = "Skråtak", u_verdi = 0.18 },
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
]
endringer.infiltrasjon = { n50 = 4.0 }
endringer.kuldebroer = { normalisert = 0.10 }
[[scenario]]
navn = "Væske-vann-varmepumpe"
beskrivelse = "Bergvarmepumpe på det vannbårne anlegget, med elektrokjel som spisslast."
endringer.oppvarming.kilde = [
{ type = "vedovn", slett = true },
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 55 },
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
]
[[scenario]]
navn = "Full pakke"
beskrivelse = "Alle tiltakene samlet: isolerte rør og kjellervegger, nye vinduer, etterisolert klimaskjerm, balansert ventilasjon med varmegjenvinning og bergvarmepumpe."
endringer.distribusjon = { tap_andel = 0.06, turtemperatur = 45 }
endringer.infiltrasjon = { n50 = 2.5 }
endringer.kuldebroer = { normalisert = 0.09 }
endringer.ventilasjon = { type = "balansert", gjenvinning = 0.80, sfp = 1.5 }
endringer.flate = [
{ navn = "Yttervegg nord", u_verdi = 0.22 },
{ navn = "Yttervegg øst", u_verdi = 0.22 },
{ navn = "Yttervegg sør", u_verdi = 0.22 },
{ navn = "Yttervegg vest", u_verdi = 0.22 },
{ navn = "Skråtak", u_verdi = 0.18 },
{ navn = "Himling mot kaldt loft", u_verdi = 0.15 },
{ navn = "Etasjeskiller mot kjeller", u_verdi = 0.18 },
]
endringer.vindu = [
{ navn = "Vinduer nord", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer øst", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Vinduer vest", u_verdi = 0.80, g_verdi = 0.50 },
{ navn = "Balkongdør sør", u_verdi = 1.20 },
{ navn = "Verandadør øst", u_verdi = 1.20 },
]
endringer.sone = [{ navn = "Kjeller", flate = [
{ navn = "Kjellermur 30 cm uisolert, nord", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, øst", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, sør", u_verdi = 0.35 },
{ navn = "Kjellermur 30 cm uisolert, vest", u_verdi = 0.35 },
{ navn = "Kjellermur 25 cm uisolert", u_verdi = 0.35 },
] }]
endringer.oppvarming.kilde = [
{ type = "vedovn", slett = true },
{ type = "vaeske_vann_vp", andel_rom = 0.9, andel_tappevann = 0.8, turtemperatur = 45 },
{ type = "el_kjel", andel_rom = 0.1, andel_tappevann = 0.2 },
]
+952 -43
View File
File diff suppressed because it is too large Load Diff
+22 -5
View File
@@ -112,11 +112,14 @@ pub fn hent_klima(b: &Bygning) -> Result<(Vaerdata, String, bool)> {
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA); == crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
return Ok((v, navn, er_referanse)); return Ok((v, navn, er_referanse));
} }
let sted = b // Uten kommune, men med posisjon, velges klimastedet nærmest posisjonen.
.klima let sted = match (&b.klima.kommune, b.klima.posisjon) {
.kommune (Some(k), _) => k.clone(),
.clone() (None, Some([bredde, lengde])) => {
.unwrap_or_else(|| REFERANSEKLIMA.to_string()); tmy::naermeste_kommune(&crate::klima::geo::Posisjon { bredde, lengde })?.0
}
(None, None) => REFERANSEKLIMA.to_string(),
};
let er_referanse = crate::data::normaliser_stedsnavn(&sted) let er_referanse = crate::data::normaliser_stedsnavn(&sted)
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA); == crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
let v = tmy::for_kommune(&sted)?; let v = tmy::for_kommune(&sted)?;
@@ -140,6 +143,20 @@ pub fn kjor_med_klima(
let varmetap = Varmetapsbudsjett::beregn(&lost); let varmetap = Varmetapsbudsjett::beregn(&lost);
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?; let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
let mut merknader = systemer_oppsett.merknader.clone(); let mut merknader = systemer_oppsett.merknader.clone();
if b.klima.kommune.is_none() && b.klima.epw.is_none() {
if let Some([bredde, lengde]) = b.klima.posisjon {
let bygg = crate::klima::geo::Posisjon { bredde, lengde };
let stasjon = crate::klima::geo::Posisjon {
bredde: vaerdata.bredde,
lengde: vaerdata.lengde,
};
merknader.push(format!(
"Klimastedet {klimanavn} er valgt ut fra posisjonen {bredde:.4}° nord, {lengde:.4}° øst, \
{:.0} km unna. Sett `kommune` under [klima] for å velge et annet.",
bygg.avstand_km(&stasjon)
));
}
}
merknader.extend( merknader.extend(
crate::modell::kontroll::kontroller(b) crate::modell::kontroll::kontroller(b)
.into_iter() .into_iter()
+224
View File
@@ -0,0 +1,224 @@
//! Posisjon fra BIM-modellen, og valg av klimasted ut fra den.
//!
//! En IFC-modell kan oppgi hvor bygningen står på to måter: som breddegrad og lengdegrad på
//! `IfcSite`, eller som prosjiserte koordinater gjennom `IfcMapConversion` og
//! `IfcProjectedCRS`. I Norge er det siste normalt EUREF89 UTM sone 32, 33 eller 35. Denne
//! modulen gjør begge om til breddegrad og lengdegrad, slik at nærmeste klimasted kan velges
//! automatisk.
use std::f64::consts::FRAC_PI_2;
/// Store halvakse for GRS80, som EUREF89 bygger på, m.
const A: f64 = 6_378_137.0;
/// Flattrykning for GRS80.
const F: f64 = 1.0 / 298.257_222_101;
/// Målestokkfaktor langs sentralmeridianen i UTM.
const K0: f64 = 0.9996;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Posisjon {
/// Breddegrad, grader nord.
pub bredde: f64,
/// Lengdegrad, grader øst.
pub lengde: f64,
}
impl Posisjon {
/// Avstand langs jordoverflaten, km.
pub fn avstand_km(&self, annen: &Posisjon) -> f64 {
let (b1, b2) = (self.bredde.to_radians(), annen.bredde.to_radians());
let db = b2 - b1;
let dl = (annen.lengde - self.lengde).to_radians();
let h = (db / 2.0).sin().powi(2) + b1.cos() * b2.cos() * (dl / 2.0).sin().powi(2);
2.0 * 6371.0 * h.sqrt().asin()
}
/// Sant for plasseringen Vectorworks setter når prosjektstedet ikke er angitt, og for
/// nullpunktet. Begge betyder at modellen i praksis ikke oppgir noen posisjon.
pub fn er_standardplassering(&self) -> bool {
let washington =
(self.bredde - 38.8895).abs() < 0.05 && (self.lengde + 77.0353).abs() < 0.05;
let null = self.bredde.abs() < 1e-6 && self.lengde.abs() < 1e-6;
washington || null
}
/// Grovt: innenfor fastlands-Norge og Svalbard.
pub fn i_norge(&self) -> bool {
(57.5..=81.5).contains(&self.bredde) && (4.0..=32.0).contains(&self.lengde)
}
}
/// Fra Web Mercator (EPSG:3857), som Vectorworks bruker når ingen annen projeksjon er valgt.
pub fn fra_web_mercator(x: f64, y: f64) -> Posisjon {
Posisjon {
bredde: (2.0 * (y / A).exp().atan() - FRAC_PI_2).to_degrees(),
lengde: (x / A).to_degrees(),
}
}
/// Fra UTM på nordlige halvkule, med GRS80-ellipsoiden som EUREF89 bruker.
///
/// Inversformlene er de vanlige seriene etter Snyder, «Map Projections A Working Manual»,
/// som gir centimeternøyaktighet innenfor en sone.
pub fn fra_utm(sone: u32, x: f64, y: f64) -> Posisjon {
let e2 = F * (2.0 - F);
let ep2 = e2 / (1.0 - e2);
let e1 = (1.0 - (1.0 - e2).sqrt()) / (1.0 + (1.0 - e2).sqrt());
let m = y / K0;
let mu = m / (A * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2.powi(3) / 256.0));
let fi1 = mu
+ (3.0 * e1 / 2.0 - 27.0 * e1.powi(3) / 32.0) * (2.0 * mu).sin()
+ (21.0 * e1 * e1 / 16.0 - 55.0 * e1.powi(4) / 32.0) * (4.0 * mu).sin()
+ (151.0 * e1.powi(3) / 96.0) * (6.0 * mu).sin()
+ (1097.0 * e1.powi(4) / 512.0) * (8.0 * mu).sin();
let (s, c, t) = (fi1.sin(), fi1.cos(), fi1.tan());
let n1 = A / (1.0 - e2 * s * s).sqrt();
let t1 = t * t;
let c1 = ep2 * c * c;
let r1 = A * (1.0 - e2) / (1.0 - e2 * s * s).powf(1.5);
let d = (x - 500_000.0) / (n1 * K0);
let bredde = fi1
- (n1 * t / r1)
* (d * d / 2.0
- (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * d.powi(4) / 24.0
+ (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1)
* d.powi(6)
/ 720.0);
let sentralmeridian = (sone as f64 - 1.0) * 6.0 - 180.0 + 3.0;
let lengde = sentralmeridian
+ ((d - (1.0 + 2.0 * t1 + c1) * d.powi(3) / 6.0
+ (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1)
* d.powi(5)
/ 120.0)
/ c)
.to_degrees();
Posisjon {
bredde: bredde.to_degrees(),
lengde,
}
}
/// Gjør koordinater i et navngitt koordinatsystem om til posisjon.
///
/// `navn` er slik det står i `IfcProjectedCRS`, for eksempel «EPSG:25832». Koordinatsystemer
/// enok ikke kjenner gir `None`, og da brukes ikke posisjonen.
pub fn fra_koordinatsystem(navn: &str, x: f64, y: f64) -> Option<Posisjon> {
let stor = navn.to_uppercase();
let kode: Option<u32> = stor.find("EPSG").and_then(|i| {
let siffer: String = stor[i + 4..]
.chars()
.skip_while(|c| !c.is_ascii_digit())
.take_while(|c| c.is_ascii_digit())
.collect();
siffer.parse().ok()
});
match kode {
Some(3857) | Some(900_913) => Some(fra_web_mercator(x, y)),
// Geografiske koordinater: østverdien er lengdegrad og nordverdien breddegrad.
Some(4326) | Some(4258) => Some(Posisjon {
bredde: y,
lengde: x,
}),
// ETRS89 / EUREF89 UTM sone 2838.
Some(k @ 25828..=25838) => Some(fra_utm(k - 25800, x, y)),
// WGS 84 UTM nord, sone 2838.
Some(k @ 32628..=32638) => Some(fra_utm(k - 32600, x, y)),
// ETRS89 / UTM med NN2000-høyder, som Kartverket bruker: 5972 er sone 32, 5973 sone 33,
// 5975 sone 35.
Some(k @ 5972..=5976) => Some(fra_utm(k - 5940, x, y)),
_ => {
// Uten EPSG-kode: kjenn igjen «UTM32», «UTM 33N» og lignende.
let i = stor.find("UTM")?;
let sone: String = stor[i + 3..]
.chars()
.skip_while(|c| !c.is_ascii_digit())
.take_while(|c| c.is_ascii_digit())
.collect();
let sone: u32 = sone.parse().ok()?;
(1..=60).contains(&sone).then(|| fra_utm(sone, x, y))
}
}
}
#[cfg(test)]
mod tester {
use super::*;
#[test]
fn web_mercator_fra_vectorworks_gir_washington() {
// Verdiene i IfcMapConversion når prosjektstedet ikke er satt i Vectorworks.
let p = fra_web_mercator(-8_575_524.352_288_59, 4_705_852.257_469_73);
assert!((p.bredde - 38.8895).abs() < 1e-3, "bredde {}", p.bredde);
assert!((p.lengde + 77.0352).abs() < 1e-3, "lengde {}", p.lengde);
assert!(p.er_standardplassering());
assert!(!p.i_norge());
}
/// Referanseverdier fra pyproj (PROJ), transformert fra EPSG:4326.
#[test]
fn utm_og_web_mercator_stemmer_med_proj() {
// (sone, øst, nord, breddegrad, lengdegrad, toleranse i grader)
//
// Innenfor sonen og et stykke utenfor er avviket under 1e-5 grader, om lag én meter.
// Tromsø i sone 32 ligger nesten 10 grader utenfor sonen, der rekkeutviklingen gir
// om lag 1,3 m avvik. Det er langt innenfor hva valg av klimasted trenger, men viser
// hvor grensen går.
let utm = [
(32, 613_351.291, 6_611_921.980, 59.6300, 11.0100, 1e-5),
(32, 596_946.981, 6_642_824.393, 59.9115, 10.7336, 1e-5),
(33, 275_055.615, 6_616_968.071, 59.6300, 11.0100, 1e-5),
(33, 653_421.188, 7_731_721.083, 69.6492, 18.9553, 1e-5),
(35, 188_546.727, 7_747_295.774, 69.6492, 18.9553, 1e-5),
(32, 884_909.622, 7_758_204.184, 69.6492, 18.9553, 1e-4),
];
for (sone, x, y, b, l, toleranse) in utm {
let p = fra_utm(sone, x, y);
assert!(
(p.bredde - b).abs() < toleranse && (p.lengde - l).abs() < toleranse,
"sone {sone} ({x}, {y}) ga {p:?}, forventet {b}, {l}"
);
}
let p = fra_web_mercator(1_225_627.594, 8_317_818.189);
assert!(
(p.bredde - 59.63).abs() < 1e-6 && (p.lengde - 11.01).abs() < 1e-6,
"{p:?}"
);
}
#[test]
fn sentralmeridianen_gir_eksakt_lengdegrad() {
// Østverdi 500 000 ligger på sonens sentralmeridian, 9° øst for sone 32.
let p = fra_utm(32, 500_000.0, 6_600_000.0);
assert!((p.lengde - 9.0).abs() < 1e-9, "lengde {}", p.lengde);
let q = fra_utm(33, 500_000.0, 6_600_000.0);
assert!((q.lengde - 15.0).abs() < 1e-9, "lengde {}", q.lengde);
}
#[test]
fn ekvator_paa_sentralmeridianen() {
let p = fra_utm(32, 500_000.0, 0.0);
assert!(p.bredde.abs() < 1e-9 && (p.lengde - 9.0).abs() < 1e-9);
}
#[test]
fn ukjent_koordinatsystem_gir_ingen_posisjon() {
assert!(fra_koordinatsystem("EPSG:2154", 650_000.0, 6_860_000.0).is_none());
assert!(fra_koordinatsystem("lokalt", 1.0, 2.0).is_none());
}
#[test]
fn epsg_koder_tolkes() {
let a = fra_koordinatsystem("EPSG:25832", 500_000.0, 6_600_000.0).unwrap();
let b = fra_koordinatsystem("ETRS89 / UTM zone 32N", 500_000.0, 6_600_000.0).unwrap();
assert!((a.lengde - 9.0).abs() < 1e-9 && (b.lengde - 9.0).abs() < 1e-9);
let c = fra_koordinatsystem("EPSG:5973", 500_000.0, 6_600_000.0).unwrap();
assert!(
(c.lengde - 15.0).abs() < 1e-9,
"5973 er sone 33, fikk {}",
c.lengde
);
}
}
+1
View File
@@ -1,6 +1,7 @@
//! Klimadata: TMY-filer (EPW) for norske kommuner, solberegning og klimakorrigering. //! Klimadata: TMY-filer (EPW) for norske kommuner, solberegning og klimakorrigering.
pub mod epw; pub mod epw;
pub mod geo;
pub mod korreksjon; pub mod korreksjon;
pub mod sol; pub mod sol;
pub mod tmy; pub mod tmy;
+30
View File
@@ -76,6 +76,36 @@ fn kommune_av_filnavn(navn: &str) -> Option<String> {
Some(base.to_string()) Some(base.to_string())
} }
/// Klimastedet nærmest en posisjon, og avstanden dit i km.
///
/// Hver TMY-fil oppgir i første linje koordinatene for punktet dataene gjelder. Bare starten
/// av hver fil pakkes ut, så oppslaget går raskt selv om arkivet er stort.
pub fn naermeste_kommune(p: &crate::klima::geo::Posisjon) -> Result<(String, f64)> {
let sti = sikre_arkiv()?;
let fil = std::fs::File::open(&sti)?;
let mut zip = zip::ZipArchive::new(fil)?;
let mut best: Option<(String, f64)> = None;
for i in 0..zip.len() {
let oppf = zip.by_index(i)?;
let Some(kommune) = kommune_av_filnavn(oppf.name()) else {
continue;
};
let mut hode = Vec::with_capacity(512);
oppf.take(512).read_to_end(&mut hode)?;
let linje = crate::klima::epw::avkod(&hode);
let felt: Vec<&str> = linje.lines().next().unwrap_or("").split(',').collect();
let tall = |i: usize| felt.get(i).and_then(|s| s.trim().parse::<f64>().ok());
let (Some(bredde), Some(lengde)) = (tall(6), tall(7)) else {
continue;
};
let d = p.avstand_km(&crate::klima::geo::Posisjon { bredde, lengde });
if best.as_ref().is_none_or(|(_, bd)| d < *bd) {
best = Some((kommune, d));
}
}
best.ok_or_else(|| anyhow!("TMY-arkivet inneholder ingen klimafiler med koordinater"))
}
/// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både /// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både
/// «Indre Østfold» og «Indre Ostfold» treffer samme fil. /// «Indre Østfold» og «Indre Ostfold» treffer samme fil.
pub fn for_kommune(kommune: &str) -> Result<Vaerdata> { pub fn for_kommune(kommune: &str) -> Result<Vaerdata> {
+6 -1
View File
@@ -152,7 +152,12 @@ fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
eprintln!("Rapport: {}", pdf.display()); eprintln!("Rapport: {}", pdf.display());
} }
for m in &resultat.merknader { // Manglene er allerede skrevet ut før beregningen; her kommer bare det som kom i tillegg.
let alt_sagt: Vec<String> = enok::modell::kontroll::kontroller(&bygning)
.into_iter()
.map(|m| m.melding)
.collect();
for m in resultat.merknader.iter().filter(|m| !alt_sagt.contains(m)) {
eprintln!(" ! {m}"); eprintln!(" ! {m}");
} }
skriv_sammendrag(&resultat, &scenarier); skriv_sammendrag(&resultat, &scenarier);
+1 -1
View File
@@ -58,7 +58,7 @@ pub fn kontroller(b: &Bygning) -> Vec<Mangel> {
}); });
} }
if b.klima.kommune.is_none() && b.klima.epw.is_none() { if b.klima.kommune.is_none() && b.klima.epw.is_none() && b.klima.posisjon.is_none() {
ut.push(Mangel { ut.push(Mangel {
felt: "klima.kommune", felt: "klima.kommune",
melding: "Kommune er ikke oppgitt, så beregningen bruker referanseklimaet Oslo. \ melding: "Kommune er ikke oppgitt, så beregningen bruker referanseklimaet Oslo. \
+4
View File
@@ -117,6 +117,10 @@ fn en() -> u32 {
#[derive(Deserialize, Serialize, Clone, Debug, Default)] #[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct Klima { pub struct Klima {
/// Byggets posisjon som [breddegrad, lengdegrad]. Er kommune ikke oppgitt, velges
/// klimastedet nærmest denne posisjonen.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub posisjon: Option<[f64; 2]>,
/// Kommune for lokalt klima (TMY-fil fra Zenodo 14779899). Utelatt: bare Oslo. /// Kommune for lokalt klima (TMY-fil fra Zenodo 14779899). Utelatt: bare Oslo.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub kommune: Option<String>, pub kommune: Option<String>,
+30
View File
@@ -0,0 +1,30 @@
//! Kontroll av valg av klimasted. Testene bruker TMY-arkivet og laster det ned ved første kjøring.
use enok::data::normaliser_stedsnavn;
use enok::klima::geo::Posisjon;
use enok::klima::tmy::naermeste_kommune;
#[test]
fn naermeste_klimasted_for_tomter_er_indre_ostfold() {
let (kommune, km) = naermeste_kommune(&Posisjon {
bredde: 59.63,
lengde: 11.01,
})
.unwrap();
assert_eq!(
normaliser_stedsnavn(&kommune),
"indreostfold",
"fikk {kommune}"
);
assert!(km < 25.0, "{km} km");
}
#[test]
fn naermeste_klimasted_for_oslo_raadhus_er_oslo() {
let (kommune, _) = naermeste_kommune(&Posisjon {
bredde: 59.9115,
lengde: 10.7336,
})
.unwrap();
assert_eq!(normaliser_stedsnavn(&kommune), "oslo", "fikk {kommune}");
}