use crate::circuit::{Circuit, CircuitElement};
use crate::error::SimError;
use crate::results::SimulationResult;
#[derive(Debug, Clone)]
pub struct DcSweepPoint {
pub sweep_value: f64,
pub result: SimulationResult,
}
#[derive(Debug, Clone)]
pub struct DcSweepResult {
pub source_id: String,
pub points: Vec<DcSweepPoint>,
}
impl DcSweepResult {
pub fn node_voltage_curve(&self, node: &str) -> Vec<(f64, f64)> {
self.points
.iter()
.filter_map(|p| {
p.result
.node_voltages
.get(node)
.map(|v| (p.sweep_value, *v))
})
.collect()
}
pub fn component_current_curve(&self, component_id: &str) -> Vec<(f64, f64)> {
self.points
.iter()
.filter_map(|p| {
p.result
.component_results
.iter()
.find(|c| c.id == component_id)
.map(|c| (p.sweep_value, c.current_through))
})
.collect()
}
}
pub fn dc_sweep(
circuit: &Circuit,
source_id: &str,
start: f64,
stop: f64,
num_points: usize,
) -> Result<DcSweepResult, SimError> {
if num_points < 2 {
return Err(SimError::InvalidComponent(
"DC sweep requires at least 2 points".to_string(),
));
}
let source_exists = circuit
.components
.iter()
.any(|c| matches!(c, CircuitElement::VoltageSource { id, .. } if id == source_id));
if !source_exists {
return Err(SimError::InvalidComponent(format!(
"Voltage source '{}' not found in circuit",
source_id
)));
}
let step = (stop - start) / (num_points - 1) as f64;
let mut points = Vec::with_capacity(num_points);
for i in 0..num_points {
let sweep_value = start + step * i as f64;
let mut swept_circuit = circuit.clone();
for component in &mut swept_circuit.components {
if let CircuitElement::VoltageSource { id, voltage, .. } = component {
if id == source_id {
*voltage = sweep_value;
}
}
}
let result = crate::solve_circuit(&swept_circuit)?;
points.push(DcSweepPoint {
sweep_value,
result,
});
}
Ok(DcSweepResult {
source_id: source_id.to_string(),
points,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::circuit::CircuitElement;
use approx::assert_relative_eq;
#[test]
fn sweep_resistor_linear() {
let circuit = Circuit {
ground_node: "0".into(),
components: vec![
CircuitElement::VoltageSource {
id: "V1".into(),
nodes: ["n1".into(), "0".into()],
voltage: 0.0,
waveform: None,
},
CircuitElement::Resistor {
id: "R1".into(),
nodes: ["n1".into(), "0".into()],
resistance: 1000.0,
},
],
};
let result = dc_sweep(&circuit, "V1", 0.0, 10.0, 11).unwrap();
assert_eq!(result.points.len(), 11);
for (i, point) in result.points.iter().enumerate() {
let expected_v = i as f64;
assert_relative_eq!(point.sweep_value, expected_v, epsilon = 1e-10);
assert_relative_eq!(point.result.node_voltages["n1"], expected_v, epsilon = 1e-6);
}
}
#[test]
fn sweep_diode_iv() {
let circuit = Circuit {
ground_node: "0".into(),
components: vec![
CircuitElement::VoltageSource {
id: "V1".into(),
nodes: ["n1".into(), "0".into()],
voltage: 0.0,
waveform: None,
},
CircuitElement::Resistor {
id: "R1".into(),
nodes: ["n1".into(), "n2".into()],
resistance: 100.0,
},
CircuitElement::Diode {
id: "D1".into(),
nodes: ["n2".into(), "0".into()],
temperature: 300.15,
},
],
};
let result = dc_sweep(&circuit, "V1", -1.0, 1.0, 21).unwrap();
assert_eq!(result.points.len(), 21);
let neg_point = &result.points[0]; let d1_neg = neg_point
.result
.component_results
.iter()
.find(|c| c.id == "D1")
.unwrap();
assert!(d1_neg.current_through.abs() < 1e-6);
let pos_point = result.points.last().unwrap(); let d1_pos = pos_point
.result
.component_results
.iter()
.find(|c| c.id == "D1")
.unwrap();
assert!(d1_pos.current_through > 1e-3);
}
#[test]
fn sweep_invalid_source() {
let circuit = Circuit {
ground_node: "0".into(),
components: vec![CircuitElement::Resistor {
id: "R1".into(),
nodes: ["n1".into(), "0".into()],
resistance: 1000.0,
}],
};
let result = dc_sweep(&circuit, "V_nonexistent", 0.0, 10.0, 11);
assert!(result.is_err());
}
#[test]
fn sweep_too_few_points() {
let circuit = Circuit {
ground_node: "0".into(),
components: vec![CircuitElement::VoltageSource {
id: "V1".into(),
nodes: ["n1".into(), "0".into()],
voltage: 0.0,
waveform: None,
}],
};
let result = dc_sweep(&circuit, "V1", 0.0, 10.0, 1);
assert!(result.is_err());
}
#[test]
fn sweep_curve_extraction() {
let circuit = Circuit {
ground_node: "0".into(),
components: vec![
CircuitElement::VoltageSource {
id: "V1".into(),
nodes: ["n1".into(), "0".into()],
voltage: 0.0,
waveform: None,
},
CircuitElement::Resistor {
id: "R1".into(),
nodes: ["n1".into(), "0".into()],
resistance: 1000.0,
},
],
};
let result = dc_sweep(&circuit, "V1", 0.0, 5.0, 6).unwrap();
let curve = result.node_voltage_curve("n1");
assert_eq!(curve.len(), 6);
let i_curve = result.component_current_curve("R1");
assert_eq!(i_curve.len(), 6);
assert_relative_eq!(i_curve.last().unwrap().1, 0.005, epsilon = 1e-6);
}
}