2026-09-14 18:03:00 +02:00
|
|
|
//! 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}"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-09-14 19:16:18 +02:00
|
|
|
|
|
|
|
|
const MED_KJELLER: &str = r#"
|
|
|
|
|
[bygning]
|
|
|
|
|
navn = "Hus med kjeller"
|
|
|
|
|
kategori = "smaahus"
|
|
|
|
|
byggeaar = 1950
|
|
|
|
|
oppvarmet_bra = 100.0
|
|
|
|
|
oppvarmet_volum = 240.0
|
|
|
|
|
|
|
|
|
|
[[flate]]
|
|
|
|
|
navn = "Yttervegg"
|
|
|
|
|
type = "vegg"
|
|
|
|
|
areal = 120.0
|
|
|
|
|
u_verdi = 1.0
|
|
|
|
|
masseklasse = "lett"
|
|
|
|
|
|
|
|
|
|
[[flate]]
|
|
|
|
|
navn = "Tak"
|
|
|
|
|
type = "tak"
|
|
|
|
|
areal = 100.0
|
|
|
|
|
u_verdi = 0.5
|
|
|
|
|
masseklasse = "lett"
|
|
|
|
|
|
|
|
|
|
[[flate]]
|
|
|
|
|
navn = "Etasjeskiller mot kjeller"
|
|
|
|
|
type = "gulv"
|
|
|
|
|
areal = 100.0
|
|
|
|
|
u_verdi = 0.8
|
|
|
|
|
mot = "uoppvarmet"
|
|
|
|
|
sone = "Kjeller"
|
|
|
|
|
masseklasse = "middels"
|
|
|
|
|
|
|
|
|
|
[[sone]]
|
|
|
|
|
navn = "Kjeller"
|
|
|
|
|
volum = 220.0
|
|
|
|
|
luftveksling = 0.4
|
|
|
|
|
andel_distribusjonstap = 1.0
|
|
|
|
|
fast_effekt = 0.0
|
|
|
|
|
|
|
|
|
|
[[sone.flate]]
|
|
|
|
|
navn = "Kjellermur"
|
|
|
|
|
type = "vegg"
|
|
|
|
|
areal = 90.0
|
|
|
|
|
u_verdi = 3.0
|
|
|
|
|
mot = "grunn"
|
|
|
|
|
dybde = 1.6
|
|
|
|
|
over_terreng = 0.6
|
|
|
|
|
|
|
|
|
|
[[sone.flate]]
|
|
|
|
|
navn = "Kjellergulv"
|
|
|
|
|
type = "gulv"
|
|
|
|
|
areal = 100.0
|
|
|
|
|
u_verdi = 3.5
|
|
|
|
|
mot = "grunn"
|
|
|
|
|
omkrets = 40.0
|
|
|
|
|
dybde = 2.2
|
|
|
|
|
|
|
|
|
|
[kuldebroer]
|
|
|
|
|
normalisert = 0.0
|
|
|
|
|
|
|
|
|
|
[infiltrasjon]
|
|
|
|
|
n50 = 4.0
|
|
|
|
|
|
|
|
|
|
[ventilasjon]
|
|
|
|
|
type = "naturlig"
|
|
|
|
|
luftmengde = 1.2
|
|
|
|
|
|
|
|
|
|
[[oppvarming.kilde]]
|
|
|
|
|
type = "el_kjel"
|
|
|
|
|
andel_rom = 1.0
|
|
|
|
|
andel_tappevann = 1.0
|
|
|
|
|
|
|
|
|
|
[distribusjon]
|
|
|
|
|
type = "radiator"
|
|
|
|
|
roer_uoppvarmet = 1.0
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
fn med_kjeller() -> (Lost, Vaerdata) {
|
|
|
|
|
let b: Bygning = toml::from_str(MED_KJELLER).unwrap();
|
|
|
|
|
let l = Lost::fra(&b, &Datakilde::default()).unwrap();
|
|
|
|
|
(l, stille_vaer(-5.0))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Kjellertemperaturen skal ligge mellom ute og inne, og beregnes for hver time.
|
|
|
|
|
#[test]
|
|
|
|
|
fn kjelleren_ligger_mellom_ute_og_inne() {
|
|
|
|
|
let (l, v) = med_kjeller();
|
|
|
|
|
assert_eq!(l.soner.len(), 1);
|
|
|
|
|
let ts = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
|
|
|
|
let t_kjeller = &ts.sonetemperatur[0];
|
|
|
|
|
let snitt = t_kjeller.iter().sum::<f64>() / t_kjeller.len() as f64;
|
|
|
|
|
assert!(
|
|
|
|
|
snitt > -5.0 && snitt < 21.0,
|
|
|
|
|
"kjellertemperaturen ble {snitt:.1} °C"
|
|
|
|
|
);
|
|
|
|
|
// Uten varmetilskudd holder etasjeskilleren kjelleren godt over utetemperaturen.
|
|
|
|
|
assert!(snitt > 0.0, "kjellertemperaturen ble {snitt:.1} °C");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Varme fra uisolerte rør i kjelleren skal heve kjellertemperaturen og redusere
|
|
|
|
|
/// varmetapet gjennom etasjeskilleren, slik termografirapporter på slike hus beskriver.
|
|
|
|
|
#[test]
|
|
|
|
|
fn varme_ror_i_kjelleren_reduserer_romoppvarmingen() {
|
|
|
|
|
let (l, v) = med_kjeller();
|
|
|
|
|
let uten = simuler(&l, &v, Systemtilskudd::default()).unwrap();
|
|
|
|
|
let med = simuler(
|
|
|
|
|
&l,
|
|
|
|
|
&v,
|
|
|
|
|
Systemtilskudd {
|
|
|
|
|
tappevann: 0.0,
|
|
|
|
|
sone_fast: vec![1500.0],
|
|
|
|
|
sone_andel_romvarme: vec![0.0],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let t_uten = uten.sonetemperatur[0].iter().sum::<f64>() / TIMER as f64;
|
|
|
|
|
let t_med = med.sonetemperatur[0].iter().sum::<f64>() / TIMER as f64;
|
|
|
|
|
assert!(
|
|
|
|
|
t_med > t_uten + 2.0,
|
|
|
|
|
"kjelleren ble bare {t_med:.1} mot {t_uten:.1} °C"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let varme_uten = Timeserie::sum_kwh(&uten.romoppvarming);
|
|
|
|
|
let varme_med = Timeserie::sum_kwh(&med.romoppvarming);
|
|
|
|
|
assert!(
|
|
|
|
|
varme_med < varme_uten,
|
|
|
|
|
"romoppvarmingen økte: {varme_med:.0} mot {varme_uten:.0} kWh"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Grunnen demper varmetapet fra kjelleren: en uisolert kjellermur under terreng skal
|
|
|
|
|
/// gi lavere varmetransport enn den samme muren over terreng.
|
|
|
|
|
#[test]
|
|
|
|
|
fn nedgravd_mur_taper_mindre_enn_mur_over_terreng() {
|
|
|
|
|
let (l, _) = med_kjeller();
|
|
|
|
|
let sone = &l.soner[0];
|
|
|
|
|
// Muren er 90 m² med U = 3,0. Over terreng ville hele gitt 270 W/K.
|
|
|
|
|
assert!(
|
|
|
|
|
sone.h_mot_grunn < 150.0,
|
|
|
|
|
"grunnen dempet ikke nok: {} W/K",
|
|
|
|
|
sone.h_mot_grunn
|
|
|
|
|
);
|
|
|
|
|
// Den delen som står over terreng skal fortsatt telle fullt.
|
|
|
|
|
assert!(
|
|
|
|
|
sone.h_mot_ute > 40.0,
|
|
|
|
|
"delen over terreng mangler: {} W/K",
|
|
|
|
|
sone.h_mot_ute
|
|
|
|
|
);
|
|
|
|
|
}
|