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
This commit is contained in:
bl
2026-09-16 14:39:58 +02:00
co-authored by Claude Opus 5
parent 6d87ef496f
commit 32ecb0b132
7 changed files with 180 additions and 91 deletions
+5 -5
View File
@@ -481,13 +481,13 @@ fn bygg(
let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon);
aapningsareal += areal;
let vt = vindustyper.get(&a.vindustype);
let u = vt.and_then(|v| v.u_verdi).unwrap_or_else(|| {
let u = vt.and_then(|v| v.u_verdi);
if u.is_none() {
merknader.push(format!(
"Åpningen «{}» mangler U-verdi i gbXML; satt til 2,4 W/(m²K).",
"Åpningen «{}» mangler U-verdi i gbXML; verdien utledes av byggeåret.",
a.navn
));
2.4
});
}
let dor = a.typ.to_lowercase().contains("door");
vinduer.push(Vindu {
navn: vt
@@ -687,7 +687,7 @@ mod tester {
// Vinduet.
assert_eq!(b.vindu.len(), 1);
assert_eq!(b.vindu[0].areal, 3.0);
assert_eq!(b.vindu[0].u_verdi, 1.2);
assert_eq!(b.vindu[0].u_verdi, Some(1.2));
assert_eq!(b.vindu[0].g_verdi, Some(0.5));
}
+32 -12
View File
@@ -245,7 +245,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
let mut kjellerflater: Vec<Flate> = Vec::new();
let mut flater: Vec<Flate> = Vec::new();
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
let mut uten_uverdi = 0usize;
let mut uten_uverdi: Vec<String> = Vec::new();
let mut uten_geometri = 0usize;
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
@@ -287,7 +287,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
(mot, None)
};
if d.u_verdi.is_none() {
uten_uverdi += 1;
uten_uverdi.push(d.navn.clone());
}
let mal = if i_kjeller {
&mut kjellerflater
@@ -358,14 +358,12 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
.and_then(|v| asimut_for_vegg.get(v))
.copied()
.unwrap_or(0.0);
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
uten_uverdi += 1;
if dor {
2.0
} else {
2.4
}
});
// Uten U-verdi i modellen lar vi feltet stå tomt, slik at beregningen bruker en
// typisk verdi for byggeåret i stedet for en fast standardverdi.
let u = u_verdi(&egenskaper, r);
if u.is_none() {
uten_uverdi.push(navn_av(e, r, if dor { "Dør" } else { "Vindu" }));
}
vinduer.push(Vindu {
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
areal,
@@ -445,9 +443,31 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
));
}
if uten_uverdi > 0 {
if !uten_uverdi.is_empty() {
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
// konstruksjon nevnes én gang, med antallet.
let mut antall: Vec<(String, usize)> = Vec::new();
for navn in &uten_uverdi {
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
match antall.iter_mut().find(|(n, _)| *n == kort) {
Some((_, a)) => *a += 1,
None => antall.push((kort, 1)),
}
}
let liste: Vec<String> = antall
.iter()
.map(|(n, a)| {
if *a > 1 {
format!("{n} ×{a}")
} else {
n.clone()
}
})
.collect();
merknader.push(format!(
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
"{} bygningsdeler mangler U-verdi i IFC-filen: {}. Legg inn U-verdier i modellen, eller la enok bruke typiske verdier for byggeåret.",
uten_uverdi.len(),
liste.join(", ")
));
}
merknader.push(
+1 -1
View File
@@ -153,7 +153,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
antall: tall(felt(8)).unwrap_or(1.0),
orientering: tall(felt(4)).unwrap_or(0.0),
helning: tall(felt(5)).unwrap_or(90.0),
u_verdi: u,
u_verdi: Some(u),
g_verdi: tall(felt(7)),
karmandel: None,
solskjerming: None,
+99 -60
View File
@@ -56,6 +56,9 @@ struct Beregn {
/// Beregner bare hovedalternativet, uten scenarioene i filen.
#[arg(long)]
uten_scenarier: bool,
/// Tilleggsfil som flettes inn: det modellen ikke beskriver, og rettelser.
#[arg(short, long, value_name = "FIL")]
tillegg: Option<PathBuf>,
}
#[derive(Args)]
@@ -102,14 +105,77 @@ fn main() -> Result<()> {
}
}
/// Leser en bygningsbeskrivelse eller en BIM-modell, med en valgfri tilleggsfil flettet inn.
///
/// En `.toml`-fil er bygningsbeskrivelsen selv; andre filtyper importeres. Returnerer også
/// merknadene fra importen, og om filen var en modell.
fn les_modell(
fil: &Path,
tillegg: Option<&Path>,
) -> Result<(enok::modell::Bygning, Vec<String>, bool)> {
let endelse = fil
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let tillegg = tillegg.map(inndata::les_tillegg).transpose()?;
let valg = tillegg
.as_ref()
.map(|t| t.import.clone())
.unwrap_or_default();
let (bygning, mut merknader, modell) = match endelse.as_str() {
"toml" => (inndata::les_bygning(fil)?, Vec::new(), false),
"gbxml" | "xml" => {
let r = inndata::gbxml::les(fil)?;
(r.bygning, r.merknader, true)
}
"ifc" => {
let r = inndata::ifc::les_med(fil, &valg)?;
(r.bygning, r.merknader, true)
}
"csv" => {
let r = inndata::regneark::les(fil)?;
(r.bygning, r.merknader, true)
}
annen => bail!(
"ukjent filtype «{annen}». enok leser bygning.toml og importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
),
};
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
}
let Some(t) = tillegg else {
return Ok((bygning, merknader, modell));
};
let (bygning, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
for m in bom {
merknader.push(format!(
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
));
}
Ok((bygning, merknader, modell))
}
fn beregn(a: Beregn, kilde: &Datakilde) -> Result<()> {
let bygning = inndata::les_bygning(&a.fil)?;
let (bygning, merknader, modell) = les_modell(&a.fil, a.tillegg.as_deref())?;
for m in &merknader {
eprintln!(" · {m}");
}
for m in enok::modell::kontroll::kontroller(&bygning) {
eprintln!(" ! {}: {}", m.felt, m.melding);
}
let ut =
a.ut.unwrap_or_else(|| PathBuf::from("ut").join(filnavnvennlig(&bygning.bygning.navn)));
std::fs::create_dir_all(&ut)?;
if modell {
// Bygningsbeskrivelsen modellen ga, skrives med, slik at beregningen kan etterprøves.
let sti = ut.join("bygning.toml");
std::fs::write(
&sti,
skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?,
)?;
eprintln!("Bygningsbeskrivelsen fra modellen: {}", sti.display());
}
eprintln!("Henter klimadata …");
let (vaerdata, klimanavn, er_referanse) = kjor::hent_klima(&bygning)?;
@@ -215,66 +281,9 @@ fn skriv_sammendrag(r: &kjor::Resultat, scenarier: &[(String, kjor::Resultat)])
}
fn import(a: Import) -> Result<()> {
let endelse = a
.fil
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let tillegg = a.tillegg.as_deref().map(inndata::les_tillegg).transpose()?;
let valg = tillegg
.as_ref()
.map(|t| t.import.clone())
.unwrap_or_default();
let (bygning, mut merknader) = match endelse.as_str() {
"gbxml" | "xml" => {
let r = inndata::gbxml::les(&a.fil)?;
(r.bygning, r.merknader)
}
"ifc" => {
let r = inndata::ifc::les_med(&a.fil, &valg)?;
(r.bygning, r.merknader)
}
"csv" => {
let r = inndata::regneark::les(&a.fil)?;
(r.bygning, r.merknader)
}
annen => bail!(
"ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
),
};
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
}
let (bygning, mut innhold) = match (&tillegg, &a.tillegg) {
(Some(t), Some(sti)) => {
let (b, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
for m in bom {
merknader.push(format!(
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
));
}
let hode = format!(
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
# Merknader fra importen:\n",
a.fil.display(),
sti.display()
);
(b, hode)
}
_ => (
bygning,
String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n"),
),
};
let (bygning, merknader, _) = les_modell(&a.fil, a.tillegg.as_deref())?;
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
let tekst = toml::to_string_pretty(&bygning).context("serialiserer bygningen til TOML")?;
for m in &merknader {
innhold.push_str(&format!("# {m}\n"));
}
innhold.push('\n');
innhold.push_str(&tekst);
let innhold = skriv_bygning(&bygning, &merknader, &a.fil, a.tillegg.as_deref())?;
std::fs::write(&ut, innhold)?;
println!("Skrev {}", ut.display());
for m in &merknader {
@@ -283,6 +292,36 @@ fn import(a: Import) -> Result<()> {
Ok(())
}
/// Bygningsbeskrivelsen som TOML, med et hode som sier hvor den kommer fra.
fn skriv_bygning(
bygning: &enok::modell::Bygning,
merknader: &[String],
fil: &Path,
tillegg: Option<&Path>,
) -> Result<String> {
let tekst = toml::to_string_pretty(bygning).context("serialiserer bygningen til TOML")?;
let mut innhold = match tillegg {
Some(t) => format!(
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
# Merknader fra importen:\n",
fil.display(),
t.display()
),
None => format!(
"# Generert av enok fra «{}».\n\
# Merknader fra importen:\n",
fil.display()
),
};
for m in merknader {
innhold.push_str(&format!("# {m}\n"));
}
innhold.push('\n');
innhold.push_str(&tekst);
Ok(innhold)
}
fn klima(a: Klima) -> Result<()> {
if a.liste {
for k in enok::klima::tmy::kommuner()? {
+23 -6
View File
@@ -218,7 +218,7 @@ impl Lost {
)?;
}
for v in &b.vindu {
flater.push(los_vindu(v, &mut f));
flater.push(los_vindu(v, periode, &mut f));
}
if flater.is_empty() {
bail!("bygningen har ingen flater; legg inn minst én [[flate]]");
@@ -646,8 +646,25 @@ fn los_flate(
})
}
fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
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),
@@ -660,11 +677,11 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
0.0
} else {
v.g_verdi.unwrap_or_else(|| {
let antatt = if v.u_verdi <= 1.2 { 0.50 } else { 0.75 };
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 v.u_verdi <= 1.2 {
kilde: if u <= 1.2 {
"antatt 3-lags energiglass"
} else {
"antatt 2-lags glass"
@@ -688,8 +705,8 @@ fn los_vindu(v: &Vindu, logg: &mut Vec<Forutsetning>) -> LostFlate {
navn: v.navn.clone(),
typ: FlateType::Vegg,
areal,
u_verdi: v.u_verdi,
u_konstruksjon: v.u_verdi,
u_verdi: u,
u_konstruksjon: u,
helning: v.helning,
orientering: v.orientering,
kappa: 0.0,
+3 -1
View File
@@ -268,7 +268,9 @@ pub struct Vindu {
pub orientering: f64,
#[serde(default = "nitti")]
pub helning: f64,
pub u_verdi: f64,
/// U-verdi inkl. karm, W/(m²K). Utelatt: typisk for byggeåret.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub u_verdi: Option<f64>,
/// Solfaktor for glasset (g-verdi). Utelatt: 0,75 for 2-lags, 0,5 for 3-lags energiglass.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub g_verdi: Option<f64>,