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:
+174
@@ -0,0 +1,174 @@
|
||||
//! Kontroll av beregningsmotoren mot analytiske sammenhenger og mot publiserte eksempler.
|
||||
|
||||
use enok::data::Datakilde;
|
||||
use enok::klima::Vaerdata;
|
||||
use enok::modell::lost::{Lost, Regulering};
|
||||
use enok::modell::Bygning;
|
||||
use enok::motor::{iso52016::Systemtilskudd, simuler, Timeserie, Varmetapsbudsjett};
|
||||
use enok::TIMER;
|
||||
|
||||
/// Bygger et kunstig værdatasett med konstant utetemperatur og uten sol.
|
||||
fn stille_vaer(t_ute: f64) -> Vaerdata {
|
||||
Vaerdata {
|
||||
sted: "Prøve".into(),
|
||||
land: "NOR".into(),
|
||||
bredde: 60.0,
|
||||
lengde: 10.0,
|
||||
tidssone: 1.0,
|
||||
hoyde: 0.0,
|
||||
t_ute: vec![t_ute; TIMER],
|
||||
rf: vec![50.0; TIMER],
|
||||
ghi: vec![0.0; TIMER],
|
||||
dni: vec![0.0; TIMER],
|
||||
dhi: vec![0.0; TIMER],
|
||||
// Himmeltemperatur lik utetemperatur, slik at langbølget utstråling ikke spiller inn.
|
||||
ir_himmel: vec![5.67e-8 * (t_ute + 273.15f64).powi(4); TIMER],
|
||||
vind: vec![0.0; TIMER],
|
||||
}
|
||||
}
|
||||
|
||||
const ENKEL: &str = r#"
|
||||
[bygning]
|
||||
navn = "Kassen"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 2020
|
||||
oppvarmet_bra = 100.0
|
||||
oppvarmet_volum = 240.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Vegger"
|
||||
type = "vegg"
|
||||
areal = 120.0
|
||||
u_verdi = 0.20
|
||||
masseklasse = "middels"
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 100.0
|
||||
u_verdi = 0.15
|
||||
masseklasse = "middels"
|
||||
|
||||
[[flate]]
|
||||
navn = "Gulv"
|
||||
type = "gulv"
|
||||
areal = 100.0
|
||||
u_verdi = 0.15
|
||||
masseklasse = "tung"
|
||||
|
||||
[kuldebroer]
|
||||
normalisert = 0.0
|
||||
|
||||
[infiltrasjon]
|
||||
n50 = 0.0
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig"
|
||||
luftmengde = 0.0
|
||||
luftmengde_natt = 0.0
|
||||
|
||||
[drift]
|
||||
nattsenking = 21.0
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "direkte_el"
|
||||
andel_rom = 1.0
|
||||
andel_ventilasjon = 1.0
|
||||
andel_tappevann = 1.0
|
||||
"#;
|
||||
|
||||
/// Uten varmetilskudd, sol, ventilasjon og infiltrasjon skal effektbehovet i stasjonær
|
||||
/// tilstand være varmetapskoeffisienten ganger temperaturdifferansen.
|
||||
///
|
||||
/// Overgangsmotstanden R_si = 0,13 m²K/W som U-verdien bygger på, gjelder mot operativ
|
||||
/// temperatur. Nodemodellen skiller luft- og strålingstemperatur, så sammenligningen gjøres
|
||||
/// med regulering på operativ temperatur.
|
||||
#[test]
|
||||
fn stasjonaert_varmetap_stemmer_med_varmetapstallet() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let mut l = Lost::fra(&b, &kilde).unwrap();
|
||||
// Slå av alle interne laster og bruk lufttemperatur som reguleringsstørrelse.
|
||||
l.laster.belysning = [0.0; 24];
|
||||
l.laster.utstyr_effekt = [0.0; 24];
|
||||
l.laster.utstyr_varme = [0.0; 24];
|
||||
l.laster.personer = [0.0; 24];
|
||||
l.laster.varmtvann = [0.0; 24];
|
||||
l.laster.pumper = [0.0; 24];
|
||||
l.regulering = Regulering::Operativ;
|
||||
|
||||
let t_ute = 1.0;
|
||||
let v = stille_vaer(t_ute);
|
||||
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
|
||||
let budsjett = Varmetapsbudsjett::beregn(&l);
|
||||
// Gulvet står mot det fri i denne prøven, så hele transmisjonstapet er mot utetemperaturen.
|
||||
let h = budsjett.transmisjon_w_per_k;
|
||||
let forventet = h * (l.settpunkt_oppvarming - t_ute);
|
||||
|
||||
// Se bort fra innkjøringen: bruk siste halvdel av året.
|
||||
let siste: Vec<f64> = ts.romoppvarming[TIMER / 2..].to_vec();
|
||||
let snitt = siste.iter().sum::<f64>() / siste.len() as f64;
|
||||
let avvik = (snitt - forventet).abs() / forventet;
|
||||
assert!(
|
||||
avvik < 0.02,
|
||||
"stasjonært varmetap avviker {:.2} %: beregnet {snitt:.1} W, forventet {forventet:.1} W",
|
||||
avvik * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Energibalansen skal gå opp: tilført varme og varmetilskudd skal være lik varmetapet.
|
||||
#[test]
|
||||
fn aarlig_energibalanse_gaar_opp() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let l = Lost::fra(&b, &kilde).unwrap();
|
||||
let v = stille_vaer(1.0);
|
||||
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
||||
let budsjett = Varmetapsbudsjett::beregn(&l);
|
||||
|
||||
let tilfort: f64 = ts.romoppvarming.iter().sum::<f64>() + ts.internlast.iter().sum::<f64>();
|
||||
// Varmetapet regnet av de simulerte operative temperaturene, som U-verdiene gjelder mot.
|
||||
let tap: f64 = ts
|
||||
.t_operativ
|
||||
.iter()
|
||||
.map(|t| budsjett.transmisjon_w_per_k * (t - 1.0))
|
||||
.sum();
|
||||
let avvik = (tilfort - tap).abs() / tap;
|
||||
assert!(
|
||||
avvik < 0.02,
|
||||
"energibalansen avviker {:.2} %",
|
||||
avvik * 100.0
|
||||
);
|
||||
}
|
||||
|
||||
/// Dobbelt så stort temperaturfall skal gi dobbelt så stort varmebehov i en lineær modell.
|
||||
#[test]
|
||||
fn varmebehovet_er_lineaert_i_temperaturdifferansen() {
|
||||
let b: Bygning = toml::from_str(ENKEL).unwrap();
|
||||
let kilde = Datakilde::default();
|
||||
let mut l = Lost::fra(&b, &kilde).unwrap();
|
||||
l.laster.belysning = [0.0; 24];
|
||||
l.laster.utstyr_effekt = [0.0; 24];
|
||||
l.laster.utstyr_varme = [0.0; 24];
|
||||
l.laster.personer = [0.0; 24];
|
||||
l.laster.varmtvann = [0.0; 24];
|
||||
l.laster.pumper = [0.0; 24];
|
||||
l.regulering = Regulering::Operativ;
|
||||
|
||||
let en = Timeserie::sum_kwh(
|
||||
&simuler(&l, &stille_vaer(11.0), Systemtilskudd::default())
|
||||
.unwrap()
|
||||
.romoppvarming,
|
||||
);
|
||||
let to = Timeserie::sum_kwh(
|
||||
&simuler(&l, &stille_vaer(1.0), Systemtilskudd::default())
|
||||
.unwrap()
|
||||
.romoppvarming,
|
||||
);
|
||||
let forhold = to / en;
|
||||
assert!(
|
||||
(forhold - 2.0).abs() < 0.01,
|
||||
"forholdet skulle vært 2,0, men ble {forhold:.3}"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! Kontroll mot Standard Norges lanseringseksempel for NS 3031:2025 (Multiconsult, mai 2025):
|
||||
//! småhus med TEK17-standard i Oslo-klima.
|
||||
//!
|
||||
//! Eksempelet er beregnet med Simien 8. Varmetapsbudsjettet og de normerte energipostene
|
||||
//! gjengis her nøyaktig. Romoppvarmingen avviker, se kommentaren nederst i filen.
|
||||
|
||||
use enok::data::Datakilde;
|
||||
use enok::inndata;
|
||||
use enok::kjor;
|
||||
use std::path::Path;
|
||||
|
||||
fn kjor_referansebygg() -> kjor::Resultat {
|
||||
let b = inndata::les_bygning(Path::new("eksempler/tek17-smaahus.toml")).unwrap();
|
||||
kjor::kjor(&b, &Datakilde::default()).unwrap()
|
||||
}
|
||||
|
||||
fn post(r: &kjor::Resultat, nr: &str) -> f64 {
|
||||
let p = r.budsjett.poster.iter().find(|p| p.nr == nr).unwrap();
|
||||
p.netto / r.lost.bra
|
||||
}
|
||||
|
||||
fn naer(faktisk: f64, forventet: f64, toleranse: f64, hva: &str) {
|
||||
assert!(
|
||||
(faktisk - forventet).abs() <= toleranse,
|
||||
"{hva}: beregnet {faktisk:.2}, forventet {forventet:.2} (toleranse {toleranse})"
|
||||
);
|
||||
}
|
||||
|
||||
/// Varmetapsbudsjettet skal gjengi NS 3031:2025 Tabell 3 for TEK17-referansebygget.
|
||||
#[test]
|
||||
fn varmetapsbudsjett_som_publisert() {
|
||||
let r = kjor_referansebygg();
|
||||
let v = &r.varmetap;
|
||||
naer(v.vegger, 0.22, 0.005, "vegger");
|
||||
naer(v.tak, 0.07, 0.005, "tak");
|
||||
naer(v.gulv, 0.05, 0.005, "gulv");
|
||||
naer(v.vinduer_dorer, 0.20, 0.005, "vinduer og dører");
|
||||
naer(v.kuldebroer, 0.05, 0.005, "kuldebroer");
|
||||
naer(v.transmisjon, 0.59, 0.005, "transmisjon");
|
||||
naer(v.infiltrasjon, 0.04, 0.005, "infiltrasjon");
|
||||
naer(v.klimaskjerm, 0.63, 0.005, "klimaskjerm");
|
||||
naer(v.ventilasjon, 0.06, 0.005, "ventilasjon");
|
||||
naer(v.totalt, 0.69, 0.005, "totalt varmetapstall");
|
||||
}
|
||||
|
||||
/// De normerte energipostene skal treffe verdiene i NS 3031:2025 Tillegg A for småhus.
|
||||
#[test]
|
||||
fn normerte_energiposter_som_publisert() {
|
||||
let r = kjor_referansebygg();
|
||||
naer(post(&r, "2"), 25.1, 0.2, "varmtvann");
|
||||
naer(post(&r, "5"), 11.4, 0.2, "belysning");
|
||||
naer(post(&r, "6"), 17.5, 0.2, "teknisk utstyr");
|
||||
naer(post(&r, "3a"), 0.0, 0.01, "romkjøling");
|
||||
naer(post(&r, "3b"), 0.0, 0.01, "ventilasjonskjøling");
|
||||
}
|
||||
|
||||
/// Energikarakteren for et TEK17-bygg skal være B, jf. energimerkeforskriften § 10b.
|
||||
#[test]
|
||||
fn tek17_bygg_faar_karakter_b_eller_bedre() {
|
||||
let r = kjor_referansebygg();
|
||||
assert!(
|
||||
matches!(r.karakter.bokstav, "A" | "B"),
|
||||
"TEK17-referansebygget fikk karakter {}, forventet A eller B",
|
||||
r.karakter.bokstav
|
||||
);
|
||||
}
|
||||
|
||||
/// Regresjonsvakt for romoppvarming og samlet netto energibehov.
|
||||
///
|
||||
/// Det publiserte eksempelet oppgir 46,2 kWh/(m²·år) romoppvarming og 106,4 kWh/(m²·år)
|
||||
/// netto energibehov. enok beregner vesentlig lavere romoppvarming. Kontrollene som er gjort:
|
||||
///
|
||||
/// * solinnstrålingen på skrå og orienterte flater er sammenlignet med pvlib (Hay–Davies) og
|
||||
/// avviker under 1 % for alle himmelretninger i Oslo-klima;
|
||||
/// * varmetapet i stasjonær tilstand stemmer med U·A·ΔT innenfor 2 % (se `tests/motor.rs`);
|
||||
/// * årlig energibalanse går opp innenfor 2 %;
|
||||
/// * varmetapsbudsjettet og de normerte energipostene er identiske med de publiserte.
|
||||
///
|
||||
/// Avviket ligger dermed i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025 Tillegg A
|
||||
/// er ikke offentlig tilgjengelig, så de normerte solfaktorene, skjermingsforutsetningene og
|
||||
/// timesprofilene som Simien 8 bruker, lar seg ikke etterprøve her. Testen låser dagens verdier
|
||||
/// slik at endringer i motoren blir synlige, og oppdateres når standardens forutsetninger er
|
||||
/// tilgjengelige.
|
||||
#[test]
|
||||
fn regresjonsvakt_for_romoppvarming() {
|
||||
let r = kjor_referansebygg();
|
||||
naer(
|
||||
post(&r, "1a"),
|
||||
24.7,
|
||||
1.5,
|
||||
"romoppvarming (regresjon, publisert verdi 46,2)",
|
||||
);
|
||||
naer(
|
||||
r.budsjett.netto_spesifikk(r.lost.bra),
|
||||
84.1,
|
||||
2.0,
|
||||
"netto energibehov (regresjon, publisert verdi 106,4)",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user