Energiberegning etter NS 3031:2025 med timesmetode og energirapport

enok leser en bygningsbeskrivelse i TOML, henter typisk klimaår for kommunen,
simulerer 8760 timer etter NS-EN ISO 52016-1 og skriver energirapport, CSV og JSON.

Motor
- Femnodemodell for opake bygningsdeler, tonodemodell for vinduer, luftnode og
  strålingsnode per sone, implisitt løsning per time med egen tett LU-løser.
- Varme- og kjølebehov ved eksakt interpolasjon mellom to løsninger, siden systemet
  er lineært. Vinduslufting for bygninger uten kjøleanlegg.
- Solberegning etter ISO 52010-1, kontrollert mot pvlib med avvik under 1 prosent.

Beregningsgang
- Beregningspunktene A til E: netto, brutto, tilført, levert per energibærer og
  vektet levert energi, med solceller, klimakorrigering og effektbehov.
- Energikarakter etter Enovas skala fra 1.1.2026, med arealkorreksjon.

Inndata
- TOML med scenarier for tiltak, og import fra IFC, gbXML og regneark.
- Normerte verdier, karakterskala, vektingsfaktorer og klimadata i data/,
  som kan overstyres med --data.

Kontroll
- Stasjonært varmetap mot U·A·ΔT innenfor 2 prosent, energibalansen innenfor
  2 prosent, varmetapsbudsjettet for TEK17-referansebygget identisk med publisert
  tabell. Kjent avvik i romoppvarming mot Simien 8 er dokumentert i README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
bl
2026-09-14 18:03:00 +02:00
co-authored by Claude Opus 5
commit 0964cacba3
51 changed files with 10309 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
//! Kjører en fullstendig energiberegning: fra bygningsbeskrivelse til energikarakter.
use crate::data::Datakilde;
use crate::klima::{korreksjon, tmy, Vaerdata, REFERANSEKLIMA};
use crate::merking::{karakter, Energikarakter};
use crate::modell::lost::{Forutsetning, Lost};
use crate::modell::Bygning;
use crate::motor::{iso52016::Systemtilskudd, simuler, Timeserie, Varmetapsbudsjett};
use crate::systemer::{self, Effektbehov, Energibudsjett, Levert, Systemoppsett};
use crate::Result;
use serde::Serialize;
/// Alt som kommer ut av én beregning.
pub struct Resultat {
pub bygning: Bygning,
pub lost: Lost,
pub vaerdata: Vaerdata,
pub klimanavn: String,
pub varmetap: Varmetapsbudsjett,
pub timeserie: Timeserie,
pub systemer: Systemoppsett,
pub budsjett: Energibudsjett,
pub levert: Levert,
pub karakter: Energikarakter,
pub effektbehov: Effektbehov,
pub solceller: Option<crate::systemer::solcelle::Produksjon>,
pub merknader: Vec<String>,
}
/// Kompakt, serialiserbart sammendrag for `resultat.json`.
#[derive(Serialize)]
pub struct Sammendrag<'a> {
pub navn: &'a str,
pub kategori: &'a str,
pub byggeaar: i32,
pub oppvarmet_bra: f64,
pub klimasted: &'a str,
pub referanseklima: &'a str,
pub varmetapsbudsjett: &'a Varmetapsbudsjett,
pub energibudsjett: &'a Energibudsjett,
pub netto_spesifikk: f64,
pub levert: &'a Levert,
pub energikarakter: &'a Energikarakter,
pub effektbehov: &'a Effektbehov,
pub forutsetninger: &'a [Forutsetning],
pub merknader: &'a [String],
pub maanedsverdier: Maanedsverdier,
}
#[derive(Serialize)]
pub struct Maanedsverdier {
pub maaned: [&'static str; 12],
pub romoppvarming: [f64; 12],
pub ventilasjonsvarme: [f64; 12],
pub varmtvann: [f64; 12],
pub vifter_pumper: [f64; 12],
pub belysning: [f64; 12],
pub utstyr: [f64; 12],
pub kjoling: [f64; 12],
pub utetemperatur: [f64; 12],
}
impl Resultat {
pub fn sammendrag(&self) -> Sammendrag<'_> {
let m = |s: &[f64]| Timeserie::maaned_kwh(s);
let mut vifter_pumper = m(&self.timeserie.vifter);
let pumper = m(&self.timeserie.pumper);
let mut kjoling = m(&self.timeserie.romkjoling);
let ventkjol = m(&self.timeserie.ventilasjonskjoling);
for i in 0..12 {
vifter_pumper[i] += pumper[i];
kjoling[i] += ventkjol[i];
}
Sammendrag {
navn: &self.lost.navn,
kategori: self.lost.kategori.nokkel(),
byggeaar: self.lost.byggeaar,
oppvarmet_bra: self.lost.bra,
klimasted: &self.klimanavn,
referanseklima: REFERANSEKLIMA,
varmetapsbudsjett: &self.varmetap,
energibudsjett: &self.budsjett,
netto_spesifikk: self.budsjett.netto_spesifikk(self.lost.bra),
levert: &self.levert,
energikarakter: &self.karakter,
effektbehov: &self.effektbehov,
forutsetninger: &self.lost.forutsetninger,
merknader: &self.merknader,
maanedsverdier: Maanedsverdier {
maaned: crate::MAANEDER,
romoppvarming: m(&self.timeserie.romoppvarming),
ventilasjonsvarme: m(&self.timeserie.ventilasjonsvarme),
varmtvann: m(&self.timeserie.varmtvann),
vifter_pumper,
belysning: m(&self.timeserie.belysning),
utstyr: m(&self.timeserie.utstyr),
kjoling,
utetemperatur: self.vaerdata.maanedsmiddeltemperatur(),
},
}
}
}
/// Henter værdata for bygningen: egen EPW-fil, oppgitt kommune, eller referanseklimaet.
pub fn hent_klima(b: &Bygning) -> Result<(Vaerdata, String, bool)> {
if let Some(sti) = &b.klima.epw {
let v = Vaerdata::fra_fil(sti)?;
let navn = v.sted.clone();
let er_referanse = crate::data::normaliser_stedsnavn(&navn)
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
return Ok((v, navn, er_referanse));
}
let sted = b
.klima
.kommune
.clone()
.unwrap_or_else(|| REFERANSEKLIMA.to_string());
let er_referanse = crate::data::normaliser_stedsnavn(&sted)
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
let v = tmy::for_kommune(&sted)?;
Ok((v, sted, er_referanse))
}
pub fn kjor(b: &Bygning, kilde: &Datakilde) -> Result<Resultat> {
let lost = Lost::fra(b, kilde)?;
let (vaerdata, klimanavn, er_referanse) = hent_klima(b)?;
kjor_med_klima(b, kilde, lost, vaerdata, klimanavn, er_referanse)
}
pub fn kjor_med_klima(
b: &Bygning,
kilde: &Datakilde,
lost: Lost,
vaerdata: Vaerdata,
klimanavn: String,
er_referanse: bool,
) -> Result<Resultat> {
let varmetap = Varmetapsbudsjett::beregn(&lost);
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
let mut merknader = systemer_oppsett.merknader.clone();
let tilskudd = Systemtilskudd {
tappevann: systemer::tappevannstilskudd(&lost, &systemer_oppsett),
};
let timeserie = simuler(&lost, &vaerdata, tilskudd)?;
let budsjett = systemer::energibudsjett(&timeserie, &systemer_oppsett);
let solceller = b.solcelle.as_ref().map(|s| {
systemer::solcelle::beregn(s, &vaerdata, &timeserie, &systemer_oppsett, lost.albedo)
});
let (egenbruk, eksportert) = solceller
.as_ref()
.map(|p| (p.egenbruk_kwh, p.eksportert_kwh))
.unwrap_or((0.0, 0.0));
let korrigering = if er_referanse {
None
} else {
let tabell = kilde.klimakorreksjon()?;
let klimaavhengig = systemer::klimaavhengig_spesifikk(&budsjett, lost.bra);
let k = korreksjon::korriger(&tabell, &klimanavn, lost.kategori, klimaavhengig);
if k.is_none() {
merknader.push(format!(
"Fant ingen klimakorrigeringsfaktor for «{klimanavn}». Energiytelsen er oppgitt i lokalt \
klima og er ikke sammenlignbar med energimerkeskalaen."
));
}
k
};
let vekting = kilde.vekting()?;
let levert = systemer::levert(
&budsjett,
&vekting,
lost.bra,
korrigering,
egenbruk,
eksportert,
);
let skala = kilde.skala()?;
let karakter = karakter(
skala.for_kategori(lost.kategori)?,
levert.spesifikk_vektet,
lost.skala_areal,
);
let effektbehov = systemer::effektbehov(&timeserie, &systemer_oppsett);
Ok(Resultat {
bygning: b.clone(),
lost,
vaerdata,
klimanavn,
varmetap,
timeserie,
systemer: systemer_oppsett,
budsjett,
levert,
karakter,
effektbehov,
solceller,
merknader,
})
}