Files
enok/tests/motor.rs
T
blandClaude Opus 5 b6babde13a
test / test (push) Failing after 3s
Geometri fra IFC, grunnmodell etter ISO 13370 og uoppvarmede soner
Importen er prøvd mot en virkelig IFC-fil fra Vectorworks Architect 2026 og
avdekket tre feil og ett hull.

IFC-import
- Lengdeenheten hentes nå gjennom prosjektets enhetsoppsett. Filen inneholder to
  lengdeenheter, og oppslaget traff tilfeldig den ene.
- Arealer regnes av geometrien, siden modellen ikke har mengder for vegger og dekker.
  Trekantene grupperes etter retning, og den største gruppen er delens hovedflate.
  Metoden gir riktig areal også når vinduer er skåret ut av veggen.
- Materialsjikt eksporteres som hver sin geometridel. Deler som ligger i samme plan
  og overlapper hverandre telles én gang, ellers ble en vegg talt sju ganger.
- Himmelretningen følger flatenormalen i stedet for et anslag ut fra tyngdepunktet.
- Flater med samme konstruksjon og himmelretning slås sammen, og rommene listes
  med areal, siden IFC ikke sier hvilke som er oppvarmet.

Grunnen og kjelleren
- Flater mot grunnen regnes etter NS-EN ISO 13370. En kjellervegg deles i en del over
  terreng og en nedgravd del, etter høydene `over_terreng` og `dybde`.
- Uoppvarmede soner får egen temperatur time for time etter NS-EN ISO 13789, med
  varmetilskudd fra distribusjonstap og beredere som står der. Uisolerte varmerør i
  en kjeller teller dermed både som tap og som varme som holder etasjeskilleren varm.
- Varmetapsbudsjettet oppgir konstruksjonens egen U-verdi, slik NS 3031 Tabell 3 og
  energikravene i TEK er formulert, mens timesberegningen bruker grunnens demping.

Nordhusstykket 5 er modellert av IFC-geometrien med kjelleren som egen sone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-14 19:16:18 +02:00

324 lines
8.7 KiB
Rust

//! 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}"
);
}
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
);
}