//! 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::motor::grunn; 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, /// U-verdien som brukes i timesberegningen. Mot grunnen er den korrigert etter /// NS-EN ISO 13370 og dermed lavere enn konstruksjonens egen. pub u_verdi: f64, /// Konstruksjonens egen U-verdi, som varmetapsbudsjettet og TEK-kravene gjelder. pub u_konstruksjon: 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, /// Indeks til den uoppvarmede sonen flaten vender mot, når temperaturen beregnes. pub sone: Option, /// 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, } /// En uoppvarmet sone med varmebalanse etter NS-EN ISO 13789. #[derive(Serialize, Clone, Debug)] pub struct LostSone { pub navn: String, /// Varmetransport fra den oppvarmede delen inn i sonen, W/K. pub h_mot_oppvarmet: f64, /// Varmetransport fra sonen til uteluft, inkludert ventilasjon, W/K. pub h_mot_ute: f64, /// Varmetransport fra sonen til grunnen, W/K. pub h_mot_grunn: f64, /// Andel av varmeanleggets distribusjonstap som avgis i sonen. pub andel_distribusjonstap: f64, /// Andel av tappevannsanleggets tap som avgis i sonen. pub andel_tappevannstap: f64, /// Fast varmetilskudd, W. pub fast_effekt: f64, } impl LostSone { /// Sonens temperatur, °C, av den stasjonære varmebalansen. pub fn temperatur(&self, t_inne: f64, t_ute: f64, t_grunn: f64, tilskudd: f64) -> f64 { let h = self.h_mot_oppvarmet + self.h_mot_ute + self.h_mot_grunn; if h <= 1e-9 { return t_ute; } (self.h_mot_oppvarmet * t_inne + self.h_mot_ute * t_ute + self.h_mot_grunn * t_grunn + tilskudd) / h } } #[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, pub soner: Vec, /// 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, /// 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, pub maks_kjoleeffekt: Option, pub albedo: f64, pub forutsetninger: Vec, } #[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, /// Kjølebatteri: tilluften holdes på denne temperaturen i driftstiden. pub tilluft_kjoling: Option, 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], /// Driftstider med driftsdager og driftsuker. Profilene over gjelder bare på driftsdager. pub driftstider: crate::data::Driftstider, } impl LostLaster { /// Om time `t` i året faller på en driftsdag. pub fn driftsdag(&self, t: usize) -> bool { self.driftstider.driftsdag(t / 24) } } const R_SI: f64 = 0.13; const R_SE: f64 = 0.04; impl Lost { pub fn fra(b: &Bygning, kilde: &Datakilde) -> Result { 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 ledningsevne = b.grunn.ledningsevne.unwrap_or_else(|| { if b.flate .iter() .chain(b.sone.iter().flat_map(|s| s.flater.iter())) .any(|x| x.mot == Mot::Grunn) { f.push(Forutsetning { felt: "grunn.ledningsevne".into(), verdi: format!("{:.1} W/(m·K)", grunn::LEDNINGSEVNE_JORD), kilde: "NS-EN ISO 13370 tabell 1, leire eller silt".into(), }); } grunn::LEDNINGSEVNE_JORD }); // Sonenavn må være kjent før flatene løses, slik at flatene kan peke på dem. let sonenavn: Vec = b.sone.iter().map(|s| s.navn.clone()).collect(); for fl in &b.flate { if let Some(n) = &fl.sone { if !sonenavn.contains(n) { bail!("flaten «{}» viser til den ukjente sonen «{}»", fl.navn, n); } } } let mut flater = Vec::new(); for fl in &b.flate { los_flate_med_grunn( fl, &b.flate, &materialer, periode, ledningsevne, &sonenavn, &mut flater, &mut f, )?; } for v in &b.vindu { flater.push(los_vindu(v, periode, &mut f)); } if flater.is_empty() { bail!("bygningen har ingen flater; legg inn minst én [[flate]]"); } // Uoppvarmede soner. let mut soner = Vec::new(); for (i, sone) in b.sone.iter().enumerate() { let mut egne = Vec::new(); for fl in &sone.flater { los_flate_med_grunn( fl, &sone.flater, &materialer, periode, ledningsevne, &[], &mut egne, &mut f, )?; } let h_mot_oppvarmet: f64 = flater .iter() .filter(|x| x.sone == Some(i)) .map(|x| x.areal * x.u_verdi) .sum(); if h_mot_oppvarmet <= 1e-9 { bail!( "sonen «{}» har ingen flater mot den oppvarmede delen; sett `sone = \"{}\"` på etasjeskilleren", sone.navn, sone.navn ); } let volum = sone.volum.unwrap_or(0.0); let luftveksling = sone.luftveksling.unwrap_or_else(|| { f.push(Forutsetning { felt: format!("sone.{}.luftveksling", sone.navn), verdi: "0,3 h⁻¹".into(), kilde: "antatt luftveksling i uoppvarmet sone".into(), }); 0.3 }); let h_mot_ute: f64 = egne .iter() .filter(|x| x.mot == Mot::Ute) .map(|x| x.areal * x.u_verdi) .sum::() + volum * luftveksling * crate::motor::RHO_C_LUFT; let h_mot_grunn: f64 = egne .iter() .filter(|x| x.mot == Mot::Grunn) .map(|x| x.areal * x.u_verdi) .sum(); soner.push(LostSone { navn: sone.navn.clone(), h_mot_oppvarmet, h_mot_ute, h_mot_grunn, andel_distribusjonstap: sone.andel_distribusjonstap.unwrap_or(1.0), andel_tappevannstap: sone.andel_tappevannstap.unwrap_or(0.0), fast_effekt: sone.fast_effekt, }); } // 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() || ventilasjon.tilluft_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, soner, 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() } } /// Løser en flate, og deler kjellervegger i en del over og en del under terreng. /// /// Mot grunnen brukes NS-EN ISO 13370: jordas varmemotstand gjør at en uisolert kjellervegg /// eller et uisolert kjellergulv taper langt mindre varme enn U-verdien alene skulle tilsi. /// Den delen av kjellerveggen som står over terreng er derimot en vanlig yttervegg, og /// arealet deles mellom de to i forhold til høydene. #[allow(clippy::too_many_arguments)] fn los_flate_med_grunn( fl: &Flate, naboflater: &[Flate], materialer: &Materialer, periode: Option<&crate::data::Periode>, ledningsevne: f64, sonenavn: &[String], ut: &mut Vec, logg: &mut Vec, ) -> Result<()> { let mut lost = los_flate(fl, materialer, periode, logg)?; lost.sone = fl .sone .as_ref() .and_then(|n| sonenavn.iter().position(|x| x == n)); if fl.mot != Mot::Grunn { ut.push(lost); return Ok(()); } // Gulvet mot grunnen gir den ekvivalente tykkelsen som kjellerveggene regnes mot. let gulv = naboflater .iter() .filter(|x| x.mot == Mot::Grunn && x.typ == FlateType::Gulv) .max_by(|a, b| { a.areal .partial_cmp(&b.areal) .unwrap_or(std::cmp::Ordering::Equal) }); let (u_gulv_k, areal_gulv, omkrets) = match gulv { Some(g) => ( los_flate(g, materialer, periode, &mut Vec::new())?.u_verdi, g.areal, g.omkrets.unwrap_or(0.0), ), None => (lost.u_verdi, fl.areal, fl.omkrets.unwrap_or(0.0)), }; match fl.typ { FlateType::Gulv => { let dybde = fl.dybde.unwrap_or(0.0); if fl.omkrets.is_none() { logg.push(Forutsetning { felt: format!("flate.{}.omkrets", fl.navn), verdi: format!("{:.1} m", 4.0 * fl.areal.sqrt()), kilde: "antatt kvadratisk gulv; oppgi omkrets for riktig B′".into(), }); } let u = grunn::u_gulv( lost.u_verdi, fl.areal, fl.omkrets.unwrap_or(0.0), dybde, ledningsevne, ); logg.push(Forutsetning { felt: format!("flate.{}.u_mot_grunn", fl.navn), verdi: format!("{u:.3} W/(m²K)"), kilde: format!( "NS-EN ISO 13370 av konstruksjonens {:.2} W/(m²K) og {dybde:.1} m dybde", lost.u_verdi ), }); lost.u_verdi = u; ut.push(lost); } FlateType::Vegg | FlateType::Tak => { let dybde = fl.dybde.unwrap_or(0.0); let over = fl.over_terreng.unwrap_or(0.0); let hoyde = dybde + over; let andel_over = if hoyde > 1e-6 { over / hoyde } else { 0.0 }; if fl.dybde.is_none() { logg.push(Forutsetning { felt: format!("flate.{}.dybde", fl.navn), verdi: "0 m".into(), kilde: "uten oppgitt dybde regnes veggen som å stå i terrengnivå".into(), }); } if andel_over > 1e-6 { let mut over_flate = lost.clone(); over_flate.navn = format!("{} (over terreng)", lost.navn); over_flate.areal = fl.areal * andel_over; over_flate.mot = Mot::Ute; ut.push(over_flate); } if andel_over < 1.0 - 1e-6 { let u = grunn::u_kjellervegg( lost.u_verdi, u_gulv_k, areal_gulv, omkrets, dybde, ledningsevne, ); logg.push(Forutsetning { felt: format!("flate.{}.u_mot_grunn", fl.navn), verdi: format!("{u:.3} W/(m²K)"), kilde: format!( "NS-EN ISO 13370 av konstruksjonens {:.2} W/(m²K) og {dybde:.1} m under terreng", lost.u_verdi ), }); let mut under = lost; if andel_over > 1e-6 { under.navn = format!("{} (under terreng)", under.navn); } under.areal = fl.areal * (1.0 - andel_over); under.u_verdi = u; ut.push(under); } } } Ok(()) } fn los_flate( fl: &Flate, materialer: &Materialer, periode: Option<&crate::data::Periode>, logg: &mut Vec, ) -> Result { 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, u_konstruksjon: u, helning, orientering: fl.orientering, kappa, masseplassering: fl.masseplassering, mot: fl.mot, b_faktor, absorptans: fl.absorptans.unwrap_or(0.6), glass: false, sone: None, g_effektiv: 0.0, skjermingsfaktor: 1.0, automatisk_skjerming: false, horisont: 0.0, }) } fn los_vindu( v: &Vindu, periode: Option<&crate::data::Periode>, logg: &mut Vec, ) -> LostFlate { let areal = v.areal * v.antall; // U-verdi: oppgitt, eller typisk for byggeperioden. Et vindu uten oppgitt U-verdi er // vanligvis originalvinduet, og da sier byggeåret mer enn en fast standardverdi. let u = v.u_verdi.unwrap_or_else(|| { let antatt = periode.map(|p| p.vindu).unwrap_or(2.8); logg.push(Forutsetning { felt: format!("vindu.{}.u_verdi", v.navn), verdi: format!("{antatt:.2} W/(m²K)"), kilde: periode .map(|p| format!("typisk for {}", p.navn)) .unwrap_or_else(|| "antatt".into()), }); antatt }); 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 u <= 1.2 { 0.50 } else { 0.75 }; logg.push(Forutsetning { felt: format!("vindu.{}.g_verdi", v.navn), verdi: format!("{antatt:.2}"), kilde: if u <= 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: u, u_konstruksjon: u, helning: v.helning, orientering: v.orientering, kappa: 0.0, masseplassering: Masseplassering::Fordelt, mot: Mot::Ute, b_faktor: 1.0, absorptans: 0.0, glass: true, sone: None, g_effektiv, skjermingsfaktor, automatisk_skjerming: automatisk, horisont: v.horisont.unwrap_or(0.0), } } fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec) -> 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)); // Kjølebatteriet holder konstant tilluftstemperatur i driftstiden, slik DiBKs modellbygg // for energirammene er regnet (Multiconsult 2014, avsnitt 7.3). let tilluft_kjoling = (v.kjolebatteri == Some(true)).then(|| { v.tilluft_kjoling.unwrap_or_else(|| { let t = n .settpunkt .ventilasjon_tilluft_kjoling .unwrap_or(n.settpunkt.ventilasjon_tilluft); logg.push(Forutsetning { felt: "ventilasjon.tilluft_kjoling".into(), verdi: format!("{t:.0} °C"), kilde: format!("normert tilluftstemperatur, {}", n.kilde), }); t }) }); LostVentilasjon { typ: v.typ, luftmengde, luftmengde_natt, gjenvinning, sfp, varmebatteri, tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft, tilluft_kjoling, 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) -> 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, {} dager/uke, {} uker/år", n.belysning.effekt, n.driftstid.belysning.timer(), n.driftstid.dager_per_uke, n.driftstid.uker_per_aar ), 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 driftsdager = n.driftstid.driftsdager_per_aar() as f64; let tappevann = b.tappevann.energibehov.map(|kwh| { // kWh/(m²·år) → W/m² i driftstiden på driftsdager. kwh * 1000.0 / (driftsdager * 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() * driftsdager / 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), driftstider: n.driftstid.clone(), } }