ee-toolkit 0.0.2__tar.gz → 0.1.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ee-toolkit
3
- Version: 0.0.2
3
+ Version: 0.1.0
4
4
  Project-URL: Documentation, https://git.sr.ht/~laplace/ee-toolkit#readme
5
5
  Project-URL: Issues, https://git.sr.ht/~laplace/ee-toolkit/todo
6
6
  Project-URL: Source, https://git.sr.ht/~laplace/ee-toolkit
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2026-present Alexander Becker <nabla.becker@mailbox.org>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.0.2"
4
+ __version__ = "0.1.0"
@@ -6,12 +6,15 @@ Commands:
6
6
  eseries E-series standard value lookup
7
7
  oscillator LC/Colpitts/Hartley resonance frequencies
8
8
  opamp Op-amp gain, integrator, differentiator
9
+ resistor Series / parallel resistor combinations
9
10
  """
10
11
  import click
11
12
  from .filters import filter_group
12
13
  from .eseries import eseries_group
13
14
  from .oscillator import oscillator_group
14
15
  from .opamp import opamp_group
16
+ from .mosfet import mosfet_group
17
+ from .resistor import resistor_group
15
18
 
16
19
 
17
20
  @click.group()
@@ -25,6 +28,8 @@ def cli():
25
28
  eseries Nearest E-series value / two-component decomposition
26
29
  oscillator LC, Colpitts, Hartley resonance frequency
27
30
  opamp Inverting / non-inverting gain, integrator, differentiator
31
+ mosfet Power MOSFET calculations (gate drive, switching)
32
+ resistor Series / parallel resistor combinations
28
33
 
29
34
  Run ee-calc COMMAND --help for details on a command.
30
35
  """
@@ -34,3 +39,5 @@ cli.add_command(filter_group, name='filter')
34
39
  cli.add_command(eseries_group, name='eseries')
35
40
  cli.add_command(oscillator_group, name='oscillator')
36
41
  cli.add_command(opamp_group, name='opamp')
42
+ cli.add_command(mosfet_group, name='mosfet')
43
+ cli.add_command(resistor_group, name='resistor')
@@ -0,0 +1,95 @@
1
+ """
2
+ Power MOSFET calculations.
3
+
4
+ Gate Drive Requirements
5
+ -----------------------
6
+ Required Drive Current: Ig = Qg / t_sw
7
+ Switching Time: t_sw = Qg / Ig
8
+ Gate Drive Power: Pgate = Qg * Vdr * f_sw
9
+ Peak Current: Ig_peak = Vdr / Rg_total
10
+ """
11
+ import click
12
+ from .units import parse_si, fmt_si
13
+
14
+
15
+ def ig_from_tsw(qg: float, tsw: float) -> float:
16
+ """Average gate current required for a specific switching time."""
17
+ return qg / tsw
18
+
19
+
20
+ def tsw_from_ig(qg: float, ig: float) -> float:
21
+ """Estimated switching time for a given average gate current."""
22
+ return qg / ig
23
+
24
+
25
+ def pgate_power(qg: float, vdr: float, fsw: float) -> float:
26
+ """Power dissipated in the gate driver."""
27
+ return qg * vdr * fsw
28
+
29
+
30
+ def ig_peak_from_rg(vdr: float, rg: float) -> float:
31
+ """Peak gate drive current limited by total gate resistance."""
32
+ return vdr / rg
33
+
34
+
35
+ @click.group()
36
+ def mosfet_group():
37
+ """Power MOSFET calculations (gate drive, switching)."""
38
+
39
+
40
+ @mosfet_group.command('gate')
41
+ @click.option('--qg', 'qg_str', required=True, metavar='VALUE',
42
+ help='Total gate charge (Qg), e.g. 50n')
43
+ @click.option('--vdr', 'vdr_str', required=True, metavar='VALUE',
44
+ help='Gate drive voltage (Vdr), e.g. 12')
45
+ @click.option('--tsw', 'tsw_str', default=None, metavar='VALUE',
46
+ help='Desired switching time (t_sw), e.g. 20n')
47
+ @click.option('--ig', 'ig_str', default=None, metavar='VALUE',
48
+ help='Available gate drive current (Ig), e.g. 1.5')
49
+ @click.option('--rg', 'rg_str', default=None, metavar='VALUE',
50
+ help='Total gate resistance (driver + ext + int), e.g. 10')
51
+ @click.option('--fsw', 'fsw_str', default=None, metavar='VALUE',
52
+ help='Switching frequency, e.g. 100k')
53
+ def cmd_gate(qg_str, vdr_str, tsw_str, ig_str, rg_str, fsw_str):
54
+ """Gate driver requirements for power MOSFETs.
55
+
56
+ \b
57
+ Formulas:
58
+ Ig_req = Qg / t_sw
59
+ t_sw = Qg / Ig
60
+ P_gate = Qg * Vdr * f_sw
61
+ Ig_peak = Vdr / Rg_total
62
+
63
+ Example:
64
+ ee-calc mosfet gate --qg 50n --vdr 12 --tsw 20n --fsw 100k
65
+ """
66
+ qg = parse_si(qg_str)
67
+ vdr = parse_si(vdr_str)
68
+
69
+ click.echo("Power MOSFET Gate Drive")
70
+ click.echo(f" Qg = {fmt_si(qg, 'C')}")
71
+ click.echo(f" Vdr = {fmt_si(vdr, 'V')}")
72
+
73
+ if tsw_str:
74
+ tsw = parse_si(tsw_str)
75
+ ig_req = ig_from_tsw(qg, tsw)
76
+ click.echo(f"\nTarget Switching Time: t_sw = {fmt_si(tsw, 's')}")
77
+ click.echo(f" Required average gate current (Ig) = {fmt_si(ig_req, 'A')}")
78
+
79
+ if ig_str:
80
+ ig = parse_si(ig_str)
81
+ tsw_est = tsw_from_ig(qg, ig)
82
+ click.echo(f"\nAvailable Gate Current: Ig = {fmt_si(ig, 'A')}")
83
+ click.echo(f" Estimated switching time (t_sw) = {fmt_si(tsw_est, 's')}")
84
+
85
+ if rg_str:
86
+ rg = parse_si(rg_str)
87
+ ig_peak = ig_peak_from_rg(vdr, rg)
88
+ click.echo(f"\nTotal Gate Resistance: Rg = {fmt_si(rg, 'Ω')}")
89
+ click.echo(f" Peak driver current (Ipk) = {fmt_si(ig_peak, 'A')}")
90
+
91
+ if fsw_str:
92
+ fsw = parse_si(fsw_str)
93
+ pd = pgate_power(qg, vdr, fsw)
94
+ click.echo(f"\nSwitching Frequency: f_sw = {fmt_si(fsw, 'Hz')}")
95
+ click.echo(f" Gate driver power (Pd) = {fmt_si(pd, 'W')}")
@@ -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
+ )
File without changes
File without changes
File without changes