use rust_decimal::{Decimal, dec};
use time::OffsetDateTime;
use super::{Abgrenzungsfall, Einspeisung, Gate};
use crate::eeg::{EegError, Row, add, align, coincident, mul, share, sub, sum};
use crate::series::Series;
use crate::series::interval::QualityFlag;
use crate::time::calendar::DayBoundary;
use crate::time::resolution::Resolution;
pub const WIRKUNGSGRAD_LADEPUNKT: Decimal = dec!(0.85);
#[derive(Debug, Clone, Copy)]
pub enum Basisfall<'a> {
Stromspeicher,
Ladepunkt,
StromspeicherUndLadepunkt,
MitSpeicherzaehler {
z3v: &'a Series,
z3e: &'a Series,
},
}
impl Basisfall<'_> {
#[must_use]
pub const fn fall(&self) -> Abgrenzungsfall {
match self {
Self::Stromspeicher => Abgrenzungsfall::A1,
Self::Ladepunkt => Abgrenzungsfall::A2,
Self::StromspeicherUndLadepunkt => Abgrenzungsfall::A3,
Self::MitSpeicherzaehler { .. } => Abgrenzungsfall::A4,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GewichteteAnlage<'a> {
pub leistung: Decimal,
pub aw: Gate<'a>,
}
#[derive(Debug, Clone, Copy)]
pub struct VorrangAnlage<'a> {
pub z4e: &'a Series,
pub aw: Gate<'a>,
}
#[derive(Debug, Clone)]
pub enum Anlagen<'a> {
Eine(Gate<'a>),
Gleichartig(Vec<GewichteteAnlage<'a>>),
Vorrang {
vorrang: Vec<VorrangAnlage<'a>>,
rest: Gate<'a>,
},
}
#[derive(Debug, Clone)]
pub struct Abgrenzungsoption<'a> {
pub z1nb: &'a Series,
pub einspeisung: Einspeisung<'a>,
pub z2v: &'a Series,
pub z2e: &'a Series,
pub basisfall: Basisfall<'a>,
pub anlagen: Anlagen<'a>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AnlageViertelstunde {
pub aw: bool,
pub f23: Decimal,
pub f34: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Viertelstunde {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub z1nb: Decimal,
pub einspeisung: Decimal,
pub z2v: Decimal,
pub z2e: Decimal,
pub f1: Decimal,
pub f2: Decimal,
pub f23: Decimal,
pub anlagen: Vec<AnlageViertelstunde>,
pub quality: QualityFlag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AnlageMonat {
pub f26: Decimal,
pub f28: Decimal,
pub f29: Decimal,
pub f30: Option<Decimal>,
pub f31: Decimal,
pub f32: Decimal,
pub f35: Option<Decimal>,
pub f36: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Monat {
pub faelle: Vec<Abgrenzungsfall>,
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub f3: Decimal,
pub f4: Decimal,
pub f5: Decimal,
pub f6: Decimal,
pub f7: Option<Decimal>,
pub f8: Option<Decimal>,
pub f9: Decimal,
pub f10: Decimal,
pub f11: Decimal,
pub f12: Decimal,
pub f13: Decimal,
pub f14: Option<Decimal>,
pub f15: Decimal,
pub f16: Decimal,
pub f17: Option<Decimal>,
pub f18: Option<Decimal>,
pub f19: Decimal,
pub f20: Decimal,
pub f21: Decimal,
pub f28: Decimal,
pub anlagen: Vec<AnlageMonat>,
pub viertelstunden: Vec<Viertelstunde>,
}
impl Monat {
pub fn foerderfaehig(&self) -> Result<Decimal, EegError> {
sum(self.anlagen.iter().map(|a| a.f32))
}
}
fn one_month(rows: &[Row]) -> Result<(OffsetDateTime, OffsetDateTime), EegError> {
let (Some(first), Some(last)) = (rows.first(), rows.last()) else {
return Err(EegError::Empty);
};
let month = DayBoundary::Strom
.bucket(first.from, Resolution::Month)
.ok_or(EegError::Calendar)?;
if let Some(out) = rows.iter().find(|r| r.to > month.end()) {
return Err(EegError::OutsidePeriod { at: out.from });
}
Ok((first.from, last.to))
}
fn clamp0(x: Decimal) -> Decimal {
x.max(Decimal::ZERO)
}
struct Split {
num: Decimal,
den: Decimal,
}
impl Abgrenzungsoption<'_> {
pub fn monat(&self) -> Result<Monat, EegError> {
let mut inputs: Vec<(&'static str, &Series)> = vec![
("Z1NB", self.z1nb),
(self.einspeisung.name(), self.einspeisung.series()),
("Z2V", self.z2v),
("Z2E", self.z2e),
];
if let Basisfall::MitSpeicherzaehler { z3v, z3e } = self.basisfall {
inputs.push(("Z3V", z3v));
inputs.push(("Z3E", z3e));
}
let z4_at = inputs.len();
if let Anlagen::Vorrang { vorrang, .. } = &self.anlagen {
if vorrang.is_empty() {
return Err(EegError::Inconsistent {
what: "A6 needs at least one plant with priority",
});
}
inputs.extend(vorrang.iter().map(|p| ("Z4E", p.z4e)));
}
let rows = align(&inputs)?;
let (from, to) = one_month(&rows)?;
let mut faelle = vec![self.basisfall.fall()];
match &self.anlagen {
Anlagen::Eine(_) => {}
Anlagen::Gleichartig(list) => {
if list.is_empty() {
return Err(EegError::Inconsistent {
what: "A5 needs at least one plant",
});
}
if list.iter().any(|p| p.leistung <= Decimal::ZERO) {
return Err(EegError::ZeroWeight {
formula: "(ZF)",
what: "installed power",
});
}
faelle.push(Abgrenzungsfall::A5);
}
Anlagen::Vorrang { .. } => faelle.push(Abgrenzungsfall::A6),
}
if matches!(self.einspeisung, Einspeisung::Zw(_)) {
faelle.push(Abgrenzungsfall::A7);
}
let gates: Vec<Gate<'_>> = match &self.anlagen {
Anlagen::Eine(g) => vec![*g],
Anlagen::Gleichartig(list) => list.iter().map(|p| p.aw).collect(),
Anlagen::Vorrang { vorrang, rest } => {
vorrang.iter().map(|p| p.aw).chain([*rest]).collect()
}
};
let weights: Option<(Vec<Decimal>, Decimal)> = match &self.anlagen {
Anlagen::Gleichartig(list) => {
let w: Vec<Decimal> = list.iter().map(|p| p.leistung).collect();
let total = sum(w.iter().copied())?;
Some((w, total))
}
_ => None,
};
let mut quarters = Vec::with_capacity(rows.len());
for r in &rows {
let (z1nb, ne, z2v, z2e) = (r.values[0], r.values[1], r.values[2], r.values[3]);
let f1 = coincident(z1nb, z2v);
let f2 = coincident(ne, z2e);
let f23 = sub(ne, f2)?;
let anlagen = match (&self.anlagen, &weights) {
(Anlagen::Gleichartig(_), Some((w, total))) => gates
.iter()
.zip(w)
.map(|(g, wk)| {
Ok(AnlageViertelstunde {
aw: g.passes(r.from),
f23: share(*wk, f23, *total)?.ok_or(EegError::Overflow)?,
f34: None,
})
})
.collect::<Result<Vec<_>, EegError>>()?,
(Anlagen::Vorrang { .. }, _) => {
let mut left_v = sub(z2v, f1)?;
let mut left_23 = f23;
let mut out = Vec::with_capacity(gates.len());
for (k, g) in gates.iter().enumerate() {
let (f34, f23k) = match r.values.get(z4_at + k) {
Some(&z4e) => {
let f34 = left_v.min(z4e);
let f23k = left_23.min(sub(z4e, f34)?);
(f34, f23k)
}
None => (left_v, left_23),
};
left_v = sub(left_v, f34)?;
left_23 = sub(left_23, f23k)?;
out.push(AnlageViertelstunde {
aw: g.passes(r.from),
f23: f23k,
f34: Some(f34),
});
}
out
}
_ => vec![AnlageViertelstunde {
aw: gates[0].passes(r.from),
f23,
f34: None,
}],
};
quarters.push(Viertelstunde {
from: r.from,
to: r.to,
z1nb,
einspeisung: ne,
z2v,
z2e,
f1,
f2,
f23,
anlagen,
quality: r.quality,
});
}
let f3 = sum(quarters.iter().map(|q| q.z1nb))?;
let f4 = sum(quarters.iter().map(|q| q.einspeisung))?;
let f5 = sum(quarters.iter().map(|q| q.z2v))?;
let f6 = sum(quarters.iter().map(|q| q.z2e))?;
let (f7, f8) = if matches!(self.basisfall, Basisfall::MitSpeicherzaehler { .. }) {
(
Some(sum(rows.iter().map(|r| r.values[4]))?),
Some(sum(rows.iter().map(|r| r.values[5]))?),
)
} else {
(None, None)
};
let f9 = sum(quarters.iter().map(|q| q.f1))?;
let f10 = sub(f5, f9)?;
let f11 = sum(quarters.iter().map(|q| q.f2))?;
let f12 = clamp0(sub(f6, f5)?);
let f13 = clamp0(sub(f11, f12)?);
let (f14, f15) = match self.basisfall {
Basisfall::Stromspeicher => (
share(f6, Decimal::ONE, f5)?,
share(f6, f10, f5)?.unwrap_or(Decimal::ZERO),
),
_ => (
Some(WIRKUNGSGRAD_LADEPUNKT),
mul(WIRKUNGSGRAD_LADEPUNKT, f10)?,
),
};
let f16 = clamp0(sub(f13, f15)?);
let f17 = match (self.basisfall, f7, f8) {
(Basisfall::Stromspeicher, _, _) => Some(clamp0(sub(f5, f6)?)),
(Basisfall::MitSpeicherzaehler { .. }, Some(v), Some(e)) => Some(clamp0(sub(v, e)?)),
_ => None,
};
let f18 = share(f16, Decimal::ONE, f6)?;
let f19 = match f17 {
Some(l) => share(f16, l, f6)?.unwrap_or(Decimal::ZERO),
None => Decimal::ZERO,
};
let f20 = add(f16, f19)?.min(f3);
let f21 = sub(f3, f20)?;
let f28 = f13.min(f15);
let f35: Vec<Option<Decimal>> = match &self.anlagen {
Anlagen::Vorrang { .. } => (0..gates.len())
.map(|k| {
sum(quarters
.iter()
.map(|q| q.anlagen[k].f34.unwrap_or(Decimal::ZERO)))
.map(Some)
})
.collect::<Result<_, _>>()?,
_ => vec![None; gates.len()],
};
let mut anlagen = Vec::with_capacity(gates.len());
for k in 0..gates.len() {
let gated = |value: fn(&Viertelstunde, usize) -> Decimal| {
sum(quarters
.iter()
.filter(|q| q.anlagen[k].aw)
.map(|q| value(q, k)))
};
let split = match (&self.anlagen, &weights) {
(Anlagen::Gleichartig(_), Some((w, total))) => Split {
num: mul(w[k], f28)?,
den: *total,
},
(Anlagen::Vorrang { .. }, _) => Split {
num: mul(f35[k].unwrap_or(Decimal::ZERO), f28)?,
den: f10,
},
_ => Split {
num: f28,
den: Decimal::ONE,
},
};
let f28k = share(split.num, Decimal::ONE, split.den)?.unwrap_or(Decimal::ZERO);
let f26 = match (&self.anlagen, &weights) {
(Anlagen::Gleichartig(_), Some((w, total))) => {
share(w[k], gated(|q, _| q.f23)?, *total)?.unwrap_or(Decimal::ZERO)
}
_ => gated(|q, k| q.anlagen[k].f23)?,
};
let f29 = gated(|q, _| q.f2)?;
let f30 = share(f29, Decimal::ONE, f11)?;
let f31 = share(f29, split.num, mul(split.den, f11)?)?.unwrap_or(Decimal::ZERO);
anlagen.push(AnlageMonat {
f26,
f28: f28k,
f29,
f30,
f31,
f32: add(f26, f31)?,
f35: f35[k],
f36: match f35[k] {
Some(v) => share(v, Decimal::ONE, f10)?,
None => None,
},
});
}
Ok(Monat {
faelle,
from,
to,
f3,
f4,
f5,
f6,
f7,
f8,
f9,
f10,
f11,
f12,
f13,
f14,
f15,
f16,
f17,
f18,
f19,
f20,
f21,
f28,
anlagen,
viertelstunden: quarters,
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum Einzelzaehlerfall<'a> {
Kolokation {
aw: Gate<'a>,
},
Netzspeicher,
OhneErzeugung,
}
#[derive(Debug, Clone, Copy)]
pub struct Einzelzaehler<'a> {
pub z1nb: &'a Series,
pub z1ne: &'a Series,
pub fall: Einzelzaehlerfall<'a>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct EinzelViertelstunde {
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub z1nb: Decimal,
pub z1ne: Decimal,
pub aw: Option<bool>,
pub quality: QualityFlag,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct EinzelMonat {
pub fall: Abgrenzungsfall,
pub from: OffsetDateTime,
pub to: OffsetDateTime,
pub f3: Decimal,
pub f4: Decimal,
pub f16: Option<Decimal>,
pub f20: Decimal,
pub f21: Decimal,
pub f38: Option<Decimal>,
pub f39: Option<Decimal>,
pub f40: Option<Decimal>,
pub f32: Option<Decimal>,
pub viertelstunden: Vec<EinzelViertelstunde>,
}
impl Einzelzaehler<'_> {
pub fn monat(&self) -> Result<EinzelMonat, EegError> {
let rows = align(&[("Z1NB", self.z1nb), ("Z1NE", self.z1ne)])?;
let (from, to) = one_month(&rows)?;
let aw = match self.fall {
Einzelzaehlerfall::Kolokation { aw } => Some(aw),
_ => None,
};
let viertelstunden: Vec<EinzelViertelstunde> = rows
.iter()
.map(|r| EinzelViertelstunde {
from: r.from,
to: r.to,
z1nb: r.values[0],
z1ne: r.values[1],
aw: aw.map(|g| g.passes(r.from)),
quality: r.quality,
})
.collect();
let f3 = sum(viertelstunden.iter().map(|q| q.z1nb))?;
let f4 = sum(viertelstunden.iter().map(|q| q.z1ne))?;
let mut m = EinzelMonat {
fall: Abgrenzungsfall::A10,
from,
to,
f3,
f4,
f16: None,
f20: f3,
f21: Decimal::ZERO,
f38: None,
f39: None,
f40: None,
f32: None,
viertelstunden,
};
match self.fall {
Einzelzaehlerfall::Netzspeicher => {}
Einzelzaehlerfall::OhneErzeugung => {
m.fall = Abgrenzungsfall::A11;
m.f16 = Some(f4);
m.f20 = f4.min(f3);
m.f21 = sub(f3, m.f20)?;
}
Einzelzaehlerfall::Kolokation { .. } => {
m.fall = Abgrenzungsfall::A8;
let f38 = sum(m
.viertelstunden
.iter()
.filter(|q| q.aw == Some(true))
.map(|q| q.z1ne))?;
let f40 = clamp0(sub(f4, f3)?);
m.f38 = Some(f38);
m.f39 = share(f38, Decimal::ONE, f4)?;
m.f40 = Some(f40);
m.f32 = Some(share(f38, f40, f4)?.unwrap_or(Decimal::ZERO));
}
}
Ok(m)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Jahr {
pub f22: Decimal,
pub f33: Vec<Decimal>,
}
fn one_year(mut spans: Vec<(OffsetDateTime, OffsetDateTime)>) -> Result<(), EegError> {
spans.sort_unstable();
let year = |t| DayBoundary::Strom.day_of(t).map(|d| d.year());
let first = spans.first().ok_or(EegError::Empty)?;
let y = year(first.0);
for w in spans.windows(2) {
if w[1].0 < w[0].1 {
return Err(EegError::Inconsistent {
what: "two months overlap",
});
}
}
if spans.iter().any(|s| year(s.0) != y) {
return Err(EegError::Inconsistent {
what: "the months span two calendar years",
});
}
Ok(())
}
pub fn jahr(monate: &[Monat]) -> Result<Jahr, EegError> {
one_year(monate.iter().map(|m| (m.from, m.to)).collect())?;
let plants = monate.first().map_or(0, |m| m.anlagen.len());
if monate.iter().any(|m| m.anlagen.len() != plants) {
return Err(EegError::Inconsistent {
what: "the months hold different numbers of plants",
});
}
Ok(Jahr {
f22: sum(monate.iter().map(|m| m.f21))?,
f33: (0..plants)
.map(|k| sum(monate.iter().map(|m| m.anlagen[k].f32)))
.collect::<Result<_, _>>()?,
})
}
pub fn jahr_einzelzaehler(monate: &[EinzelMonat]) -> Result<Jahr, EegError> {
one_year(monate.iter().map(|m| (m.from, m.to)).collect())?;
let fall = monate.first().map(|m| m.fall);
if monate.iter().any(|m| Some(m.fall) != fall) {
return Err(EegError::Inconsistent {
what: "the months mix Sonderfälle",
});
}
let f33 = if fall == Some(Abgrenzungsfall::A8) {
vec![sum(monate.iter().map(|m| m.f32.unwrap_or(Decimal::ZERO)))?]
} else {
Vec::new()
};
Ok(Jahr {
f22: sum(monate.iter().map(|m| m.f21))?,
f33,
})
}