1use dune_netlist::ir::{AcSpec, Device, Netlist, Param, ParamValue};
25
26use crate::circuit::Circuit;
27use crate::consts::CTOK;
28use crate::error::Error;
29use crate::options::Options;
30use crate::solution::Solution;
31
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
52pub enum SweepParam {
53 Temp,
55 Instance { name: String, param: Option<String> },
60}
61
62impl SweepParam {
63 pub fn parse(text: &str) -> Result<Self, Error> {
67 let t = text.trim().to_ascii_lowercase();
68 let bad = || {
69 Error::Build(format!(
70 "sweep parameter `{text}`: expected `temp`, an element name or `@name[param]`"
71 ))
72 };
73 if t == "temp" {
74 return Ok(SweepParam::Temp);
75 }
76 if let Some(rest) = t.strip_prefix('@') {
77 let (name, param) = rest
78 .strip_suffix(']')
79 .and_then(|r| r.split_once('['))
80 .ok_or_else(bad)?;
81 let (name, param) = (name.trim(), param.trim());
82 if name.is_empty() || param.is_empty() || !is_word(name) || !is_word(param) {
83 return Err(bad());
84 }
85 let param = match param {
86 "r" => "resistance",
87 "c" => "capacitance",
88 other => other,
89 };
90 return Ok(SweepParam::Instance {
91 name: name.to_string(),
92 param: Some(param.to_string()),
93 });
94 }
95 if t.is_empty() || !is_word(&t) {
96 return Err(bad());
97 }
98 Ok(SweepParam::Instance {
99 name: t,
100 param: None,
101 })
102 }
103
104 pub fn key(&self) -> String {
107 match self {
108 SweepParam::Temp => "temp".into(),
109 SweepParam::Instance { name, param: None } => name.clone(),
110 SweepParam::Instance {
111 name,
112 param: Some(p),
113 } => format!("@{name}[{p}]"),
114 }
115 }
116}
117
118impl std::fmt::Display for SweepParam {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.write_str(&self.key())
121 }
122}
123
124impl std::str::FromStr for SweepParam {
125 type Err = Error;
126 fn from_str(s: &str) -> Result<Self, Error> {
127 SweepParam::parse(s)
128 }
129}
130
131fn is_word(s: &str) -> bool {
133 !s.chars()
134 .any(|c| c.is_whitespace() || matches!(c, '[' | ']' | '@' | '=' | '(' | ')' | ','))
135}
136
137#[derive(Debug)]
141pub struct SweepOutput {
142 pub solutions: Vec<Solution>,
143 pub notes: Vec<String>,
144}
145
146#[derive(Debug)]
151pub struct SweepRun {
152 pub values: Vec<f64>,
153 pub outcome: Result<SweepOutput, Error>,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq)]
158enum Slot {
159 Temp,
160 Value(usize),
162 SrcDc(usize),
163 SrcAcMag(usize),
164 SrcAcPhase(usize),
165 MosGeom(usize, &'static str),
167}
168
169fn resolve(nl: &Netlist, params: &[SweepParam]) -> Result<Vec<Slot>, Error> {
172 let mut slots = Vec::with_capacity(params.len());
173 for p in params {
174 let SweepParam::Instance { name, param } = p else {
175 if slots.contains(&Slot::Temp) {
176 return Err(Error::Build("sweep parameter `temp` given twice".into()));
177 }
178 slots.push(Slot::Temp);
179 continue;
180 };
181 let Some(idx) = nl.elements.iter().position(|e| &e.name == name) else {
182 return Err(Error::Build(format!(
183 "sweep parameter `{p}`: no element `{name}` in the circuit"
184 )));
185 };
186 let el = &nl.elements[idx];
187 let param = param.as_deref();
188 let slot = match (&el.device, param) {
189 (Device::Resistor { .. }, None | Some("resistance"))
190 | (Device::Capacitor { .. }, None | Some("capacitance"))
191 | (Device::Inductor { .. }, None | Some("inductance")) => Slot::Value(idx),
192 (Device::Inductor { .. }, Some("l")) => Slot::Value(idx),
194 (Device::Source { .. }, None | Some("dc")) => Slot::SrcDc(idx),
195 (Device::Source { .. }, Some("acmag")) => Slot::SrcAcMag(idx),
196 (Device::Source { .. }, Some("acphase")) => Slot::SrcAcPhase(idx),
197 (Device::Mosfet { .. }, Some("w")) => Slot::MosGeom(idx, "w"),
198 (Device::Mosfet { .. }, Some("l")) => Slot::MosGeom(idx, "l"),
199 (_, None) => {
200 return Err(Error::Build(format!(
201 "sweep parameter `{p}`: `{name}` has no primary value to sweep \
202 (R, C, L values and V/I DC levels do); name a parameter as `@{name}[...]`"
203 )));
204 }
205 (_, Some(other)) => {
206 return Err(Error::Build(format!(
207 "sweep parameter `{p}`: `{name}` has no sweepable parameter `{other}` \
208 (sweepable: R/C/L value, V/I dc/acmag/acphase, MOSFET w/l, temp)"
209 )));
210 }
211 };
212 if let Some(j) = slots.iter().position(|s| *s == slot) {
213 return Err(Error::Build(format!(
214 "sweep parameter `{p}` given twice (also as `{}`)",
215 params[j]
216 )));
217 }
218 slots.push(slot);
219 }
220 Ok(slots)
221}
222
223fn assign(nl: &mut Netlist, opts: &mut Options, slots: &[Slot], values: &[f64]) {
225 for (slot, &v) in slots.iter().zip(values) {
226 match *slot {
227 Slot::Temp => opts.temp = v + CTOK,
229 Slot::Value(i) => match &mut nl.elements[i].device {
230 Device::Resistor { value, .. }
231 | Device::Capacitor { value, .. }
232 | Device::Inductor { value, .. } => *value = Some(v),
233 _ => unreachable!("resolved as an R/C/L value"),
234 },
235 Slot::SrcDc(i) => {
236 if let Device::Source { dc, .. } = &mut nl.elements[i].device {
237 *dc = Some(v);
238 }
239 }
240 Slot::SrcAcMag(i) | Slot::SrcAcPhase(i) => {
241 if let Device::Source { ac, .. } = &mut nl.elements[i].device {
242 let spec = ac.get_or_insert(AcSpec {
246 mag: 0.0,
247 phase: 0.0,
248 });
249 if matches!(slot, Slot::SrcAcMag(_)) {
250 spec.mag = v;
251 } else {
252 spec.phase = v;
253 }
254 }
255 }
256 Slot::MosGeom(i, key) => {
257 let span = nl.elements[i].span;
258 if let Device::Mosfet { params, .. } = &mut nl.elements[i].device {
259 let mut found = false;
262 for p in params.iter_mut().filter(|p| p.name == key) {
263 p.value = ParamValue::Num(v);
264 found = true;
265 }
266 if !found {
267 params.push(Param {
268 name: key.to_string(),
269 value: ParamValue::Num(v),
270 span,
271 });
272 }
273 }
274 }
275 }
276 }
277}
278
279fn run_one(nl: &Netlist, opts: &Options, slots: &[Slot], values: &[f64]) -> SweepRun {
281 let mut nl = nl.clone();
282 let mut opts = opts.clone();
283 assign(&mut nl, &mut opts, slots, values);
284 let outcome = (|| {
285 let (circuit, mut notes) = Circuit::from_netlist(&nl)?;
286 let mut solutions = Vec::new();
287 for analysis in &nl.analyses {
288 let (sols, n) = circuit.run(analysis, &opts)?;
289 notes.extend(n);
290 solutions.extend(sols);
291 }
292 Ok(SweepOutput { solutions, notes })
293 })();
294 SweepRun {
295 values: values.to_vec(),
296 outcome,
297 }
298}
299
300fn prepare(nl: &Netlist, params: &[SweepParam], runs: &[Vec<f64>]) -> Result<Vec<Slot>, Error> {
303 let slots = resolve(nl, params)?;
304 for (r, row) in runs.iter().enumerate() {
305 if row.len() != params.len() {
306 return Err(Error::Build(format!(
307 "sweep run {r}: {} values for {} parameters",
308 row.len(),
309 params.len()
310 )));
311 }
312 if let Some((p, v)) = params.iter().zip(row).find(|(_, v)| !v.is_finite()) {
313 return Err(Error::Build(format!(
314 "sweep run {r}: `{p}` = {v} is not finite"
315 )));
316 }
317 }
318 Ok(slots)
319}
320
321pub fn sweep(
335 netlist: &Netlist,
336 opts: &Options,
337 params: &[SweepParam],
338 runs: &[Vec<f64>],
339) -> Result<Vec<SweepRun>, Error> {
340 let slots = prepare(netlist, params, runs)?;
341 Ok(runs
342 .iter()
343 .map(|row| run_one(netlist, opts, &slots, row))
344 .collect())
345}
346
347#[cfg(feature = "parallel")]
354pub fn sweep_parallel(
355 netlist: &Netlist,
356 opts: &Options,
357 params: &[SweepParam],
358 runs: &[Vec<f64>],
359 threads: usize,
360) -> Result<Vec<SweepRun>, Error> {
361 use rayon::prelude::*;
362 let slots = prepare(netlist, params, runs)?;
363 let pool = rayon::ThreadPoolBuilder::new()
364 .num_threads(threads)
365 .build()
366 .map_err(|e| Error::Build(format!("sweep thread pool: {e}")))?;
367 Ok(pool.install(|| {
368 runs.par_iter()
369 .map(|row| run_one(netlist, opts, &slots, row))
370 .collect()
371 }))
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn parse_forms() {
380 assert_eq!(SweepParam::parse("TEMP").unwrap(), SweepParam::Temp);
381 assert_eq!(
382 SweepParam::parse("R1").unwrap(),
383 SweepParam::Instance {
384 name: "r1".into(),
385 param: None
386 }
387 );
388 let w = SweepParam::parse("@M1[W]").unwrap();
389 assert_eq!(w.key(), "@m1[w]");
390 assert_eq!(SweepParam::parse(&w.key()).unwrap(), w);
391 assert_eq!(
392 SweepParam::parse("@r.x1.r1[r]").unwrap().key(),
393 "@r.x1.r1[resistance]"
394 );
395 for bad in ["", "@", "@r1", "@r1[]", "@[w]", "r 1", "@m1[w", "a=b"] {
396 assert!(SweepParam::parse(bad).is_err(), "{bad:?}");
397 }
398 }
399}