//! The PhysicsCircuits library (plan D13): lumped circuit elements the way //! physics papers draw them (superconducting circuits, rf and lab //! electronics), and three examples. Every part is drawn in a 1800 x 1800 //! frame (1.5 inch) around the centre (900, 900), horizontal first. //! //! * Two-terminal parts: the pins are (300, 900) and (1500, 900), exactly //! one inch apart, on the 1/4-inch grid. The `_v` version is the part turned //! by 90 degrees: pins (900, 300) and (900, 1500). //! * Grounds: one pin; `_v` has it at the top (900, 300) with the symbol //! below, `_h` has it at the left (300, 900). //! * Transformers: four pins at (600, 300), (600, 1500), (1200, 300), //! (1200, 1500) (`_v`); `_h` is turned. //! //! The set fills the gaps of xfig's Electronic library: it has no Josephson //! junction, SQUID, transmission line, tunable elements or protective earth, //! and its parts do not share one length. use super::*; use rustfig_geom::Affine; type Pt = (f64, f64); type Entry = (String, String, Vec, Vec); /// Frame centre and the two pins of a horizontal two-terminal part. const C: f64 = 900.0; const PIN_A: Pt = (300.0, 900.0); const PIN_B: Pt = (1500.0, 900.0); /// Names of the two-terminal parts (published horizontal; the catalog also /// keeps a turned `_v` version for the examples). pub const TWO_TERMINAL: [&str; 17] = [ "resistor_iec", "resistor_zigzag", "capacitor", "inductor", "jj_box", "jj_cross", "squid", "voltage_dc", "voltage_ac", "current_dc", "current_ac", "battery", "transmission_line", "resistor_var", "capacitor_var", "inductor_var", "jj_var", ]; fn pt(q: Pt) -> Point { p(q.0, q.1) } fn ln(th: i32, v: &[Pt]) -> Object { polyline(solid(th, 50), v.iter().map(|&q| pt(q)).collect(), None) } fn lead(a: Pt, b: Pt) -> Object { ln(2, &[a, b]) } fn rect(th: i32, a: Pt, b: Pt) -> Object { Object::new(ObjectKind::Polyline(Polyline::rect(solid(th, 50), pt(a), pt(b)))) } fn ring(c: Pt, r: f64, th: i32) -> Object { circle(pt(c), r, solid(th, 50)) } fn ellipse(c: Pt, rx: f64, ry: f64) -> Object { Object::new(ObjectKind::Ellipse(Ellipse { kind: EllipseKind::EllipseRadii, style: solid(2, 50), direction: 1, angle: 0.0, center: pt(c), radii: p(rx, ry), start: pt(c), end: p(c.0 + rx, c.1 + ry), })) } fn filled_dot(c: Pt, r: f64) -> Object { let mut s = solid(1, 40); s.fill_color = ColorRef::BLACK; s.area_fill = 20; circle(pt(c), r, s) } fn arrow(a: Pt, b: Pt) -> Object { polyline(solid(1, 48), vec![pt(a), pt(b)], Some(arrow_head(120.0))) } /// A thin line with an arrow head at each end. fn double_arrow(a: Pt, b: Pt) -> Object { let mut l = Polyline::new(PolyKind::Polyline, solid(1, 48), vec![pt(a), pt(b)]); l.forward_arrow = Some(arrow_head(120.0)); l.backward_arrow = Some(arrow_head(120.0)); Object::new(ObjectKind::Polyline(l)) } /// The tunable-element arrow, across the part. fn tune() -> Object { arrow((690.0, 1110.0), (1110.0, 690.0)) } fn wave(x0: f64, x1: f64, y: f64, amp: f64) -> Object { let n = 16; let v: Vec = (0..=n) .map(|i| { let t = f64::from(i) / f64::from(n); (x0 + (x1 - x0) * t, y - amp * (2.0 * PI * t).sin()) }) .collect(); ln(2, &v) } /// Leads from the pins to `a` and from `b` to the other pin. fn leads(a: f64, b: f64) -> Vec { vec![lead(PIN_A, (a, C)), lead((b, C), PIN_B)] } fn cross(c: Pt, h: f64) -> Vec { vec![ ln(3, &[(c.0 - h, c.1 - h), (c.0 + h, c.1 + h)]), ln(3, &[(c.0 - h, c.1 + h), (c.0 + h, c.1 - h)]), ] } fn zigzag() -> Object { let mut v = vec![PIN_A, (540.0, C)]; for (i, y) in [780.0, 1020.0, 780.0, 1020.0, 780.0, 1020.0].iter().enumerate() { v.push((600.0 + 120.0 * i as f64, *y)); } v.push((1260.0, C)); v.push(PIN_B); ln(2, &v) } /// A coil of `n` humps between `x0` and `x1` on the line y = 900, bulging up. fn coil(x0: f64, x1: f64, n: usize) -> Vec { let w = (x1 - x0) / n as f64; let mut v = Vec::new(); for h in 0..n { for i in 0..=10 { if h > 0 && i == 0 { continue; } let t = PI * f64::from(i) / 10.0; v.push((x0 + w * h as f64 + w / 2.0 * (1.0 - t.cos()), C - w / 2.0 * t.sin())); } } v } fn inductor() -> Vec { let mut v = vec![PIN_A]; v.extend(coil(540.0, 1260.0, 4)); v.push(PIN_B); vec![ln(2, &v)] } fn capacitor() -> Vec { let mut o = leads(840.0, 960.0); o.push(ln(3, &[(840.0, 720.0), (840.0, 1080.0)])); o.push(ln(3, &[(960.0, 720.0), (960.0, 1080.0)])); o } fn jj_box() -> Vec { let mut o = leads(780.0, 1020.0); o.push(rect(2, (780.0, 780.0), (1020.0, 1020.0))); o.extend(cross((900.0, 900.0), 120.0)); o } fn jj_cross() -> Vec { let mut o = leads(780.0, 1020.0); o.extend(cross((900.0, 900.0), 120.0)); o } fn squid() -> Vec { let mut o = vec![ lead(PIN_A, (600.0, C)), lead((1200.0, C), PIN_B), ln(2, &[(600.0, C), (600.0, 660.0), (840.0, 660.0)]), ln(2, &[(960.0, 660.0), (1200.0, 660.0), (1200.0, C)]), ln(2, &[(600.0, C), (600.0, 1140.0), (840.0, 1140.0)]), ln(2, &[(960.0, 1140.0), (1200.0, 1140.0), (1200.0, C)]), ]; o.extend(cross((900.0, 660.0), 60.0)); o.extend(cross((900.0, 1140.0), 60.0)); o } const SR: f64 = 270.0; fn source_circle() -> Vec { let mut o = leads(900.0 - SR, 900.0 + SR); o.push(ring((C, C), SR, 2)); o } fn voltage_dc() -> Vec { let mut o = source_circle(); o.push(ln(2, &[(750.0, C), (810.0, C)])); o.push(ln(2, &[(780.0, 870.0), (780.0, 930.0)])); o.push(ln(2, &[(990.0, C), (1050.0, C)])); o } fn voltage_ac() -> Vec { let mut o = source_circle(); o.push(wave(780.0, 1020.0, C, 60.0)); o } fn current_dc() -> Vec { let mut o = source_circle(); o.push(arrow((720.0, C), (1080.0, C))); o } fn current_ac() -> Vec { let mut o = source_circle(); o.push(wave(780.0, 1020.0, 810.0, 45.0)); o.push(arrow((750.0, 1000.0), (1050.0, 1000.0))); o } fn battery() -> Vec { let mut o = leads(840.0, 960.0); o.push(ln(3, &[(840.0, 690.0), (840.0, 1110.0)])); o.push(ln(3, &[(960.0, 780.0), (960.0, 1020.0)])); o.push(ln(2, &[(715.0, 800.0), (775.0, 800.0)])); o.push(ln(2, &[(745.0, 770.0), (745.0, 830.0)])); o } fn transmission_line() -> Vec { let mut o = leads(570.0, 1230.0); o.push(ellipse((C, C), 330.0, 150.0)); o.push(ln(2, &[(600.0, C), (1200.0, C)])); o } fn two_terminal(name: &str) -> Vec { match name { "resistor_iec" => { let mut o = leads(600.0, 1200.0); o.push(rect(2, (600.0, 780.0), (1200.0, 1020.0))); o } "resistor_zigzag" => vec![zigzag()], "capacitor" => capacitor(), "inductor" => inductor(), "jj_box" => jj_box(), "jj_cross" => jj_cross(), "squid" => squid(), "voltage_dc" => voltage_dc(), "voltage_ac" => voltage_ac(), "current_dc" => current_dc(), "current_ac" => current_ac(), "battery" => battery(), "transmission_line" => transmission_line(), "resistor_var" => { let mut o = two_terminal("resistor_iec"); o.push(tune()); o } "capacitor_var" => { let mut o = capacitor(); o.push(tune()); o } "inductor_var" => { let mut o = inductor(); o.push(tune()); o } "jj_var" => { let mut o = jj_cross(); o.push(tune()); o } _ => Vec::new(), } } fn what(name: &str) -> &'static str { match name { "resistor_iec" => "resistor, rectangle (IEC)", "resistor_zigzag" => "resistor, zigzag", "capacitor" => "capacitor", "inductor" => "inductor (coil)", "jj_box" => "Josephson junction, cross in a box", "jj_cross" => "Josephson junction, plain cross", "squid" => "SQUID loop with two Josephson junctions", "voltage_dc" => "DC voltage source (+ on the first pin)", "voltage_ac" => "AC voltage source", "current_dc" => "DC current source", "current_ac" => "AC current source", "battery" => "battery (long plate is +)", "transmission_line" => "transmission-line segment", "resistor_var" => "variable resistor (arrow)", "capacitor_var" => "variable capacitor (arrow)", "inductor_var" => "variable inductor (arrow)", "jj_var" => "tunable Josephson junction (arrow)", _ => "", } } /// Stem from the pin (900, 300) down to `y`. fn stem(y: f64) -> Object { lead((C, 300.0), (C, y)) } fn bars(y0: f64, dy: f64, halves: &[f64]) -> Vec { halves .iter() .enumerate() .map(|(i, h)| { let y = y0 + dy * i as f64; ln(3, &[(C - h, y), (C + h, y)]) }) .collect() } fn triangle(filled: bool) -> Object { let mut s = solid(2, 50); if filled { s.fill_color = ColorRef::BLACK; s.area_fill = 20; } let pp = [(660.0, 600.0), (1140.0, 600.0), (C, 900.0), (660.0, 600.0)]; Object::new(ObjectKind::Polyline(Polyline::new( PolyKind::Polygon, s, pp.iter().map(|&q| pt(q)).collect(), ))) } /// Ground symbols, drawn for `_v` (pin at the top). fn ground(name: &str) -> Vec { match name { "ground_earth" => { let mut o = vec![stem(600.0)]; o.extend(bars(600.0, 90.0, &[240.0, 150.0, 60.0])); o } "ground_chassis" => { let mut o = vec![stem(600.0), ln(3, &[(660.0, 600.0), (1140.0, 600.0)])]; for i in 0..5 { let x = 720.0 + 90.0 * f64::from(i); o.push(ln(2, &[(x, 600.0), (x - 90.0, 720.0)])); } o } "ground_protective" => { let mut o = vec![stem(570.0), ring((C, 780.0), 210.0, 2)]; o.extend(bars(720.0, 60.0, &[150.0, 100.0, 50.0])); o } "ground_analog" => vec![stem(600.0), triangle(false)], "ground_digital" => vec![stem(600.0), triangle(true)], _ => Vec::new(), } } /// A transformer or mutual inductance, drawn for `_v`: coil axes vertical. fn coupled(core: bool) -> Vec { let side = |x: f64, k: f64| { let mut v = vec![(x, 300.0)]; // Turn the horizontal coil: its run (x) becomes y, its bulge x. v.extend(coil(540.0, 1260.0, 4).iter().map(|&(cx, cy)| (x + k * (C - cy), cx))); v.push((x, 1500.0)); ln(2, &v) }; let mut o = vec![side(600.0, -1.0), side(1200.0, 1.0)]; if core { o.push(ln(2, &[(870.0, 540.0), (870.0, 1260.0)])); o.push(ln(2, &[(930.0, 540.0), (930.0, 1260.0)])); o.push(filled_dot((690.0, 450.0), 30.0)); o.push(filled_dot((1110.0, 450.0), 30.0)); } else { o.push(double_arrow((720.0, C), (1080.0, C))); } o } fn port() -> Vec { vec![lead(PIN_A, (1410.0, C)), ring((1500.0, C), 90.0, 2)] } /// Rotate frame objects by `deg` (counter-clockwise on screen) about the /// frame centre, then move them by `d`. fn place(objs: &[Object], d: Pt, deg: f64) -> Vec { let t = Affine::about(Affine::rotate(deg.to_radians()), Point::new(C, C)).then(&Affine::translate(d.0, d.1)); objs.iter() .cloned() .map(|mut o| { o.transform(&t, false); o }) .collect() } /// A frame point turned by `deg` about the frame centre. fn rot_pt(q: Pt, deg: f64) -> Pt { let (s, c) = deg.to_radians().sin_cos(); let (x, y) = (q.0 - C, q.1 - C); (C + x * c + y * s, C - x * s + y * c) } fn catalog() -> Vec { let mut v: Vec = Vec::new(); let mut add = |name: String, text: String, objs: Vec, pins: &[Pt], deg: f64| { let o = place(&objs, (0.0, 0.0), deg); let rp: Vec = pins .iter() .map(|&q| { let r = rot_pt(q, deg); (r.0.round(), r.1.round()) }) .collect(); v.push((name, text, o, rp)); }; for name in TWO_TERMINAL { for (suffix, deg, orient) in [("h", 0.0, "horizontal"), ("v", 90.0, "vertical")] { add( format!("{name}_{suffix}"), format!("{}, {orient}; pins 1 inch apart on the grid", what(name)), two_terminal(name), &[PIN_A, PIN_B], deg, ); } } for (name, text) in [ ("ground_earth", "earth ground (three bars)"), ("ground_chassis", "chassis or frame ground (hatched bar)"), ("ground_protective", "protective earth (earth bars in a circle)"), ("ground_analog", "analog ground (open triangle)"), ("ground_digital", "digital ground (filled triangle)"), ] { add( format!("{name}_v"), format!("{text}, pin on top"), ground(name), &[(C, 300.0)], 0.0, ); add( format!("{name}_h"), format!("{text}, pin on the left"), ground(name), &[(C, 300.0)], 90.0, ); } let pins = [(600.0, 300.0), (600.0, 1500.0), (1200.0, 300.0), (1200.0, 1500.0)]; for (suffix, deg, orient) in [("h", 90.0, "horizontal"), ("v", 0.0, "vertical")] { add( format!("transformer_{suffix}"), format!("transformer with iron core, {orient}; four pins on the grid"), coupled(true), &pins, deg, ); add( format!("mutual_inductance_{suffix}"), format!("two inductively coupled coils (mutual inductance), {orient}; four pins on the grid"), coupled(false), &pins, deg, ); } for (suffix, deg, orient) in [("h", 0.0, "horizontal"), ("v", 90.0, "vertical")] { add( format!("port_{suffix}"), format!("port: wire ending in an open terminal circle, {orient}; the pin is the free wire end"), port(), &[PIN_A], deg, ); } add( "terminal".into(), "terminal: open circle, the pin is its centre".into(), vec![ring((C, C), 90.0, 2)], &[(C, C)], 0.0, ); add( "junction".into(), "wire junction: filled dot, the pin is its centre".into(), vec![filled_dot((C, C), 60.0)], &[(C, C)], 0.0, ); v } fn wires(v: &[&[Pt]]) -> Vec { v.iter().filter_map(|w| Some(lead(*w.first()?, *w.get(1)?))).collect() } fn examples(out: &mut Vec) { let cat = catalog(); // A part by file name, moved so that its centre is at `c` (parts with // one pin: so that the pin is at `c`). let at = |name: &str, c: Pt| -> Vec { let Some(e) = cat.iter().find(|e| e.0 == name) else { return Vec::new(); }; let one_pin = e.3.len() == 1; let q = e.3.first().copied().unwrap_or((C, C)); let d = if one_pin { (c.0 - q.0, c.1 - q.1) } else { (c.0 - C, c.1 - C) }; place(&e.2, d, 0.0) }; let mut ex = |name: &str, text: &str, objs: Vec| { let c1 = format!("Physics circuits example: {text}"); out.push(part( name, objs, &[ &c1, "Made from the library's parts; edit the labels (LaTeX text). Keywords: circuit example physics", ], Vec::new(), )); }; // Series RLC circuit driven by an AC source. let mut o = at("voltage_ac_v", (600.0, 1500.0)); o.extend(at("resistor_iec_h", (1500.0, 900.0))); o.extend(at("inductor_h", (3300.0, 900.0))); o.extend(at("capacitor_v", (3900.0, 1500.0))); o.extend(at("ground_earth_v", (2250.0, 2100.0))); o.extend(wires(&[ &[(600.0, 900.0), (900.0, 900.0)], &[(2100.0, 900.0), (2700.0, 900.0)], &[(600.0, 2100.0), (3900.0, 2100.0)], ])); o.extend(at("junction", (2250.0, 2100.0))); o.push(label(150.0, 1500.0, "$V(t)$")); o.push(label(1500.0, 660.0, "$R$")); o.push(label(3300.0, 660.0, "$L$")); o.push(label(4350.0, 1500.0, "$C$")); ex("example_rlc", "series RLC circuit driven by an AC voltage source", o); // Transmon: Josephson junction and shunt capacitor to ground, coupled // to a drive line through a coupling capacitor. let mut o = at("terminal", (300.0, 900.0)); o.extend(at("capacitor_h", (1200.0, 900.0))); o.extend(at("jj_box_v", (2400.0, 1500.0))); o.extend(at("capacitor_v", (3300.0, 1500.0))); o.extend(at("ground_earth_v", (2850.0, 2100.0))); o.extend(wires(&[ &[(390.0, 900.0), (600.0, 900.0)], &[(1800.0, 900.0), (3300.0, 900.0)], &[(2400.0, 2100.0), (3300.0, 2100.0)], ])); o.extend(at("junction", (2400.0, 900.0))); o.extend(at("junction", (2850.0, 2100.0))); o.push(label(1200.0, 660.0, "$C_c$")); o.push(label(1950.0, 1500.0, "$E_J$")); o.push(label(3750.0, 1500.0, "$C_S$")); o.push(label(300.0, 660.0, "drive")); ex( "example_transmon", "transmon qubit: Josephson junction and shunt capacitor, coupled through a capacitor", o, ); // LC resonator coupled to a transmission line. let mut o = at("terminal", (300.0, 900.0)); o.extend(at("transmission_line_h", (1500.0, 900.0))); o.extend(at("capacitor_v", (3000.0, 1500.0))); o.extend(at("inductor_v", (2400.0, 2700.0))); o.extend(at("capacitor_v", (3600.0, 2700.0))); o.extend(at("ground_earth_v", (3000.0, 3300.0))); o.extend(at("terminal", (4200.0, 900.0))); o.extend(wires(&[ &[(390.0, 900.0), (900.0, 900.0)], &[(2100.0, 900.0), (4110.0, 900.0)], &[(2400.0, 2100.0), (3600.0, 2100.0)], &[(2400.0, 3300.0), (3600.0, 3300.0)], ])); o.extend(at("junction", (3000.0, 900.0))); o.extend(at("junction", (3000.0, 2100.0))); o.extend(at("junction", (3000.0, 3300.0))); o.push(label(1500.0, 660.0, "$Z_0$")); o.push(label(3450.0, 1500.0, "$C_c$")); o.push(label(1950.0, 2700.0, "$L_r$")); o.push(label(4050.0, 2700.0, "$C_r$")); ex( "example_resonator", "LC resonator coupled to a transmission line through a capacitor", o, ); } /// The name a catalog entry is published under, or `None` for the turned /// versions that only the examples use: one orientation per part (the user's /// decision, 2026-10-25: turning a part is one click in xfig). Two-terminal /// parts and the port lie horizontal; grounds point down and the coupled /// coils stand upright, as drawn. fn published_name(name: &str) -> Option { if let Some(base) = name.strip_suffix("_h") { let horizontal = TWO_TERMINAL.contains(&base) || base == "port"; return horizontal.then(|| base.to_string()); } if let Some(base) = name.strip_suffix("_v") { let upright = base.starts_with("ground_") || base == "transformer" || base == "mutual_inductance"; return upright.then(|| base.to_string()); } Some(name.to_string()) } /// Every figure of the PhysicsCircuits library, in file-name order. pub fn physics_circuits() -> Vec { let mut v: Vec = catalog() .into_iter() .filter_map(|(name, text, objs, pins)| Some((published_name(&name)?, text, objs, pins))) .map(|(name, text, objs, pins)| { let c1 = format!("Physics circuits: {text}"); let c2 = "Keywords: circuit physics electronics resistor capacitor inductor Josephson ground source"; part(&name, objs, &[&c1, c2], pins.iter().map(|&q| p(q.0, q.1)).collect()) }) .collect(); examples(&mut v); v.sort_by(|a, b| a.name.cmp(&b.name)); v } .