Geometri fra IFC, grunnmodell etter ISO 13370 og uoppvarmede soner
test / test (push) Failing after 3s
test / test (push) Failing after 3s
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
This commit is contained in:
+263
-1
@@ -4,6 +4,7 @@
|
||||
|
||||
use crate::data::{Datakilde, Materialer, Normerte};
|
||||
use crate::modell::*;
|
||||
use crate::motor::grunn;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use serde::Serialize;
|
||||
@@ -31,7 +32,11 @@ 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,
|
||||
@@ -41,6 +46,8 @@ pub struct LostFlate {
|
||||
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).
|
||||
@@ -49,6 +56,39 @@ pub struct LostFlate {
|
||||
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,
|
||||
@@ -60,6 +100,7 @@ pub struct Lost {
|
||||
/// 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,
|
||||
@@ -128,9 +169,43 @@ impl Lost {
|
||||
|
||||
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 {
|
||||
flater.push(los_flate(fl, &materialer, periode, &mut f)?);
|
||||
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, &mut f));
|
||||
@@ -139,6 +214,65 @@ impl Lost {
|
||||
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()
|
||||
@@ -243,6 +377,7 @@ impl Lost {
|
||||
antall_boenheter: info.antall_boenheter.max(1),
|
||||
skala_areal,
|
||||
flater,
|
||||
soner,
|
||||
kuldebro_w_per_k,
|
||||
n50,
|
||||
skjerming: b.infiltrasjon.skjerming,
|
||||
@@ -279,6 +414,129 @@ impl Lost {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -358,6 +616,7 @@ fn los_flate(
|
||||
typ: fl.typ,
|
||||
areal: fl.areal,
|
||||
u_verdi: u,
|
||||
u_konstruksjon: u,
|
||||
helning,
|
||||
orientering: fl.orientering,
|
||||
kappa,
|
||||
@@ -366,6 +625,7 @@ fn los_flate(
|
||||
b_faktor,
|
||||
absorptans: fl.absorptans.unwrap_or(0.6),
|
||||
glass: false,
|
||||
sone: None,
|
||||
g_effektiv: 0.0,
|
||||
skjermingsfaktor: 1.0,
|
||||
automatisk_skjerming: false,
|
||||
@@ -416,6 +676,7 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
||||
typ: FlateType::Vegg,
|
||||
areal,
|
||||
u_verdi: v.u_verdi,
|
||||
u_konstruksjon: v.u_verdi,
|
||||
helning: v.helning,
|
||||
orientering: v.orientering,
|
||||
kappa: 0.0,
|
||||
@@ -424,6 +685,7 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
|
||||
b_faktor: 1.0,
|
||||
absorptans: 0.0,
|
||||
glass: true,
|
||||
sone: None,
|
||||
g_effektiv,
|
||||
skjermingsfaktor,
|
||||
automatisk_skjerming: automatisk,
|
||||
|
||||
Reference in New Issue
Block a user