test / test (push) Successful in 1m19s
Netto energibehov skal ikke avhenge av varmeanlegget, men enok lot systemtapet fra tappevann og varmedistribusjon telle som varmetilskudd i rommene. Det er riktig etter ISO 52000-1 og NS 3031:2025 for et bygg som skal beskrive faktisk energibruk, men ikke for energirammene, som er netto tall. «drift.systemtap_som_tilskudd» styrer det nå, og referansebyggene settes opp uten. Snittavviket mot de publiserte verdiene går fra 27,9 til 26,4 prosent. Samtidig er det dokumentert hva avviket kommer av. Beregningen er kontrollert mot en uavhengig bin-beregning rett av klimafilen: under de samme forutsetningene gir enok 49,5 mot bin-beregningens 51,5 kWh/(m²·år). Varmetapet er altså riktig, og avviket ligger i varmetilskuddene, som er målt hver for seg i README. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
262 lines
9.2 KiB
Rust
262 lines
9.2 KiB
Rust
//! 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)?;
|
|
// Er kommunen oppgitt, gjelder den for oppslaget av klimakorrigeringsfaktoren.
|
|
// Da er navnet uavhengig av hvordan stedsnavnet er skrevet inne i klimafilen.
|
|
let navn = b.klima.kommune.clone().unwrap_or_else(|| v.sted.clone());
|
|
let er_referanse = crate::data::normaliser_stedsnavn(&navn)
|
|
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
|
return Ok((v, navn, er_referanse));
|
|
}
|
|
// Uten kommune, men med posisjon, velges klimastedet nærmest posisjonen.
|
|
let sted = match (&b.klima.kommune, b.klima.posisjon) {
|
|
(Some(k), _) => k.clone(),
|
|
(None, Some([bredde, lengde])) => {
|
|
tmy::naermeste_kommune(&crate::klima::geo::Posisjon { bredde, lengde })?.0
|
|
}
|
|
(None, None) => 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();
|
|
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(
|
|
crate::modell::kontroll::kontroller(b)
|
|
.into_iter()
|
|
.map(|m| m.melding),
|
|
);
|
|
|
|
// Tap fra varmerør og bereder som står i en uoppvarmet sone varmer den sonen, og
|
|
// reduserer dermed varmetapet gjennom etasjeskilleren. Andelen som avgis i den
|
|
// oppvarmede delen teller som uregulerbart varmetilskudd der.
|
|
let tappevannstap = systemer::tappevannstilskudd_totalt(&lost, &systemer_oppsett);
|
|
let sone_tappevann: f64 = lost
|
|
.soner
|
|
.iter()
|
|
.map(|s| s.andel_tappevannstap)
|
|
.sum::<f64>()
|
|
.min(1.0);
|
|
// Uten gjenvinnbart systemtap blir netto energibehov uavhengig av varmeanlegget, slik
|
|
// energirammene i TEK er regnet. Med, slik ISO 52000-1 og NS 3031:2025 beskriver det,
|
|
// varmer tapet rommene det står i.
|
|
let med = if lost.systemtap_som_tilskudd {
|
|
1.0
|
|
} else {
|
|
0.0
|
|
};
|
|
let tilskudd = Systemtilskudd {
|
|
tappevann: med
|
|
* tappevannstap
|
|
* systemer_oppsett.tappevann_tap_i_sone
|
|
* (1.0 - sone_tappevann),
|
|
sone_fast: lost
|
|
.soner
|
|
.iter()
|
|
.map(|s| s.fast_effekt + med * tappevannstap * s.andel_tappevannstap)
|
|
.collect(),
|
|
sone_andel_romvarme: lost
|
|
.soner
|
|
.iter()
|
|
.map(|s| {
|
|
med * s.andel_distribusjonstap * systemer_oppsett.distribusjon_tap_ut
|
|
/ (1.0 - systemer_oppsett.distribusjon_tap_ut).max(1e-6)
|
|
})
|
|
.collect(),
|
|
};
|
|
let timeserie = simuler(&lost, &vaerdata, tilskudd)?;
|
|
let budsjett = systemer::energibudsjett(×erie, &systemer_oppsett);
|
|
|
|
let solceller = b.solcelle.as_ref().map(|s| {
|
|
systemer::solcelle::beregn(s, &vaerdata, ×erie, &systemer_oppsett, lost.albedo)
|
|
});
|
|
let (egenbruk, eksportert) = solceller
|
|
.as_ref()
|
|
.map(|p| (p.egenbruk_kwh, p.eksportert_kwh))
|
|
.unwrap_or((0.0, 0.0));
|
|
|
|
let korrigering = if er_referanse {
|
|
None
|
|
} else {
|
|
let tabell = kilde.klimakorreksjon()?;
|
|
let klimaavhengig = systemer::klimaavhengig_spesifikk(&budsjett, lost.bra);
|
|
let k = korreksjon::korriger(&tabell, &klimanavn, lost.kategori, klimaavhengig);
|
|
if k.is_none() {
|
|
merknader.push(format!(
|
|
"Fant ingen klimakorrigeringsfaktor for «{klimanavn}». Energiytelsen er oppgitt i lokalt \
|
|
klima og er ikke sammenlignbar med energimerkeskalaen."
|
|
));
|
|
}
|
|
k
|
|
};
|
|
|
|
let vekting = kilde.vekting()?;
|
|
let levert = systemer::levert(
|
|
&budsjett,
|
|
&vekting,
|
|
lost.bra,
|
|
korrigering,
|
|
egenbruk,
|
|
eksportert,
|
|
);
|
|
let skala = kilde.skala()?;
|
|
let karakter = karakter(
|
|
skala.for_kategori(lost.kategori)?,
|
|
levert.spesifikk_vektet,
|
|
lost.skala_areal,
|
|
);
|
|
let effektbehov = systemer::effektbehov(×erie, &systemer_oppsett);
|
|
|
|
Ok(Resultat {
|
|
bygning: b.clone(),
|
|
lost,
|
|
vaerdata,
|
|
klimanavn,
|
|
varmetap,
|
|
timeserie,
|
|
systemer: systemer_oppsett,
|
|
budsjett,
|
|
levert,
|
|
karakter,
|
|
effektbehov,
|
|
solceller,
|
|
merknader,
|
|
})
|
|
}
|