ee-toolkit 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ee_toolkit/opamp.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ Op-amp circuit calculations.
3
+
4
+ Amplifiers
5
+ ----------
6
+ Non-inverting: Gain = 1 + Rf / R1
7
+ Inverting: Gain = -(Rf / R1)
8
+
9
+ Integrator (inverting)
10
+ -----------------------
11
+ Corner frequency: f = 1 / (2π · R · C)
12
+ Gain at frequency: |A(f)| = 1 / (2π · f · R · C) [no DC description – ideal]
13
+
14
+ Differentiator (inverting)
15
+ ---------------------------
16
+ Corner frequency: f = 1 / (2π · R · C)
17
+ Gain at frequency: |A(f)| = 2π · f · R · C
18
+ """
19
+ import math
20
+ import click
21
+ from .units import parse_si, fmt_si
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Pure math helpers
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def noninv_gain(rf: float, r1: float) -> float:
29
+ """Non-inverting amplifier closed-loop gain."""
30
+ return 1.0 + rf / r1
31
+
32
+
33
+ def inv_gain(rf: float, r1: float) -> float:
34
+ """Inverting amplifier closed-loop gain (negative = phase-inverting)."""
35
+ return -(rf / r1)
36
+
37
+
38
+ def integrator_corner(r: float, c: float) -> float:
39
+ """Integrator corner frequency (Hz) — same as RC cutoff."""
40
+ return 1.0 / (2.0 * math.pi * r * c)
41
+
42
+
43
+ def integrator_gain_at(r: float, c: float, freq: float) -> float:
44
+ """Ideal integrator magnitude gain at *freq* Hz."""
45
+ return 1.0 / (2.0 * math.pi * freq * r * c)
46
+
47
+
48
+ def differentiator_corner(r: float, c: float) -> float:
49
+ """Differentiator corner frequency (Hz)."""
50
+ return 1.0 / (2.0 * math.pi * r * c)
51
+
52
+
53
+ def differentiator_gain_at(r: float, c: float, freq: float) -> float:
54
+ """Ideal differentiator magnitude gain at *freq* Hz."""
55
+ return 2.0 * math.pi * freq * r * c
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Click command group
60
+ # ---------------------------------------------------------------------------
61
+
62
+ @click.group()
63
+ def opamp_group():
64
+ """Op-amp circuit calculations (gain, integrator, differentiator)."""
65
+
66
+
67
+ # ── Non-inverting amplifier ─────────────────────────────────────────────────
68
+ @opamp_group.command('noninv')
69
+ @click.option('--rf', 'rf_str', required=True, metavar='VALUE',
70
+ help='Feedback resistor, e.g. 100k')
71
+ @click.option('--r1', 'r1_str', required=True, metavar='VALUE',
72
+ help='Input/ground resistor, e.g. 10k')
73
+ def cmd_noninv(rf_str, r1_str):
74
+ """Non-inverting amplifier closed-loop gain.
75
+
76
+ \b
77
+ Gain = 1 + Rf / R1
78
+
79
+ Example:
80
+ ee-calc opamp noninv --rf 100k --r1 10k → Gain = +11
81
+ """
82
+ rf = parse_si(rf_str)
83
+ r1 = parse_si(r1_str)
84
+ g = noninv_gain(rf, r1)
85
+ click.echo("Non-inverting amplifier")
86
+ click.echo(f" Rf = {fmt_si(rf, 'Ω')}")
87
+ click.echo(f" R1 = {fmt_si(r1, 'Ω')}")
88
+ click.echo(f" Gain = 1 + Rf/R1 = {g:.6g} ({20*math.log10(abs(g)):.2f} dB)")
89
+
90
+
91
+ # ── Inverting amplifier ─────────────────────────────────────────────────────
92
+ @opamp_group.command('inv')
93
+ @click.option('--rf', 'rf_str', required=True, metavar='VALUE',
94
+ help='Feedback resistor, e.g. 100k')
95
+ @click.option('--r1', 'r1_str', required=True, metavar='VALUE',
96
+ help='Input resistor, e.g. 10k')
97
+ def cmd_inv(rf_str, r1_str):
98
+ """Inverting amplifier closed-loop gain.
99
+
100
+ \b
101
+ Gain = -(Rf / R1)
102
+
103
+ Example:
104
+ ee-calc opamp inv --rf 100k --r1 10k → Gain = -10
105
+ """
106
+ rf = parse_si(rf_str)
107
+ r1 = parse_si(r1_str)
108
+ g = inv_gain(rf, r1)
109
+ click.echo("Inverting amplifier")
110
+ click.echo(f" Rf = {fmt_si(rf, 'Ω')}")
111
+ click.echo(f" R1 = {fmt_si(r1, 'Ω')}")
112
+ click.echo(f" Gain = -(Rf/R1) = {g:.6g} ({20*math.log10(abs(g)):.2f} dB)")
113
+
114
+
115
+ # ── Integrator ──────────────────────────────────────────────────────────────
116
+ @opamp_group.command('integrator')
117
+ @click.option('--r', 'r_str', required=True, metavar='VALUE',
118
+ help='Input resistor, e.g. 10k')
119
+ @click.option('--c', 'c_str', required=True, metavar='VALUE',
120
+ help='Feedback capacitor, e.g. 100n')
121
+ @click.option('--freq', 'freq_str', default=None, metavar='VALUE',
122
+ help='Optional: evaluate gain magnitude at this frequency, e.g. 1k')
123
+ def cmd_integrator(r_str, c_str, freq_str):
124
+ """Inverting op-amp integrator.
125
+
126
+ \b
127
+ Corner frequency: f₀ = 1 / (2π · R · C)
128
+ Gain magnitude: |A(f)| = 1 / (2π · f · R · C) = f₀ / f
129
+
130
+ Example:
131
+ ee-calc opamp integrator --r 10k --c 100n
132
+ ee-calc opamp integrator --r 10k --c 100n --freq 500
133
+ """
134
+ r = parse_si(r_str)
135
+ c = parse_si(c_str)
136
+ f0 = integrator_corner(r, c)
137
+ click.echo("Inverting integrator")
138
+ click.echo(f" R = {fmt_si(r, 'Ω')}")
139
+ click.echo(f" C = {fmt_si(c, 'F')}")
140
+ click.echo(f" Corner frequency f₀ = {fmt_si(f0, 'Hz')}")
141
+ click.echo(f" (Unity-gain frequency where |A| = 1)")
142
+ if freq_str:
143
+ freq = parse_si(freq_str)
144
+ gain = integrator_gain_at(r, c, freq)
145
+ click.echo(f"\n At f = {fmt_si(freq, 'Hz')}:")
146
+ click.echo(f" |A| = {gain:.6g} ({20*math.log10(gain):.2f} dB)")
147
+
148
+
149
+ # ── Differentiator ──────────────────────────────────────────────────────────
150
+ @opamp_group.command('differentiator')
151
+ @click.option('--r', 'r_str', required=True, metavar='VALUE',
152
+ help='Feedback resistor, e.g. 10k')
153
+ @click.option('--c', 'c_str', required=True, metavar='VALUE',
154
+ help='Input capacitor, e.g. 100n')
155
+ @click.option('--freq', 'freq_str', default=None, metavar='VALUE',
156
+ help='Optional: evaluate gain magnitude at this frequency, e.g. 1k')
157
+ def cmd_differentiator(r_str, c_str, freq_str):
158
+ """Inverting op-amp differentiator.
159
+
160
+ \b
161
+ Corner frequency: f₀ = 1 / (2π · R · C)
162
+ Gain magnitude: |A(f)| = 2π · f · R · C = f / f₀
163
+
164
+ Example:
165
+ ee-calc opamp differentiator --r 10k --c 100n
166
+ ee-calc opamp differentiator --r 10k --c 100n --freq 2k
167
+ """
168
+ r = parse_si(r_str)
169
+ c = parse_si(c_str)
170
+ f0 = differentiator_corner(r, c)
171
+ click.echo("Inverting differentiator")
172
+ click.echo(f" R = {fmt_si(r, 'Ω')}")
173
+ click.echo(f" C = {fmt_si(c, 'F')}")
174
+ click.echo(f" Corner frequency f₀ = {fmt_si(f0, 'Hz')}")
175
+ click.echo(f" (Unity-gain frequency where |A| = 1)")
176
+ if freq_str:
177
+ freq = parse_si(freq_str)
178
+ gain = differentiator_gain_at(r, c, freq)
179
+ click.echo(f"\n At f = {fmt_si(freq, 'Hz')}:")
180
+ click.echo(f" |A| = {gain:.6g} ({20*math.log10(gain):.2f} dB)")
@@ -0,0 +1,107 @@
1
+ """
2
+ Oscillator resonance frequency calculations.
3
+
4
+ LC oscillator / tank circuit:
5
+ f₀ = 1 / (2π · √(L · C))
6
+
7
+ Colpitts / Hartley (two reactive elements in series → C_total or L_total):
8
+ Colpitts: C_eff = (C1 · C2) / (C1 + C2) → f₀ = 1 / (2π · √(L · C_eff))
9
+ Hartley: L_eff = L1 + L2 → f₀ = 1 / (2π · √(L_eff · C))
10
+ """
11
+ import math
12
+ import click
13
+ from .units import parse_si, fmt_si
14
+ from .filters import lc_resonance
15
+
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Click command group
19
+ # ---------------------------------------------------------------------------
20
+
21
+ @click.group()
22
+ def oscillator_group():
23
+ """Oscillator resonance frequency calculations (LC, Colpitts, Hartley)."""
24
+
25
+
26
+ @oscillator_group.command('lc')
27
+ @click.option('--l', 'l_str', required=True, metavar='VALUE',
28
+ help='Inductance, e.g. 10u, 1m')
29
+ @click.option('--c', 'c_str', required=True, metavar='VALUE',
30
+ help='Capacitance, e.g. 100p, 10n')
31
+ def cmd_lc(l_str, c_str):
32
+ """LC tank oscillator resonance frequency.
33
+
34
+ \b
35
+ f₀ = 1 / (2π · √(L · C))
36
+
37
+ Example:
38
+ ee-calc oscillator lc --l 10u --c 100p
39
+ """
40
+ l = parse_si(l_str)
41
+ c = parse_si(c_str)
42
+ f = lc_resonance(l, c)
43
+ click.echo("LC oscillator")
44
+ click.echo(f" L = {fmt_si(l, 'H')}")
45
+ click.echo(f" C = {fmt_si(c, 'F')}")
46
+ click.echo(f" f₀ = {fmt_si(f, 'Hz')}")
47
+ click.echo(f" ω₀ = {2*math.pi*f:.6g} rad/s")
48
+
49
+
50
+ @oscillator_group.command('colpitts')
51
+ @click.option('--l', 'l_str', required=True, metavar='VALUE',
52
+ help='Inductance, e.g. 10u')
53
+ @click.option('--c1', 'c1_str', required=True, metavar='VALUE',
54
+ help='Capacitor 1, e.g. 100p')
55
+ @click.option('--c2', 'c2_str', required=True, metavar='VALUE',
56
+ help='Capacitor 2, e.g. 100p')
57
+ def cmd_colpitts(l_str, c1_str, c2_str):
58
+ """Colpitts oscillator resonance frequency.
59
+
60
+ \b
61
+ C_eff = (C1 · C2) / (C1 + C2)
62
+ f₀ = 1 / (2π · √(L · C_eff))
63
+
64
+ Example:
65
+ ee-calc oscillator colpitts --l 10u --c1 100p --c2 100p
66
+ """
67
+ l = parse_si(l_str)
68
+ c1 = parse_si(c1_str)
69
+ c2 = parse_si(c2_str)
70
+ c_eff = (c1 * c2) / (c1 + c2)
71
+ f = lc_resonance(l, c_eff)
72
+ click.echo("Colpitts oscillator")
73
+ click.echo(f" L = {fmt_si(l, 'H')}")
74
+ click.echo(f" C1 = {fmt_si(c1, 'F')}")
75
+ click.echo(f" C2 = {fmt_si(c2, 'F')}")
76
+ click.echo(f" C_eff = {fmt_si(c_eff, 'F')} (series combination)")
77
+ click.echo(f" f₀ = {fmt_si(f, 'Hz')}")
78
+
79
+
80
+ @oscillator_group.command('hartley')
81
+ @click.option('--l1', 'l1_str', required=True, metavar='VALUE',
82
+ help='Inductor 1, e.g. 5u')
83
+ @click.option('--l2', 'l2_str', required=True, metavar='VALUE',
84
+ help='Inductor 2, e.g. 5u')
85
+ @click.option('--c', 'c_str', required=True, metavar='VALUE',
86
+ help='Capacitance, e.g. 100p')
87
+ def cmd_hartley(l1_str, l2_str, c_str):
88
+ """Hartley oscillator resonance frequency.
89
+
90
+ \b
91
+ L_eff = L1 + L2
92
+ f₀ = 1 / (2π · √(L_eff · C))
93
+
94
+ Example:
95
+ ee-calc oscillator hartley --l1 5u --l2 5u --c 100p
96
+ """
97
+ l1 = parse_si(l1_str)
98
+ l2 = parse_si(l2_str)
99
+ c = parse_si(c_str)
100
+ l_eff = l1 + l2
101
+ f = lc_resonance(l_eff, c)
102
+ click.echo("Hartley oscillator")
103
+ click.echo(f" L1 = {fmt_si(l1, 'H')}")
104
+ click.echo(f" L2 = {fmt_si(l2, 'H')}")
105
+ click.echo(f" L_eff = {fmt_si(l_eff, 'H')} (series combination)")
106
+ click.echo(f" C = {fmt_si(c, 'F')}")
107
+ click.echo(f" f₀ = {fmt_si(f, 'Hz')}")
ee_toolkit/resistor.py ADDED
@@ -0,0 +1,258 @@
1
+ """
2
+ Resistor series / parallel combination calculator.
3
+
4
+ Series: R_total = R1 + R2 + … + Rn
5
+ Parallel: 1/R_total = 1/R1 + 1/R2 + … + 1/Rn
6
+
7
+ The CLI command accepts one or more --sg (series group) options.
8
+ Each --sg VALUE VALUE … is treated as resistors wired in parallel.
9
+ Multiple --sg groups are then summed in series.
10
+
11
+ Example
12
+ -------
13
+ Two 10 kΩ in parallel, in series with a 4.7 kΩ:
14
+
15
+ ee-calc resistor combine --sg 10k 10k --sg 4.7k
16
+ """
17
+ from __future__ import annotations
18
+ from typing import List, Tuple
19
+ from .eseries import all_values
20
+
21
+
22
+ def parallel(*resistances: float) -> float:
23
+ """Return the equivalent resistance of *resistances* wired in parallel.
24
+
25
+ Raises ValueError if any resistance is zero (short circuit).
26
+ """
27
+ if any(r == 0.0 for r in resistances):
28
+ raise ValueError("A resistor value of 0 Ω creates a short circuit.")
29
+ return 1.0 / sum(1.0 / r for r in resistances)
30
+
31
+
32
+ def series(*resistances: float) -> float:
33
+ """Return the equivalent resistance of *resistances* wired in series."""
34
+ return sum(resistances)
35
+
36
+
37
+ def series_parallel_groups(groups: List[List[float]]) -> float:
38
+ """
39
+ Compute the total resistance of groups wired in series, where each
40
+ group is a list of resistors wired in parallel.
41
+
42
+ Parameters
43
+ ----------
44
+ groups : list of lists
45
+ Each inner list is a parallel group. Groups are combined in series.
46
+
47
+ Returns
48
+ -------
49
+ float
50
+ Total equivalent resistance in Ω.
51
+ """
52
+ group_values = []
53
+ for g in groups:
54
+ if len(g) == 1:
55
+ group_values.append(g[0])
56
+ else:
57
+ group_values.append(parallel(*g))
58
+ return series(*group_values)
59
+
60
+
61
+ def voltage_divider(
62
+ v_in: float,
63
+ v_out: float,
64
+ r_total_target: float,
65
+ series: str,
66
+ n_results: int = 5,
67
+ ) -> List[Tuple[float, float, float, float, float, float]]:
68
+ """
69
+ Find best voltage divider resistor pair (r1, r2) for a given E-series.
70
+ Returns: list of (r1, r2, actual_vout, err_vout_percent, actual_rtot, err_rtot_percent)
71
+ """
72
+ base_vals = all_values(series)
73
+ ratio = v_out / v_in
74
+ if ratio >= 1.0 or ratio <= 0.0:
75
+ raise ValueError("V_out must be strictly between 0 and V_in.")
76
+
77
+ multiplier = ratio / (1.0 - ratio)
78
+ r1_opt = r_total_target / (1.0 + multiplier)
79
+
80
+ # Restrict search space
81
+ lo, hi = r1_opt * 0.01, r1_opt * 100.0
82
+ r1_cands = [v for v in base_vals if lo <= v <= hi]
83
+
84
+ best_for_ratio = {}
85
+
86
+ for r1 in r1_cands:
87
+ r2_ideal = r1 * multiplier
88
+ # Find nearest 5 standard values for R2 for this R1
89
+ r2_sorted = sorted(base_vals, key=lambda v: abs(v - r2_ideal))
90
+ for r2 in r2_sorted[:5]:
91
+ act_v = v_in * r2 / (r1 + r2)
92
+ # bin identical ratios together
93
+ err_v_abs = round(abs(act_v - v_out) / v_out * 100, 8)
94
+
95
+ act_r = r1 + r2
96
+ err_r = abs(act_r - r_total_target) / r_total_target * 100.0
97
+
98
+ item = (
99
+ r1, r2, act_v,
100
+ (act_v - v_out) / v_out * 100.0,
101
+ act_r,
102
+ (act_r - r_total_target) / r_total_target * 100.0
103
+ )
104
+
105
+ # keep the decade-shifted pair that is closest to r_total_target
106
+ if err_v_abs not in best_for_ratio or err_r < abs(best_for_ratio[err_v_abs][5]):
107
+ best_for_ratio[err_v_abs] = item
108
+
109
+ results = list(best_for_ratio.values())
110
+ results.sort(key=lambda x: (abs(x[3]), abs(x[5])))
111
+ return results[:n_results]
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Click commands
116
+ # ---------------------------------------------------------------------------
117
+ import re
118
+ import click
119
+ from .units import parse_si, fmt_si
120
+
121
+
122
+ class _SeriesGroupParam(click.ParamType):
123
+ """Custom Click parameter type that collects --sg arguments.
124
+
125
+ Accepts a string of whitespace- or comma-separated SI resistor values
126
+ (e.g. "10k 10k" or "10k,10k"). The option is declared with
127
+ multiple=True so it may appear several times on the command line.
128
+ """
129
+ name = "VALUES"
130
+
131
+ def convert(self, value, param, ctx):
132
+ tokens = re.split(r'[\s,]+', value.strip())
133
+ tokens = [t for t in tokens if t]
134
+ if not tokens:
135
+ self.fail("at least one resistor value is required.", param, ctx)
136
+ try:
137
+ return [parse_si(t) for t in tokens]
138
+ except ValueError as exc:
139
+ self.fail(str(exc), param, ctx)
140
+
141
+
142
+ _SG = _SeriesGroupParam()
143
+
144
+
145
+ @click.group()
146
+ def resistor_group():
147
+ """Resistor combination calculations (series / parallel)."""
148
+
149
+
150
+ @resistor_group.command('combine')
151
+ @click.option(
152
+ '--sg', 'groups',
153
+ type=_SG,
154
+ multiple=True,
155
+ metavar='VALUES',
156
+ help=(
157
+ 'Parallel group: a quoted, space- or comma-separated list of resistor '
158
+ 'values (e.g. --sg "10k 10k"). Repeat --sg to wire groups in series.'
159
+ ),
160
+ )
161
+ def cmd_combine(groups):
162
+ """Series / parallel resistor combination.
163
+
164
+ \b
165
+ Each --sg group is treated as resistors wired IN PARALLEL.
166
+ Multiple --sg groups are combined IN SERIES with each other.
167
+
168
+ \b
169
+ Formulas:
170
+ Parallel: 1/R = 1/R1 + 1/R2 + …
171
+ Series: R = R1 + R2 + …
172
+
173
+ \b
174
+ Examples:
175
+ # Two 10 kΩ in parallel → 5 kΩ
176
+ ee-calc resistor combine --sg "10k 10k"
177
+
178
+ # Two 10 kΩ in parallel, then in series with 4.7 kΩ
179
+ ee-calc resistor combine --sg "10k 10k" --sg 4.7k
180
+
181
+ # Three groups in series, each with mixed values
182
+ ee-calc resistor combine --sg "1k 2k" --sg 470 --sg "3.3k 3.3k"
183
+ """
184
+ if not groups:
185
+ raise click.UsageError("At least one --sg group is required.")
186
+
187
+ # Compute and display
188
+ click.echo("Resistor combination")
189
+ click.echo("")
190
+
191
+ group_results: List[float] = []
192
+ for i, values in enumerate(groups, start=1):
193
+ if len(values) == 1:
194
+ r_eq = values[0]
195
+ click.echo(f" Group {i} (series): {fmt_si(r_eq, 'Ω')}")
196
+ else:
197
+ try:
198
+ r_eq = parallel(*values)
199
+ except ValueError as exc:
200
+ raise click.UsageError(str(exc)) from exc
201
+ parts = " ‖ ".join(fmt_si(v, 'Ω') for v in values)
202
+ click.echo(f" Group {i} (parallel): {parts} → {fmt_si(r_eq, 'Ω')}")
203
+ group_results.append(r_eq)
204
+
205
+ total = series(*group_results)
206
+ click.echo("")
207
+ if len(group_results) > 1:
208
+ series_str = " + ".join(fmt_si(r, 'Ω') for r in group_results)
209
+ click.echo(f" Total (series sum): {series_str}")
210
+ click.echo(f" R_total = {fmt_si(total, 'Ω')}")
211
+
212
+
213
+ _SERIES_CHOICE = click.Choice(['E12', 'E24', 'E48', 'E96', 'E192'], case_sensitive=False)
214
+
215
+ @resistor_group.command('divider')
216
+ @click.option('--vin', 'vin_str', required=True, metavar='VALUE', help='Input voltage.')
217
+ @click.option('--vout', 'vout_str', required=True, metavar='VALUE', help='Target output voltage.')
218
+ @click.option('--ztot', 'ztot_str', required=True, metavar='VALUE', help='Target total impedance (R1 + R2), e.g. 10k.')
219
+ @click.option('--series', default='E24', show_default=True, type=_SERIES_CHOICE, help='E-series to use.')
220
+ @click.option('-n', 'n', default=5, show_default=True, help='Number of combinations to show.')
221
+ def cmd_divider(vin_str, vout_str, ztot_str, series, n):
222
+ """Calculate voltage divider resistors.
223
+
224
+ \b
225
+ Finds the best R1 (top) and R2 (bottom) in the specified E-series
226
+ that provide the target output voltage, while keeping the sum
227
+ R1 + R2 close to the target total impedance.
228
+
229
+ \b
230
+ Formula:
231
+ V_out = V_in * (R2 / (R1 + R2))
232
+
233
+ \b
234
+ Example:
235
+ ee-calc resistor divider --vin 5 --vout 3.3 --ztot 10k --series E48
236
+ """
237
+ try:
238
+ v_in = parse_si(vin_str)
239
+ v_out = parse_si(vout_str)
240
+ r_tot = parse_si(ztot_str)
241
+ results = voltage_divider(v_in, v_out, r_tot, series, n)
242
+ except ValueError as exc:
243
+ raise click.UsageError(str(exc)) from exc
244
+
245
+ click.echo(f"Voltage Divider (series {series.upper()})")
246
+ click.echo(f" V_in: {fmt_si(v_in, 'V')}")
247
+ click.echo(f" V_out: {fmt_si(v_out, 'V')} (target)")
248
+ click.echo(f" Z_tot: {fmt_si(r_tot, 'Ω')} (target)")
249
+ click.echo("")
250
+ click.echo(f"{'Rank':<5} {'R1 (top)':<12} {'R2 (bot)':<12} {'V_out':<10} {'Error V':>8} {'Z_total':<10} {'Error Z':>8}")
251
+ click.echo("─" * 76)
252
+ for i, (r1, r2, act_v, err_v, act_rtot, err_rtot) in enumerate(results, 1):
253
+ marker = " ◀" if i == 1 else ""
254
+ click.echo(
255
+ f" {i:<3} {fmt_si(r1, 'Ω'):<12} {fmt_si(r2, 'Ω'):<12} "
256
+ f"{fmt_si(act_v, 'V'):<10} {err_v:>+7.2f}% "
257
+ f"{fmt_si(act_rtot, 'Ω'):<10} {err_rtot:>+7.1f}%{marker}"
258
+ )