Files
enok/src/modell/lost.rs
T

545 lines
18 KiB
Rust
Raw Normal View History

//! Oppløsning av inndata: fyller manglende felter med normerte verdier eller verdier utledet
//! av byggeår, og fører logg over hva som er antatt. Loggen gjengis i rapporten slik at leseren
//! ser hvilke tall som er dokumentert og hvilke som er normerte.
use crate::data::{Datakilde, Materialer, Normerte};
use crate::modell::*;
use crate::Result;
use anyhow::{bail, Context};
use serde::Serialize;
/// Én forutsetning som ikke kom fra inndatafilen.
#[derive(Serialize, Clone, Debug)]
pub struct Forutsetning {
pub felt: String,
pub verdi: String,
pub kilde: String,
}
#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Regulering {
/// Settpunktet gjelder operativ temperatur (middel av luft- og strålingstemperatur).
Operativ,
/// Settpunktet gjelder lufttemperatur.
Luft,
}
/// En ferdig oppløst flate klar for timesberegning.
#[derive(Serialize, Clone, Debug)]
pub struct LostFlate {
pub navn: String,
pub typ: FlateType,
pub areal: f64,
pub u_verdi: f64,
pub helning: f64,
pub orientering: f64,
pub kappa: f64,
pub masseplassering: Masseplassering,
pub mot: Mot,
pub b_faktor: f64,
pub absorptans: f64,
/// Sant for vinduer og dører (to noder, ingen masse, solinnstråling gjennom glasset).
pub glass: bool,
/// Effektiv solfaktor inkl. karmandel og skjerming (bare vinduer).
pub g_effektiv: f64,
/// Skjermingsfaktor når solskjerming er i bruk (1,0 = ingen skjerming).
pub skjermingsfaktor: f64,
pub automatisk_skjerming: bool,
pub horisont: f64,
}
#[derive(Serialize, Clone, Debug)]
pub struct Lost {
pub navn: String,
pub kategori: Kategori,
pub byggeaar: i32,
pub bra: f64,
pub volum: f64,
pub antall_boenheter: u32,
/// Areal som brukes i arealkorreksjonen på karakterskalaen.
pub skala_areal: f64,
pub flater: Vec<LostFlate>,
/// W/K, summert transmisjonstap uten kuldebroer.
pub kuldebro_w_per_k: f64,
pub n50: f64,
pub skjerming: Skjerming,
pub ventilasjon: LostVentilasjon,
pub laster: LostLaster,
pub settpunkt_oppvarming: f64,
pub settpunkt_natt: f64,
pub settpunkt_kjoling: Option<f64>,
/// Vinduslufting: (settpunkt °C, største luftmengde m³/(h·m²)).
pub lufting: Option<(f64, f64)>,
pub regulering: Regulering,
pub innvendig_varmekapasitet: f64,
pub maks_varmeeffekt: Option<f64>,
pub maks_kjoleeffekt: Option<f64>,
pub albedo: f64,
pub forutsetninger: Vec<Forutsetning>,
}
#[derive(Serialize, Clone, Debug)]
pub struct LostVentilasjon {
pub typ: VentilasjonType,
/// m³/(h·m²) i driftstiden.
pub luftmengde: f64,
pub luftmengde_natt: f64,
pub gjenvinning: f64,
pub sfp: f64,
pub varmebatteri: bool,
pub tilluft_settpunkt: f64,
pub driftstid: crate::data::Tidsrom,
}
/// Timesprofiler for interne laster, W/m² oppvarmet BRA.
#[derive(Serialize, Clone, Debug)]
pub struct LostLaster {
pub belysning: [f64; 24],
pub utstyr_effekt: [f64; 24],
pub utstyr_varme: [f64; 24],
pub personer: [f64; 24],
pub varmtvann: [f64; 24],
pub pumper: [f64; 24],
pub oppvarming_driftstid: crate::data::Tidsrom,
}
const R_SI: f64 = 0.13;
const R_SE: f64 = 0.04;
impl Lost {
pub fn fra(b: &Bygning, kilde: &Datakilde) -> Result<Self> {
let mut f = Vec::new();
let normerte = kilde.normerte(b.bygning.kategori)?;
let materialer = kilde.materialer()?;
let info = &b.bygning;
if info.oppvarmet_bra <= 0.0 {
bail!("oppvarmet_bra må være større enn null");
}
let volum = info.oppvarmet_volum.unwrap_or_else(|| {
f.push(Forutsetning {
felt: "bygning.oppvarmet_volum".into(),
verdi: format!("{:.0} m³", info.oppvarmet_bra * 2.4),
kilde: "antatt 2,4 m romhøyde".into(),
});
info.oppvarmet_bra * 2.4
});
let periode = materialer.periode_for(info.byggeaar);
let mut flater = Vec::new();
for fl in &b.flate {
flater.push(los_flate(fl, &materialer, periode, &mut f)?);
}
for v in &b.vindu {
flater.push(los_vindu(v, &mut f));
}
if flater.is_empty() {
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
}
// Kuldebroer.
let kuldebro_w_per_k = if !b.kuldebroer.liste.is_empty() {
b.kuldebroer.liste.iter().map(|k| k.psi * k.lengde).sum()
} else {
let n = b.kuldebroer.normalisert.unwrap_or_else(|| {
let v = periode
.map(|p| p.kuldebro)
.unwrap_or(normerte.termisk.kuldebroer_normalisert);
f.push(Forutsetning {
felt: "kuldebroer.normalisert".into(),
verdi: format!("{v:.2} W/(m²K)"),
kilde: periode
.map(|p| format!("typisk for {}", p.navn))
.unwrap_or_else(|| "normert verdi".into()),
});
v
});
n * info.oppvarmet_bra
};
// Lekkasjetall.
let n50 = b.infiltrasjon.n50.unwrap_or_else(|| {
let v = periode.map(|p| p.n50).unwrap_or(3.0);
f.push(Forutsetning {
felt: "infiltrasjon.n50".into(),
verdi: format!("{v:.1} h⁻¹"),
kilde: periode
.map(|p| format!("typisk for {}", p.navn))
.unwrap_or_else(|| "antatt".into()),
});
v
});
let ventilasjon = los_ventilasjon(&b.ventilasjon, &normerte, &mut f);
let laster = los_laster(b, &normerte, &mut f);
let settpunkt_oppvarming = b
.drift
.settpunkt_oppvarming
.unwrap_or(normerte.settpunkt.oppvarming);
let settpunkt_natt = b
.drift
.nattsenking
.unwrap_or(normerte.settpunkt.oppvarming_natt);
let settpunkt_kjoling = if b.kjoling.ingen {
None
} else {
Some(b.kjoling.settpunkt.unwrap_or(normerte.settpunkt.kjoling))
};
let innvendig_varmekapasitet = b.drift.innvendig_varmekapasitet.unwrap_or(10_000.0);
// Vinduslufting brukes når bygningen ikke har kjøleanlegg.
let lufting = if b.lufting.ingen || settpunkt_kjoling.is_some() {
None
} else {
// Uten oppgitt grense brukes den normerte settpunkttemperaturen for kjøling.
// NS 3031 setter denne også for bygninger uten kjøleanlegg; overskuddsvarmen
// antas luftet ut, og energien telles ikke.
let sett = b.lufting.settpunkt.unwrap_or_else(|| {
f.push(Forutsetning {
felt: "lufting.settpunkt".into(),
verdi: format!("{:.0} °C", normerte.settpunkt.kjoling),
kilde:
"normert settpunkttemperatur for kjøling; bygningen har ikke kjøleanlegg"
.into(),
});
normerte.settpunkt.kjoling
});
let maks = b.lufting.maks_luftmengde.unwrap_or_else(|| {
f.push(Forutsetning {
felt: "lufting.maks_luftmengde".into(),
verdi: "8 m³/(h·m²)".into(),
kilde: "antatt største luftmengde ved vinduslufting".into(),
});
8.0
});
Some((sett, maks))
};
let skala_areal = match info.kategori {
Kategori::Smaahus => info.bygningens_oppvarmede_bra.unwrap_or(info.oppvarmet_bra),
Kategori::Boligblokk => info
.bygningens_oppvarmede_bra
.map(|a| a / info.antall_boenheter.max(1) as f64)
.unwrap_or_else(|| {
if info.antall_boenheter > 1 {
info.oppvarmet_bra / info.antall_boenheter as f64
} else {
info.oppvarmet_bra
}
}),
_ => info.oppvarmet_bra,
};
Ok(Lost {
navn: info.navn.clone(),
kategori: info.kategori,
byggeaar: info.byggeaar,
bra: info.oppvarmet_bra,
volum,
antall_boenheter: info.antall_boenheter.max(1),
skala_areal,
flater,
kuldebro_w_per_k,
n50,
skjerming: b.infiltrasjon.skjerming,
ventilasjon,
laster,
settpunkt_oppvarming,
settpunkt_natt,
settpunkt_kjoling,
lufting,
regulering: Regulering::Operativ,
innvendig_varmekapasitet,
maks_varmeeffekt: b.oppvarming.maks_effekt,
maks_kjoleeffekt: b.kjoling.maks_effekt,
albedo: b.klima.albedo.unwrap_or(0.2),
forutsetninger: f,
})
}
/// Samlet areal mot det fri og mot grunn/uoppvarmet (klimaskjermareal), m².
pub fn klimaskjermareal(&self) -> f64 {
self.flater.iter().map(|f| f.areal).sum()
}
pub fn formfaktor(&self) -> f64 {
self.klimaskjermareal() / self.bra
}
pub fn vindusareal(&self) -> f64 {
self.flater
.iter()
.filter(|f| f.glass)
.map(|f| f.areal)
.sum()
}
}
fn los_flate(
fl: &Flate,
materialer: &Materialer,
periode: Option<&crate::data::Periode>,
logg: &mut Vec<Forutsetning>,
) -> Result<LostFlate> {
if fl.areal <= 0.0 {
bail!("flaten «{}» har areal {} m²", fl.navn, fl.areal);
}
// U-verdi: oppgitt, utledet av lag, eller typisk for byggeperioden.
let (u, kappa_lag) = if let Some(u) = fl.u_verdi {
(u, None)
} else if let Some(lag) = &fl.lag {
let mut r = R_SI + R_SE;
let mut kappa = 0.0;
for l in lag {
let m = materialer.materiale(&l.materiale).with_context(|| {
format!("ukjent materiale «{}» i flaten «{}»", l.materiale, fl.navn)
})?;
r += l.tykkelse / m.lambda;
kappa += l.tykkelse * m.rho * m.c;
}
(1.0 / r, Some(kappa))
} else {
let v = periode
.map(|p| match fl.typ {
FlateType::Vegg => p.vegg,
FlateType::Tak => p.tak,
FlateType::Gulv => p.gulv,
})
.with_context(|| format!("flaten «{}» mangler u_verdi og lag", fl.navn))?;
logg.push(Forutsetning {
felt: format!("flate.{}.u_verdi", fl.navn),
verdi: format!("{v:.2} W/(m²K)"),
kilde: periode
.map(|p| format!("typisk for {}", p.navn))
.unwrap_or_default(),
});
(v, None)
};
let helning = fl.helning.unwrap_or(match fl.typ {
FlateType::Vegg => 90.0,
FlateType::Tak => 0.0,
FlateType::Gulv => 180.0,
});
let kappa = match (kappa_lag, fl.masseklasse) {
(_, Some(k)) => k.kappa(),
(Some(k), None) => k,
(None, None) => {
let k = Masseklasse::Middels.kappa();
logg.push(Forutsetning {
felt: format!("flate.{}.masseklasse", fl.navn),
verdi: "middels".into(),
kilde: "ISO 52016-1 tabell B.14".into(),
});
k
}
};
let b_faktor = match fl.mot {
Mot::Ute => 1.0,
Mot::Grunn => 1.0, // håndteres med egen bakketemperatur i timesberegningen
Mot::Uoppvarmet => fl.b_faktor.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: format!("flate.{}.b_faktor", fl.navn),
verdi: "0,5".into(),
kilde: "NS-EN ISO 13789, uoppvarmet rom".into(),
});
0.5
}),
};
Ok(LostFlate {
navn: fl.navn.clone(),
typ: fl.typ,
areal: fl.areal,
u_verdi: u,
helning,
orientering: fl.orientering,
kappa,
masseplassering: fl.masseplassering,
mot: fl.mot,
b_faktor,
absorptans: fl.absorptans.unwrap_or(0.6),
glass: false,
g_effektiv: 0.0,
skjermingsfaktor: 1.0,
automatisk_skjerming: false,
horisont: 0.0,
})
}
fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
let areal = v.areal * v.antall;
let karm = v.karmandel.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: format!("vindu.{}.karmandel", v.navn),
verdi: "0,20".into(),
kilde: "normert karm- og rammeandel".into(),
});
0.20
});
let g = if v.dor {
0.0
} else {
v.g_verdi.unwrap_or_else(|| {
let antatt = if v.u_verdi <= 1.2 { 0.50 } else { 0.75 };
logg.push(Forutsetning {
felt: format!("vindu.{}.g_verdi", v.navn),
verdi: format!("{antatt:.2}"),
kilde: if v.u_verdi <= 1.2 {
"antatt 3-lags energiglass"
} else {
"antatt 2-lags glass"
}
.into(),
});
antatt
})
};
// ISO 52016-1: F_w = 0,9 korrigerer for ikke-vinkelrett innfall over året.
let g_effektiv = g * (1.0 - karm) * 0.9;
let (skjermingsfaktor, automatisk) = match &v.solskjerming {
None => (1.0, false),
Some(s) => match s.typ {
SolskjermingType::Ingen => (1.0, false),
SolskjermingType::Manuell => (s.faktor.unwrap_or(0.8), false),
SolskjermingType::Automatisk => (s.faktor.unwrap_or(0.4), true),
},
};
LostFlate {
navn: v.navn.clone(),
typ: FlateType::Vegg,
areal,
u_verdi: v.u_verdi,
helning: v.helning,
orientering: v.orientering,
kappa: 0.0,
masseplassering: Masseplassering::Fordelt,
mot: Mot::Ute,
b_faktor: 1.0,
absorptans: 0.0,
glass: true,
g_effektiv,
skjermingsfaktor,
automatisk_skjerming: automatisk,
horisont: v.horisont.unwrap_or(0.0),
}
}
fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostVentilasjon {
let luftmengde = v.luftmengde.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: "ventilasjon.luftmengde".into(),
verdi: format!("{:.2} m³/(h·m²)", n.ventilasjon.luftmengde_driftstid),
kilde: format!("normert verdi, {}", n.kilde),
});
n.ventilasjon.luftmengde_driftstid
});
let luftmengde_natt = v
.luftmengde_natt
.unwrap_or(n.ventilasjon.luftmengde_utenfor_driftstid);
let gjenvinning = match v.typ {
VentilasjonType::Balansert => v.gjenvinning.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: "ventilasjon.gjenvinning".into(),
verdi: format!("{:.0} %", n.ventilasjon.temperaturvirkningsgrad * 100.0),
kilde: "normert temperaturvirkningsgrad".into(),
});
n.ventilasjon.temperaturvirkningsgrad
}),
_ => 0.0,
};
let sfp = v.sfp.unwrap_or(match v.typ {
VentilasjonType::Balansert => n.ventilasjon.sfp,
VentilasjonType::Avtrekk => 0.6,
VentilasjonType::Naturlig => 0.0,
});
let varmebatteri = v
.varmebatteri
.unwrap_or(matches!(v.typ, VentilasjonType::Balansert));
LostVentilasjon {
typ: v.typ,
luftmengde,
luftmengde_natt,
gjenvinning,
sfp,
varmebatteri,
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
driftstid: n.driftstid.ventilasjon,
}
}
fn profil(effekt: f64, tid: crate::data::Tidsrom, egen: Option<[f64; 24]>) -> [f64; 24] {
if let Some(p) = egen {
return p;
}
let mut ut = [0.0; 24];
for (h, v) in ut.iter_mut().enumerate() {
if tid.aktiv(h) {
*v = effekt;
}
}
ut
}
fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLaster {
let bel = b.drift.belysning_effekt.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: "belysning".into(),
verdi: format!(
"{:.2} W/m² i {:.0} h/døgn",
n.belysning.effekt,
n.driftstid.belysning.timer()
),
kilde: format!("normert, {}", n.kilde),
});
n.belysning.effekt
});
let utstyr = b.drift.utstyr_effekt.unwrap_or(n.utstyr.effekt);
// Oppgitt utstyrseffekt skalerer varmetilskuddet i samme forhold som den normerte verdien.
let utstyr_varme = match b.drift.utstyr_effekt {
Some(e) => e * (n.utstyr.varmetilskudd / n.utstyr.effekt.max(1e-9)),
None => n.utstyr.varmetilskudd,
};
let personer = b
.drift
.personer_varmetilskudd
.unwrap_or(n.personer.varmetilskudd);
let tappevann = b.tappevann.energibehov.map(|kwh| {
// kWh/(m²·år) → W/m² i driftstiden.
kwh * 1000.0 / (365.0 * n.driftstid.varmtvann.timer())
});
let vv = tappevann.unwrap_or_else(|| {
logg.push(Forutsetning {
felt: "tappevann.energibehov".into(),
verdi: format!(
"{:.1} kWh/(m²·år)",
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * 365.0 / 1000.0
),
kilde: format!("normert, {}", n.kilde),
});
n.varmtvann.effekt
});
let pumpeeffekt = match b.distribusjon.typ {
DistribusjonType::Punkt => b.distribusjon.pumpe_effekt.unwrap_or(0.0),
_ => b
.distribusjon
.pumpe_effekt
.unwrap_or(n.pumper.effekt_vannbaaren),
};
LostLaster {
belysning: profil(bel, n.driftstid.belysning, n.profil_belysning),
utstyr_effekt: profil(utstyr, n.driftstid.utstyr, n.profil_utstyr),
utstyr_varme: profil(utstyr_varme, n.driftstid.utstyr, None),
personer: profil(personer, n.driftstid.personer, n.profil_personer),
varmtvann: profil(vv, n.driftstid.varmtvann, n.profil_varmtvann),
pumper: profil(pumpeeffekt, n.driftstid.oppvarming, None),
oppvarming_driftstid: n.driftstid.oppvarming,
}
}