Files
enok/src/klima/epw.rs
T
blandClaude Opus 5 afb8ea3fc4
Energirapporter / rapporter (release) Successful in 1m2s
Release enok / bygg (release) Successful in 1m9s
test / test (push) Successful in 1m16s
Les EPW-filer som Latin-1, og bruk oppgitt kommune i klimaoppslaget
EPW-filer er ikke UTF-8. Stedsnavnet «Indre Østfold» kommer som Latin-1-byten 0xD8,
og tolket som UTF-8 ble navnet ødelagt. Da fant ikke oppslaget klimakorrigerings-
faktoren, og energiytelsen ble stående i lokalt klima uten korrigering til Oslo.
Utslaget var 79 kWh/(m²·år) for Nordhusstykket 5, uten annet varsel enn en merknad
inne i rapporten.

Filen leses nå som Latin-1 når den ikke er gyldig UTF-8. Er kommunen oppgitt, gjelder
den for oppslaget, slik at resultatet ikke avhenger av skrivemåten inne i klimafilen.
Merknader fra beregningen skrives også ut på skjermen, ikke bare i rapporten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2026-09-14 21:29:36 +02:00

169 lines
5.6 KiB
Rust

//! Leser EnergyPlus Weather (EPW) og gir 8760 timesverdier.
//!
//! Feltnumre følger EPW-spesifikasjonen (0-basert etter de åtte headerlinjene):
//! 6 tørrkuletemperatur °C, 7 duggpunkt °C, 8 relativ fuktighet %, 9 lufttrykk Pa,
//! 12 horisontal infrarød stråling W/m², 13 global horisontal W/m², 14 direkte normal W/m²,
//! 15 diffus horisontal W/m², 20 vindretning °, 21 vindhastighet m/s.
use crate::{Result, TIMER};
use anyhow::{bail, Context};
use std::path::Path;
/// Gjør bytene i en EPW-fil om til tekst.
///
/// EPW-filer er ikke UTF-8. Stedsnavn skrives i Latin-1, så «Indre Østfold» kommer som
/// byten 0xD8 der Ø skal stå. Tolket som UTF-8 blir navnet ødelagt, og da slår ikke
/// klimakorrigeringsfaktoren til, siden den slås opp på stedsnavnet. Filen leses derfor
/// som UTF-8 når den er gyldig UTF-8, og ellers som Latin-1.
pub fn avkod(bytes: &[u8]) -> String {
match std::str::from_utf8(bytes) {
Ok(s) => s.to_string(),
Err(_) => bytes.iter().map(|b| *b as char).collect(),
}
}
#[derive(Clone, Debug)]
pub struct Vaerdata {
pub sted: String,
pub land: String,
pub bredde: f64,
pub lengde: f64,
pub tidssone: f64,
pub hoyde: f64,
/// Utetemperatur, °C.
pub t_ute: Vec<f64>,
/// Relativ fuktighet, %.
pub rf: Vec<f64>,
/// Global horisontal solstråling, W/m².
pub ghi: Vec<f64>,
/// Direkte normal solstråling, W/m².
pub dni: Vec<f64>,
/// Diffus horisontal solstråling, W/m².
pub dhi: Vec<f64>,
/// Horisontal infrarød stråling fra himmelen, W/m².
pub ir_himmel: Vec<f64>,
/// Vindhastighet, m/s.
pub vind: Vec<f64>,
}
impl Vaerdata {
pub fn fra_fil(sti: &Path) -> Result<Self> {
let innhold = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
let tekst = String::from_utf8_lossy(&innhold);
Self::fra_tekst(&tekst).with_context(|| format!("tolker {}", sti.display()))
}
pub fn fra_tekst(tekst: &str) -> Result<Self> {
let mut linjer = tekst.lines();
let location = linjer.next().context("EPW mangler LOCATION-linje")?;
let f: Vec<&str> = location.split(',').collect();
if f.len() < 10 || !f[0].eq_ignore_ascii_case("LOCATION") {
bail!("første linje er ikke en gyldig LOCATION-linje");
}
let tall =
|i: usize| -> f64 { f.get(i).and_then(|s| s.trim().parse().ok()).unwrap_or(0.0) };
let mut v = Vaerdata {
sted: f[1].trim().to_string(),
land: f.get(3).unwrap_or(&"").trim().to_string(),
bredde: tall(6),
lengde: tall(7),
tidssone: tall(8),
hoyde: tall(9),
t_ute: Vec::with_capacity(TIMER),
rf: Vec::with_capacity(TIMER),
ghi: Vec::with_capacity(TIMER),
dni: Vec::with_capacity(TIMER),
dhi: Vec::with_capacity(TIMER),
ir_himmel: Vec::with_capacity(TIMER),
vind: Vec::with_capacity(TIMER),
};
// Sju gjenstående headerlinjer.
for _ in 0..7 {
linjer.next();
}
for linje in linjer {
if linje.trim().is_empty() {
continue;
}
let d: Vec<&str> = linje.split(',').collect();
if d.len() < 22 {
continue;
}
let g = |i: usize| -> f64 { d[i].trim().parse::<f64>().unwrap_or(0.0) };
// EPW bruker 9999/999999 som «mangler».
let mangler = |x: f64, grense: f64| if x >= grense { 0.0 } else { x };
v.t_ute.push(g(6));
v.rf.push(g(8));
v.ir_himmel.push(mangler(g(12), 9999.0));
v.ghi.push(mangler(g(13), 9999.0).max(0.0));
v.dni.push(mangler(g(14), 9999.0).max(0.0));
v.dhi.push(mangler(g(15), 9999.0).max(0.0));
v.vind.push(mangler(g(21), 999.0));
if v.t_ute.len() == TIMER {
break;
}
}
if v.t_ute.len() != TIMER {
bail!(
"EPW inneholder {} timer, forventet {}",
v.t_ute.len(),
TIMER
);
}
Ok(v)
}
pub fn aarsmiddeltemperatur(&self) -> f64 {
self.t_ute.iter().sum::<f64>() / self.t_ute.len() as f64
}
pub fn maanedsmiddeltemperatur(&self) -> [f64; 12] {
let mut sum = [0.0; 12];
let mut antall = [0.0; 12];
for (t, v) in self.t_ute.iter().enumerate() {
let m = crate::maaned_for_time(t);
sum[m] += v;
antall[m] += 1.0;
}
let mut ut = [0.0; 12];
for m in 0..12 {
ut[m] = if antall[m] > 0.0 {
sum[m] / antall[m]
} else {
0.0
};
}
ut
}
/// Graddager under 17 °C, til informasjon i rapporten.
pub fn graddager(&self, basis: f64) -> f64 {
self.t_ute
.chunks(24)
.map(|d| {
let m = d.iter().sum::<f64>() / d.len() as f64;
(basis - m).max(0.0)
})
.sum()
}
}
#[cfg(test)]
mod tester {
use super::*;
#[test]
fn latin1_stedsnavn_leses_riktig() {
// «Indre Østfold» der Ø er Latin-1-byten 0xD8.
let mut bytes = b"LOCATION,Indre ".to_vec();
bytes.push(0xD8);
bytes.extend_from_slice(b"stfold,None,NOR,CERRA,x,59.58,11.16,1, 130");
assert_eq!(avkod(&bytes).split(',').nth(1), Some("Indre Østfold"));
}
#[test]
fn gyldig_utf8_gaar_uendret_gjennom() {
assert_eq!(avkod("Indre Østfold".as_bytes()), "Indre Østfold");
}
}