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:
51 files changed
+10309
No files matched your search
@@ -0,0 +1,136 @@
|
||||
//! Leser EnergyPlus Weather (EPW) og gir 8760 timesverdier.
|
||||
//!
|
||||
//! Feltnumre følger EPW-spesifikasjonen (0-basert etter de åtte headerlinjene):
|
||||
//! 6 tørrkuletemperatur °C, 7 duggpunkt °C, 8 relativ fuktighet %, 9 lufttrykk Pa,
|
||||
//! 12 horisontal infrarød stråling W/m², 13 global horisontal W/m², 14 direkte normal W/m²,
|
||||
//! 15 diffus horisontal W/m², 20 vindretning °, 21 vindhastighet m/s.
|
||||
|
||||
use crate::{Result, TIMER};
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Vaerdata {
|
||||
pub sted: String,
|
||||
pub land: String,
|
||||
pub bredde: f64,
|
||||
pub lengde: f64,
|
||||
pub tidssone: f64,
|
||||
pub hoyde: f64,
|
||||
/// Utetemperatur, °C.
|
||||
pub t_ute: Vec<f64>,
|
||||
/// Relativ fuktighet, %.
|
||||
pub rf: Vec<f64>,
|
||||
/// Global horisontal solstråling, W/m².
|
||||
pub ghi: Vec<f64>,
|
||||
/// Direkte normal solstråling, W/m².
|
||||
pub dni: Vec<f64>,
|
||||
/// Diffus horisontal solstråling, W/m².
|
||||
pub dhi: Vec<f64>,
|
||||
/// Horisontal infrarød stråling fra himmelen, W/m².
|
||||
pub ir_himmel: Vec<f64>,
|
||||
/// Vindhastighet, m/s.
|
||||
pub vind: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Vaerdata {
|
||||
pub fn fra_fil(sti: &Path) -> Result<Self> {
|
||||
let innhold = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&innhold);
|
||||
Self::fra_tekst(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str) -> Result<Self> {
|
||||
let mut linjer = tekst.lines();
|
||||
let location = linjer.next().context("EPW mangler LOCATION-linje")?;
|
||||
let f: Vec<&str> = location.split(',').collect();
|
||||
if f.len() < 10 || !f[0].eq_ignore_ascii_case("LOCATION") {
|
||||
bail!("første linje er ikke en gyldig LOCATION-linje");
|
||||
}
|
||||
let tall =
|
||||
|i: usize| -> f64 { f.get(i).and_then(|s| s.trim().parse().ok()).unwrap_or(0.0) };
|
||||
let mut v = Vaerdata {
|
||||
sted: f[1].trim().to_string(),
|
||||
land: f.get(3).unwrap_or(&"").trim().to_string(),
|
||||
bredde: tall(6),
|
||||
lengde: tall(7),
|
||||
tidssone: tall(8),
|
||||
hoyde: tall(9),
|
||||
t_ute: Vec::with_capacity(TIMER),
|
||||
rf: Vec::with_capacity(TIMER),
|
||||
ghi: Vec::with_capacity(TIMER),
|
||||
dni: Vec::with_capacity(TIMER),
|
||||
dhi: Vec::with_capacity(TIMER),
|
||||
ir_himmel: Vec::with_capacity(TIMER),
|
||||
vind: Vec::with_capacity(TIMER),
|
||||
};
|
||||
// Sju gjenstående headerlinjer.
|
||||
for _ in 0..7 {
|
||||
linjer.next();
|
||||
}
|
||||
for linje in linjer {
|
||||
if linje.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let d: Vec<&str> = linje.split(',').collect();
|
||||
if d.len() < 22 {
|
||||
continue;
|
||||
}
|
||||
let g = |i: usize| -> f64 { d[i].trim().parse::<f64>().unwrap_or(0.0) };
|
||||
// EPW bruker 9999/999999 som «mangler».
|
||||
let mangler = |x: f64, grense: f64| if x >= grense { 0.0 } else { x };
|
||||
v.t_ute.push(g(6));
|
||||
v.rf.push(g(8));
|
||||
v.ir_himmel.push(mangler(g(12), 9999.0));
|
||||
v.ghi.push(mangler(g(13), 9999.0).max(0.0));
|
||||
v.dni.push(mangler(g(14), 9999.0).max(0.0));
|
||||
v.dhi.push(mangler(g(15), 9999.0).max(0.0));
|
||||
v.vind.push(mangler(g(21), 999.0));
|
||||
if v.t_ute.len() == TIMER {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if v.t_ute.len() != TIMER {
|
||||
bail!(
|
||||
"EPW inneholder {} timer, forventet {}",
|
||||
v.t_ute.len(),
|
||||
TIMER
|
||||
);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
pub fn aarsmiddeltemperatur(&self) -> f64 {
|
||||
self.t_ute.iter().sum::<f64>() / self.t_ute.len() as f64
|
||||
}
|
||||
|
||||
pub fn maanedsmiddeltemperatur(&self) -> [f64; 12] {
|
||||
let mut sum = [0.0; 12];
|
||||
let mut antall = [0.0; 12];
|
||||
for (t, v) in self.t_ute.iter().enumerate() {
|
||||
let m = crate::maaned_for_time(t);
|
||||
sum[m] += v;
|
||||
antall[m] += 1.0;
|
||||
}
|
||||
let mut ut = [0.0; 12];
|
||||
for m in 0..12 {
|
||||
ut[m] = if antall[m] > 0.0 {
|
||||
sum[m] / antall[m]
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Graddager under 17 °C, til informasjon i rapporten.
|
||||
pub fn graddager(&self, basis: f64) -> f64 {
|
||||
self.t_ute
|
||||
.chunks(24)
|
||||
.map(|d| {
|
||||
let m = d.iter().sum::<f64>() / d.len() as f64;
|
||||
(basis - m).max(0.0)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user