Legg til barnehage, kontor-, skole-, idretts- og kulturbygg

Driftstidene har nå dager per uke og uker per år, og lastene gjelder bare på driftsdager.
Normerte verdier for de nye kategoriene er hentet fra publiserte Simien-utskrifter og
DiBK-utredninger og kontrollert mot energibudsjettene i de samme kildene. Boligblokk har
fått egne verdier i stedet for en kopi av småhus.

Balanserte anlegg kan ha kjølebatteri, og varmegjenvinneren reguleres ned når den ellers
ville varmet tilluften over settpunktet.

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-15 20:05:20 +02:00
co-authored by Claude Opus 5
parent cb64c91b19
commit d02a3d9666
16 changed files with 601 additions and 39 deletions
+45 -2
View File
@@ -14,6 +14,11 @@ const MATERIALER: &str = include_str!("../data/materialer.toml");
const KLIMAKORREKSJON: &str = include_str!("../data/klimakorreksjon.csv");
const NORMERTE_SMAAHUS: &str = include_str!("../data/normerte/smaahus.toml");
const NORMERTE_BOLIGBLOKK: &str = include_str!("../data/normerte/boligblokk.toml");
const NORMERTE_BARNEHAGE: &str = include_str!("../data/normerte/barnehage.toml");
const NORMERTE_KONTORBYGG: &str = include_str!("../data/normerte/kontorbygg.toml");
const NORMERTE_SKOLEBYGG: &str = include_str!("../data/normerte/skolebygg.toml");
const NORMERTE_IDRETTSBYGG: &str = include_str!("../data/normerte/idrettsbygg.toml");
const NORMERTE_KULTURBYGG: &str = include_str!("../data/normerte/kulturbygg.toml");
/// Katalog som kan overstyre de innebygde datafilene.
#[derive(Clone, Debug, Default)]
@@ -63,8 +68,13 @@ impl Datakilde {
let (fil, innebygd) = match kategori {
Kategori::Smaahus => ("normerte/smaahus.toml", NORMERTE_SMAAHUS),
Kategori::Boligblokk => ("normerte/boligblokk.toml", NORMERTE_BOLIGBLOKK),
Kategori::Barnehage => ("normerte/barnehage.toml", NORMERTE_BARNEHAGE),
Kategori::Kontorbygg => ("normerte/kontorbygg.toml", NORMERTE_KONTORBYGG),
Kategori::Skolebygg => ("normerte/skolebygg.toml", NORMERTE_SKOLEBYGG),
Kategori::Idrettsbygg => ("normerte/idrettsbygg.toml", NORMERTE_IDRETTSBYGG),
Kategori::Kulturbygg => ("normerte/kulturbygg.toml", NORMERTE_KULTURBYGG),
annen => {
// Andre kategorier krever egen datafil i data-katalogen.
// Kategorier uten offentlig tilgjengelige normerte verdier krever egen datafil.
let fil = format!("normerte/{}.toml", annen.nokkel());
let p = self
.katalog
@@ -257,9 +267,12 @@ pub struct Settpunkt {
pub oppvarming_natt: f64,
pub kjoling: f64,
pub ventilasjon_tilluft: f64,
/// Tilluftstemperatur når anlegget har kjølebatteri. Utelatt: lik `ventilasjon_tilluft`.
#[serde(default)]
pub ventilasjon_tilluft_kjoling: Option<f64>,
}
#[derive(Deserialize, Clone, Debug)]
#[derive(Deserialize, serde::Serialize, Clone, Debug)]
pub struct Driftstider {
pub oppvarming: Tidsrom,
pub belysning: Tidsrom,
@@ -267,13 +280,43 @@ pub struct Driftstider {
pub varmtvann: Tidsrom,
pub personer: Tidsrom,
pub ventilasjon: Tidsrom,
/// Driftsdager per uke, regnet fra mandag (NS 3031 Tabell A.3).
#[serde(default = "sju")]
pub dager_per_uke: u8,
/// Driftsuker per år (NS 3031 Tabell A.3). Ukene uten drift legges i ferier, se [`FERIEUKER`].
#[serde(default = "femtito")]
pub uker_per_aar: u8,
}
fn sju() -> u8 {
7
}
fn femtito() -> u8 {
52
}
/// Uker uten drift, i den rekkefølgen de tas ut når en kategori har færre enn 52 driftsuker:
/// fellesferien først, deretter jul, vinterferie, påske og høstferie, og så uke for uke
/// utover sommeren og nyttår.
pub const FERIEUKER: [u8; 16] = [28, 29, 30, 31, 27, 32, 52, 8, 14, 40, 26, 33, 1, 25, 34, 9];
impl Driftstider {
/// Om døgn nummer `dag` (0 = 1. januar) er en driftsdag.
///
/// Året regnes å begynne på en mandag. Døgn 364 hører til uke 52.
pub fn driftsdag(&self, dag: usize) -> bool {
if dag % 7 >= self.dager_per_uke.min(7) as usize {
return false;
}
let uke = (dag / 7 + 1).min(52) as u8;
let fri = (52u8.saturating_sub(self.uker_per_aar) as usize).min(FERIEUKER.len());
!FERIEUKER[..fri].contains(&uke)
}
pub fn driftsdager_per_aar(&self) -> usize {
(0..365).filter(|&d| self.driftsdag(d)).count()
}
}
#[derive(Deserialize, serde::Serialize, Clone, Copy, Debug)]
pub struct Tidsrom {
+2
View File
@@ -84,6 +84,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
"skolebygg" | "skole" => Kategori::Skolebygg,
"barnehage" => Kategori::Barnehage,
"idrettsbygg" | "idrett" => Kategori::Idrettsbygg,
"kulturbygg" | "kultur" => Kategori::Kulturbygg,
annen => {
merknader
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
+41 -8
View File
@@ -130,6 +130,8 @@ pub struct LostVentilasjon {
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,
}
@@ -142,7 +144,15 @@ pub struct LostLaster {
pub personer: [f64; 24],
pub varmtvann: [f64; 24],
pub pumper: [f64; 24],
pub oppvarming_driftstid: crate::data::Tidsrom,
/// 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;
@@ -326,7 +336,10 @@ impl Lost {
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() {
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.
@@ -724,6 +737,22 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
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,
@@ -732,6 +761,7 @@ fn los_ventilasjon(v: &Ventilasjon, n: &Normerte, logg: &mut Vec<Forutsetning>)
sfp,
varmebatteri,
tilluft_settpunkt: n.settpunkt.ventilasjon_tilluft,
tilluft_kjoling,
driftstid: n.driftstid.ventilasjon,
}
}
@@ -754,9 +784,11 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
logg.push(Forutsetning {
felt: "belysning".into(),
verdi: format!(
"{:.2} W/m² i {:.0} h/døgn",
"{:.2} W/m² i {:.0} h/døgn, {} dager/uke, {} uker/år",
n.belysning.effekt,
n.driftstid.belysning.timer()
n.driftstid.belysning.timer(),
n.driftstid.dager_per_uke,
n.driftstid.uker_per_aar
),
kilde: format!("normert, {}", n.kilde),
});
@@ -772,16 +804,17 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
.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.
kwh * 1000.0 / (365.0 * n.driftstid.varmtvann.timer())
// 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() * 365.0 / 1000.0
n.varmtvann.effekt * n.driftstid.varmtvann.timer() * driftsdager / 1000.0
),
kilde: format!("normert, {}", n.kilde),
});
@@ -801,6 +834,6 @@ fn los_laster(b: &Bygning, n: &Normerte, logg: &mut Vec<Forutsetning>) -> LostLa
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),
oppvarming_driftstid: n.driftstid.oppvarming,
driftstider: n.driftstid.clone(),
}
}
+6
View File
@@ -397,6 +397,12 @@ pub struct Ventilasjon {
/// Har anlegget varmebatteri som ettervarmer tilluften?
#[serde(default, skip_serializing_if = "Option::is_none")]
pub varmebatteri: Option<bool>,
/// Har anlegget kjølebatteri? Tilluften holdes da på `tilluft_kjoling` i driftstiden.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kjolebatteri: Option<bool>,
/// Tilluftstemperatur med kjølebatteri, °C. Utelatt: normert.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tilluft_kjoling: Option<f64>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
+22 -11
View File
@@ -188,17 +188,20 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
}
for t in timer {
let klokke = t % 24;
let driftsdag = l.laster.driftsdag(t);
let t_ute = v.t_ute[t];
let t_himmel = sol::himmeltemperatur(v, t);
// ── laster ──────────────────────────────────────────────
let bel = l.laster.belysning[klokke] * l.bra;
let utstyr_el = l.laster.utstyr_effekt[klokke] * l.bra;
let utstyr_varme = l.laster.utstyr_varme[klokke] * l.bra;
let personer = l.laster.personer[klokke] * l.bra;
let vv = l.laster.varmtvann[klokke] * l.bra;
let pumper = l.laster.pumper[klokke] * l.bra;
let vifter = ventilasjon::vifteeffekt(l, klokke);
// Lastprofilene gjelder driftsdager; på andre dager er bygningen ute av bruk.
let skala = if driftsdag { l.bra } else { 0.0 };
let bel = l.laster.belysning[klokke] * skala;
let utstyr_el = l.laster.utstyr_effekt[klokke] * skala;
let utstyr_varme = l.laster.utstyr_varme[klokke] * skala;
let personer = l.laster.personer[klokke] * skala;
let vv = l.laster.varmtvann[klokke] * skala;
let pumper = l.laster.pumper[klokke] * skala;
let vifter = ventilasjon::vifteeffekt(l, t);
// Vifteeffekten ender som varme i lufta (ISO 52016-1: hele SFP-effekten).
let internlast = bel + utstyr_varme + personer + tilskudd.tappevann + vifter;
@@ -211,21 +214,28 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
}
// ── ventilasjon ─────────────────────────────────────────
let q_vent = ventilasjon::luftmengde(l, klokke);
let q_vent = ventilasjon::luftmengde(l, t);
let h_ve = q_vent * RHO_C_LUFT;
let t_sup_hx = ventilasjon::tilluftstemperatur(l, t_ute, t_a_forrige);
let (t_sup, vent_varme) =
let kjol_tilluft = l
.ventilasjon
.tilluft_kjoling
.filter(|&k| ventilasjon::i_drift(l, t) && t_sup_hx > k);
let (t_sup, vent_varme, vent_kjol) =
if l.ventilasjon.varmebatteri && t_sup_hx < l.ventilasjon.tilluft_settpunkt {
(
l.ventilasjon.tilluft_settpunkt,
h_ve * (l.ventilasjon.tilluft_settpunkt - t_sup_hx),
0.0,
)
} else if let Some(k) = kjol_tilluft {
(k, 0.0, h_ve * (t_sup_hx - k))
} else {
(t_sup_hx, 0.0)
(t_sup_hx, 0.0, 0.0)
};
// ── settpunkt ───────────────────────────────────────────
let sett_varme = if l.laster.oppvarming_driftstid.aktiv(klokke) {
let sett_varme = if driftsdag && l.laster.driftstider.oppvarming.aktiv(klokke) {
l.settpunkt_oppvarming
} else {
l.settpunkt_natt
@@ -339,6 +349,7 @@ pub fn simuler(l: &Lost, v: &Vaerdata, tilskudd: Systemtilskudd) -> Result<Times
ts.romoppvarming[t] = fi.max(0.0);
ts.romkjoling[t] = (-fi).max(0.0);
ts.ventilasjonsvarme[t] = vent_varme;
ts.ventilasjonskjoling[t] = vent_kjol;
ts.varmtvann[t] = vv;
ts.vifter[t] = vifter;
ts.pumper[t] = pumper;
+23 -7
View File
@@ -33,9 +33,14 @@ pub fn infiltrasjonsluftmengde(l: &Lost) -> f64 {
n_inf * v
}
/// Ventilasjonsluftmengde for en klokketime, m³/h.
pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
let spesifikk = if l.ventilasjon.driftstid.aktiv(klokketime) {
/// Om ventilasjonen går med driftsluftmengde i time `t` av året.
pub fn i_drift(l: &Lost, t: usize) -> bool {
l.laster.driftsdag(t) && l.ventilasjon.driftstid.aktiv(t % 24)
}
/// Ventilasjonsluftmengde i time `t` av året, m³/h.
pub fn luftmengde(l: &Lost, t: usize) -> f64 {
let spesifikk = if i_drift(l, t) {
l.ventilasjon.luftmengde
} else {
l.ventilasjon.luftmengde_natt
@@ -44,18 +49,29 @@ pub fn luftmengde(l: &Lost, klokketime: usize) -> f64 {
}
/// Tilluftstemperatur etter varmegjenvinner (uten ettervarming), °C.
///
/// Gjenvinneren reguleres ned når den ellers ville varmet tilluften over settpunktet, slik
/// en roterende gjenvinner med turtallsregulering eller et omløpsspjeld gjør. Når det er
/// varmere ute enn inne, kjøler gjenvinneren tilluften og går for fullt.
pub fn tilluftstemperatur(l: &Lost, t_ute: f64, t_avtrekk: f64) -> f64 {
match l.ventilasjon.typ {
VentilasjonType::Balansert => t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute),
VentilasjonType::Balansert => {
let gjenvunnet = t_ute + l.ventilasjon.gjenvinning * (t_avtrekk - t_ute);
if gjenvunnet > t_ute {
gjenvunnet.min(l.ventilasjon.tilluft_settpunkt.max(t_ute))
} else {
gjenvunnet
}
}
_ => t_ute,
}
}
/// Vifteeffekt for en klokketime, W.
pub fn vifteeffekt(l: &Lost, klokketime: usize) -> f64 {
/// Vifteeffekt i time `t` av året, W.
pub fn vifteeffekt(l: &Lost, t: usize) -> f64 {
if matches!(l.ventilasjon.typ, VentilasjonType::Naturlig) {
return 0.0;
}
let q = luftmengde(l, klokketime) / 3600.0; // m³/s
let q = luftmengde(l, t) / 3600.0; // m³/s
l.ventilasjon.sfp * 1000.0 * q
}
+1 -1
View File
@@ -149,7 +149,7 @@ pub fn kilde(r: &Resultat) -> String {
l.settpunkt_kjoling.unwrap_or(0.0)
);
let _ = writeln!(s, " <Driftstider>");
let d = &l.laster.oppvarming_driftstid;
let d = &l.laster.driftstider.oppvarming;
let _ = writeln!(
s,
" <Ventilasjon>{:.0}</Ventilasjon>",
+2 -1
View File
@@ -310,7 +310,8 @@ fn baerer_for(t: VarmekildeType, lukket: bool) -> Energibaerer {
/// Samlet tap fra tappevannsanlegget, W. Tapet varmer opp der anlegget står.
pub fn tappevannstilskudd_totalt(l: &Lost, s: &Systemoppsett) -> f64 {
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra;
let driftsandel = l.laster.driftstider.driftsdager_per_aar() as f64 / 365.0;
let netto_w: f64 = l.laster.varmtvann.iter().sum::<f64>() / 24.0 * l.bra * driftsandel;
netto_w * s.tappevann_tap_andel
}