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/__about__.py +4 -0
- ee_toolkit/__init__.py +3 -0
- ee_toolkit/cli.py +46 -0
- ee_toolkit/eseries.py +240 -0
- ee_toolkit/filters.py +134 -0
- ee_toolkit/mosfet.py +95 -0
- ee_toolkit/opamp.py +180 -0
- ee_toolkit/oscillator.py +107 -0
- ee_toolkit/resistor.py +258 -0
- ee_toolkit/smps.py +330 -0
- ee_toolkit/units.py +77 -0
- ee_toolkit-0.0.2.dist-info/METADATA +111 -0
- ee_toolkit-0.0.2.dist-info/RECORD +16 -0
- ee_toolkit-0.0.2.dist-info/WHEEL +4 -0
- ee_toolkit-0.0.2.dist-info/entry_points.txt +2 -0
- ee_toolkit-0.0.2.dist-info/licenses/LICENSE.txt +673 -0
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)")
|
ee_toolkit/oscillator.py
ADDED
|
@@ -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
|
+
)
|