use std::collections::{BTreeSet, HashMap};
use serde_json::{Value, json};
use super::{TextEmission, warn_extra_branch_rating_sets};
use crate::diagnostics::codes::EMIT_UCTE as F;
use crate::diagnostics::{DiagnosticInfo, Diagnostics, codes};
use crate::network::{
Area, BalancedNetwork, Branch, BranchCharging, Bus, BusId, BusType, Extras, Generator,
GeneratorEnergySource, Load, SourceFormat, Switch, TransformerControl, TransformerControlMode,
};
use crate::{Error, Result};
mod read;
mod write;
pub(crate) use read::parse_ucte_source;
pub(crate) use write::write_ucte;
const FMT: &str = "UCTE-DEF .uct";
pub(crate) const REVISION: &str = "2007.05.01";
const REVISIONS: [&str; 2] = ["2003.09.01", "2007.05.01"];
const BASE_MVA: f64 = 100.0;
const BASE_FREQUENCY: f64 = 50.0;
const MIN_REACTANCE_OHM: f64 = 0.05;
const DEFAULT_POWER_LIMIT: f64 = 9999.0;
const VOLTAGE_LEVELS_KV: [f64; 10] = [
750.0, 380.0, 220.0, 150.0, 120.0, 110.0, 70.0, 27.0, 330.0, 500.0,
];
const COUNTRIES: [(char, &str); 37] = [
('O', "AT"),
('A', "AL"),
('B', "BE"),
('V', "BG"),
('W', "BA"),
('3', "BY"),
('S', "CH"),
('C', "CZ"),
('D', "DE"),
('K', "DK"),
('E', "ES"),
('F', "FR"),
('5', "GB"),
('G', "GR"),
('M', "HU"),
('H', "HR"),
('I', "IT"),
('1', "LU"),
('6', "LT"),
('2', "MA"),
('7', "MD"),
('Y', "MK"),
('9', "NO"),
('N', "NL"),
('P', "PT"),
('Z', "PL"),
('R', "RO"),
('4', "RU"),
('8', "SE"),
('Q', "SK"),
('L', "SI"),
('T', "TR"),
('U', "UA"),
('0', "ME"),
('J', "RS"),
('_', "KS"),
('X', "XX"),
];
const CONTROL_AREA: &str = "ControlArea";
const CROSS_BORDER_AREA: &str = "CrossBorder";
const ORDER_CODES: &str = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0_-.";
const BUSBARS: &str = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const PLANT_TYPES: &str = "HNLCGOWF";
const SQRT_3: f64 = 1.732_050_807_568_877_2;
fn country_iso(letter: char) -> Option<&'static str> {
COUNTRIES
.iter()
.find(|(code, _)| *code == letter)
.map(|(_, iso)| *iso)
}
fn country_letter(iso: &str) -> Option<char> {
COUNTRIES
.iter()
.find(|(_, code)| code.eq_ignore_ascii_case(iso))
.map(|(letter, _)| *letter)
}
fn voltage_level_code(kv: f64) -> usize {
if kv < 27.0 {
return 7;
}
if kv > 750.0 {
return 0;
}
let mut best = 0;
for (index, level) in VOLTAGE_LEVELS_KV.iter().enumerate() {
if (kv - level).abs() < (kv - VOLTAGE_LEVELS_KV[best]).abs() {
best = index;
}
}
best
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct NodeCode([char; 8]);
impl NodeCode {
fn parse(text: &str) -> Option<Self> {
let chars: Vec<char> = text.chars().collect();
let code = Self(chars.try_into().ok()?);
(country_iso(code.country()).is_some() && code.0[6].is_ascii_digit()).then_some(code)
}
fn country(self) -> char {
self.0[0]
}
fn level(self) -> usize {
usize::from(self.0[6] as u8 - b'0')
}
fn base_kv(self) -> f64 {
VOLTAGE_LEVELS_KV[self.level()]
}
fn is_cross_border(self) -> bool {
self.country() == 'X'
}
fn text(self) -> String {
self.0.iter().collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct ElementId {
node1: NodeCode,
node2: NodeCode,
order: char,
}
impl ElementId {
fn text(self) -> String {
format!("{} {} {}", self.node1.text(), self.node2.text(), self.order)
}
}
#[derive(Clone, Debug, PartialEq)]
struct PhaseRegulation {
du: f64,
n: i64,
np: i64,
u: Option<f64>,
}
#[derive(Clone, Debug, PartialEq)]
struct AngleRegulation {
du: f64,
theta: f64,
n: i64,
np: i64,
p: Option<f64>,
symmetrical: bool,
type_stated: bool,
}
impl AngleRegulation {
fn ratio_alpha(&self, position: i64, ratio: f64) -> (f64, f64) {
let step = position as f64 * self.du / 100.0;
let dx = step * self.theta.to_radians().cos();
let dy = step * self.theta.to_radians().sin();
if self.symmetrical {
let half = dy / 2.0;
let coefficient = 2.0 / ratio - 1.0;
let alpha = (half * coefficient).atan2(1.0 + dx) + half.atan2(1.0 + dx);
let magnitude = ratio
* ((1.0 + half * half * coefficient * coefficient) / (1.0 + half * half)).sqrt();
(magnitude, alpha.to_degrees())
} else {
let real = ratio + dx;
(dy.hypot(real), dy.atan2(real).to_degrees())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_codes_follow_the_eight_character_rule() {
let code = NodeCode::parse("FFNGEN71").unwrap();
assert_eq!(code.country(), 'F');
assert_eq!(code.level(), 7);
assert!((code.base_kv() - 27.0).abs() < f64::EPSILON);
assert!(!code.is_cross_border());
assert!(NodeCode::parse("XFRBE_11").unwrap().is_cross_border());
assert_eq!(NodeCode::parse("ISSUE 21").unwrap().text(), "ISSUE 21");
assert!(NodeCode::parse("?FNGEN71").is_none());
assert!(NodeCode::parse("FFNGENA1").is_none());
assert!(NodeCode::parse("FFNGEN7").is_none());
}
#[test]
fn voltage_levels_map_to_the_nearest_code() {
assert_eq!(voltage_level_code(400.0), 1);
assert_eq!(voltage_level_code(230.0), 2);
assert_eq!(voltage_level_code(16.5), 7);
assert_eq!(voltage_level_code(800.0), 0);
assert_eq!(voltage_level_code(345.0), 8);
assert_eq!(voltage_level_code(500.0), 9);
}
#[test]
fn angle_regulation_matches_the_reference_formulas() {
let asym = AngleRegulation {
du: 8.0,
theta: 180.1,
n: 13,
np: 3,
p: None,
symmetrical: false,
type_stated: true,
};
let (magnitude, alpha) = asym.ratio_alpha(3, 1.0);
let step = 3.0 * 0.08;
let dx = step * 180.1f64.to_radians().cos();
let dy = step * 180.1f64.to_radians().sin();
assert!((magnitude - dy.hypot(1.0 + dx)).abs() < 1e-12);
assert!((alpha - dy.atan2(1.0 + dx).to_degrees()).abs() < 1e-12);
let (magnitude, alpha) = asym.ratio_alpha(3, 1.1);
assert!((magnitude - dy.hypot(1.1 + dx)).abs() < 1e-12);
assert!((alpha - dy.atan2(1.1 + dx).to_degrees()).abs() < 1e-12);
let symm = AngleRegulation {
symmetrical: true,
theta: 90.0,
du: 2.0,
n: 10,
np: -2,
p: None,
type_stated: true,
};
let (magnitude, alpha) = symm.ratio_alpha(-2, 1.0);
assert!((magnitude - 1.0).abs() < f64::EPSILON);
assert!((alpha - 2.0 * (-0.02f64).atan2(1.0).to_degrees()).abs() < 1e-12);
let (magnitude, alpha) = symm.ratio_alpha(-2, 1.05);
let coefficient = 2.0 / 1.05 - 1.0;
let expected_alpha = (-0.02_f64 * coefficient).atan2(1.0) + (-0.02_f64).atan2(1.0);
assert!((alpha - expected_alpha.to_degrees()).abs() < 1e-12);
let expected_magnitude =
1.05 * ((1.0 + 0.0004 * coefficient * coefficient) / 1.0004f64).sqrt();
assert!((magnitude - expected_magnitude).abs() < 1e-12);
}
}