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:
@@ -95,13 +95,14 @@ Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen
|
||||
| Kommando | Hva den gjør |
|
||||
| --- | --- |
|
||||
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
|
||||
| `enok beregn modell.ifc` | Beregner rett fra en modell, uten at `bygning.toml` finnes fra før |
|
||||
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
||||
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
|
||||
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
||||
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
||||
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
||||
|
||||
Nyttige valg til `beregn`: `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
|
||||
Nyttige valg til `beregn`: `--tillegg FIL`, `--ingen-pdf`, `--ingen-timeserie`, `--enova-xml`,
|
||||
`--uten-scenarier`, `--ut KATALOG`.
|
||||
|
||||
### Utdata
|
||||
@@ -194,15 +195,21 @@ Ferdige eksempler ligger i `eksempler/`.
|
||||
|
||||
### Modellen og en tilleggsfil
|
||||
|
||||
Modellen bør være kilden til alt den kan beskrive. Det den ikke beskriver, som varmeanlegg,
|
||||
lekkasjetall og tiltak, og rettelser der den mangler noe, legges i en tilleggsfil som flettes
|
||||
inn ved hver import etter de samme reglene som scenarioene:
|
||||
Modellen er kilden til alt den kan beskrive, og en modell alene gir en ferdig rapport: enok
|
||||
fyller hullene med normerte verdier og typiske verdier for byggeåret, og rapporten sier hva
|
||||
som er antatt. Det modellen ikke kan beskrive, som varmeanlegg, lekkasjetall og tiltak, og
|
||||
rettelser der den mangler noe, legges i en tilleggsfil som flettes inn etter de samme reglene
|
||||
som scenarioene:
|
||||
|
||||
```sh
|
||||
enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
||||
enok beregn bygning.toml
|
||||
enok beregn modell.ifc # bare modellen
|
||||
enok beregn modell.ifc --tillegg prosjekt.toml # modellen og tilleggsfilen
|
||||
enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml # samme, som fil
|
||||
```
|
||||
|
||||
`beregn` skriver bygningsbeskrivelsen modellen ga, til `bygning.toml` i utdatakatalogen, slik
|
||||
at beregningen kan etterprøves.
|
||||
|
||||
```toml
|
||||
# prosjekt.toml
|
||||
[import]
|
||||
@@ -284,6 +291,10 @@ egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
|
||||
| `Varmekilde` | `Oppvarming`, `HeatingSource` | `Bergvarmepumpe` |
|
||||
| `OppvarmetBRA` | `HeatedFloorArea` | `120` |
|
||||
|
||||
Mangler en bygningsdel U-verdi i modellen, sier importen hvilke det gjelder, og beregningen
|
||||
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
|
||||
eksportene ikke har U-verdi.
|
||||
|
||||
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
|
||||
modellen alene.
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+31
-11
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user