Files
enok/src/modell/lost.rs
T
blandClaude Opus 5 32ecb0b132 Beregn rett fra en BIM-modell, uten bygning.toml
«enok beregn modell.ifc» leser modellen, fletter inn en eventuell tilleggsfil og skriver
bygningsbeskrivelsen den kom fram til, i utdatakatalogen, slik at beregningen kan etterprøves.

Importen sier nå hvilke bygningsdeler som mangler U-verdi, ikke bare hvor mange. Vinduer og
dører uten U-verdi i modellen får en typisk verdi for byggeåret, slik flatene alltid har fått,
i stedet for faste 2,4 og 2,0 W/(m²K).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-16 14:39:58 +02:00

857 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<usize>,
/// 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<LostFlate>,
pub soner: Vec<LostSone>,
/// 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,
/// Kjølebatteri: tilluften holdes på denne temperaturen i driftstiden.
pub tilluft_kjoling: Option<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],
/// 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<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 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<String> = 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::<f64>()
+ 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<LostFlate>,
logg: &mut Vec<Forutsetning>,
) -> 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<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,
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<Forutsetning>,
) -> 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<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));
// 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<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, {} 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(),
}
}