#![deny(missing_docs)]
#![forbid(unsafe_code)]
use pantometry_core::conserved::quantity;
use pantometry_core::{Domain, Exchange, Kind, Ledger, Reading, Violation};
use pantometry_units::{Current, Length, Power, Resistance, Temperature, Time, Voltage};
pub const HEAT: &str = quantity::ENERGY;
pub const COPPER_RESISTIVITY_20C: f64 = 1.724e-8;
pub mod conductor;
pub use conductor::Conductor;
pub const COPPER_ALPHA: f64 = 0.00393;
#[derive(Clone, Copy, Debug, PartialEq)]
enum Drive {
Current(f64),
Voltage(f64),
}
pub struct Winding {
name: String,
resistance_20c: f64,
alpha: f64,
temperature: f64,
drive: Drive,
reserve: f64,
dissipated: f64,
}
impl Winding {
pub fn of_copper(
name: impl Into<String>,
length: Length,
cross_section_m2: f64,
at: Temperature,
) -> Winding {
let r20 = COPPER_RESISTIVITY_20C * length.to_si() / cross_section_m2.max(f64::MIN_POSITIVE);
Winding {
name: name.into(),
resistance_20c: r20,
alpha: COPPER_ALPHA,
temperature: at.to_si(),
drive: Drive::Current(0.0),
reserve: f64::INFINITY,
dissipated: 0.0,
}
}
pub fn of_resistance(
name: impl Into<String>,
at_20c: Resistance,
alpha: f64,
at: Temperature,
) -> Winding {
Winding {
name: name.into(),
resistance_20c: at_20c.to_si(),
alpha,
temperature: at.to_si(),
drive: Drive::Current(0.0),
reserve: f64::INFINITY,
dissipated: 0.0,
}
}
pub fn driven_at(mut self, current: Current) -> Winding {
self.drive = Drive::Current(current.to_si());
self
}
pub fn driven_from(mut self, voltage: Voltage) -> Winding {
self.drive = Drive::Voltage(voltage.to_si());
self
}
pub fn with_reserve(mut self, joules: f64) -> Winding {
self.reserve = joules.max(0.0);
self
}
pub fn at_temperature(&mut self, t: Temperature) {
self.temperature = t.to_si();
}
pub fn resistance(&self) -> Resistance {
let dt = self.temperature - Temperature::celsius(20.0).to_si();
Resistance::from_si(self.resistance_20c * (1.0 + self.alpha * dt))
}
pub fn dissipation(&self) -> Power {
self.dissipation_at(Temperature::from_si(self.temperature))
}
pub fn resistance_at(&self, at: Temperature) -> Resistance {
let dt = at.to_si() - Temperature::celsius(20.0).to_si();
Resistance::from_si(self.resistance_20c * (1.0 + self.alpha * dt))
}
pub fn dissipation_at(&self, at: Temperature) -> Power {
let r = self.resistance_at(at).to_si();
Power::from_si(match self.drive {
Drive::Current(i) => i * i * r,
Drive::Voltage(v) if r > 0.0 => v * v / r,
Drive::Voltage(_) => 0.0,
})
}
pub fn runaway_current(&self, path: pantometry_units::Conductance) -> Option<Current> {
match self.drive {
Drive::Voltage(_) => None,
Drive::Current(_) => {
let denom = self.resistance_20c * self.alpha;
if denom <= 0.0 || !path.to_si().is_finite() || path.to_si() <= 0.0 {
return None;
}
Some(Current::from_si((path.to_si() / denom).sqrt()))
}
}
}
pub fn current(&self) -> Current {
let r = self.resistance().to_si();
Current::from_si(match self.drive {
Drive::Current(i) => i,
Drive::Voltage(v) if r > 0.0 => v / r,
Drive::Voltage(_) => 0.0,
})
}
pub fn voltage(&self) -> Voltage {
Voltage::from_si(match self.drive {
Drive::Current(i) => i * self.resistance().to_si(),
Drive::Voltage(v) => v,
})
}
pub fn dissipated_energy(&self) -> pantometry_units::Energy {
pantometry_units::Energy::from_si(self.dissipated)
}
pub fn reserve(&self) -> pantometry_units::Energy {
pantometry_units::Energy::from_si(self.reserve)
}
}
impl Domain for Winding {
fn books_balance(&self) -> bool {
true
}
fn name(&self) -> &str {
&self.name
}
fn kind(&self) -> Kind {
Kind::QuasiStatic
}
fn step(&mut self, _t: Time, dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
let watts = self.dissipation().to_si();
if !watts.is_finite() || watts < 0.0 {
return Err(Violation::at(
&self.name,
"dissipation is not a power",
watts,
));
}
if watts > 0.0 && !self.reserve.is_finite() {
return Err(Violation::at(
&self.name,
"no reserve was set, so this winding supplies energy from nowhere and the audit \
cannot see it: an infinite ledger entry is not a large number, it is one that \
cannot be subtracted. Call with_reserve",
watts,
));
}
let joules = (watts * dt.to_si()).min(self.reserve).max(0.0);
self.reserve -= joules;
self.dissipated += joules;
bus.publish(HEAT, joules);
Ok(())
}
fn ledger(&self) -> Ledger {
Ledger::new().with(quantity::ENERGY, self.reserve)
}
fn readings(&self) -> Vec<Reading> {
vec![
Reading::new(&self.name, "dissipating", self.dissipation().to_si(), "W"),
Reading::new(&self.name, "resistance", self.resistance().to_si(), "ohm"),
Reading::new(&self.name, "spent", self.dissipated.max(0.0), "J"),
]
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}