Brute-force search, as kurthr suggested doing, takes under 250ms to find a solution that's better on both axes by an order of magnitude, without Scipy, though it still took me 15 minutes to set it up.<p>With R1 = 100Ω, R2 = 330kΩ, C = 2200pF, the frequency error is 7 Hz rather than 39 Hz (0.7% instead of 4%) and the duty-cycle error is 0.008% instead of 0.03%. Probably your capacitor is not going to be stable to 0.7% or precise to 1%, but you can certainly do better than 4%. (Use an NP0/C0G capacitor, not an X7R or something. In larger capacitance values you'd use a film cap instead, but 2200pF is fine for NP0/C0G.)<p>You probably ought to measure some capacitors before running the optimization if you're only building one circuit, and if you're really interested in precision you might put a couple of trimpots across the resistors and adjust it while watching the scope (modern digital scopes can continuously display the frequency and duty cycle, so this is quick). But that will only help if most of the resistance and capacitance comes from components that won't drift over time or vary too much with temperature.<p>Multiplying the loss values instead of adding them avoids having to choose weights for them. If you were going to use Newton's method or gradient descent, you might want to square them instead of taking the absolute value in order to get faster convergence, but of course that takes you back to solving a continuous relaxation of the discrete component selection problem you actually have. For branch-and-bound search a continuous relaxation can still be a useful thing to do, though.<p><pre><code> >>> R1 = R2 = 1000
>>> import math
>>> u, n, p = 1e-6, 1e-9, 1e-12
>>> caps = [a * b * c for a in [1.0, 2.2, 4.7] for b in [1, 10, 100] for c in [u, n, p]]
>>> e24 = [1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0, 3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1]
>>> C = 47 * u
>>> 1/(math.log(2) * (R1 + 2*R2) * C) # f
10.231879722616762
>>> (R1 + R2) / (R1 + 2*R2) # duty cycle
0.6666666666666666
>>> rs = [a * 10**b for a in e24 for b in range(2, 6)]
>>> import time
>>> s = time.time(); soln = min(((abs(1000 - f)/f) * (abs(0.5 - D)/D), R1, R2, C, f, D) for R1, R2, C, f, D in ((R1, R2, C, 1/(math.log(2) * (R1 + 2*R2) * C), (R1 + R2) / (R1 + 2*R2)) for R1 in rs for R2 in rs for C in caps)); time.time() - s
0.2474360466003418
>>> soln
(1.0000300746386372e-06, 100.0, 330000.0, 2.2000000000000003e-09, 993.4411045771049, 0.5000757460990759)
</code></pre>
It would be fair to argue that the nested generator expression there is pretty hard to read, but it didn't really take that long to type, the Python REPL is pretty shitty at editing multiline functions, and I didn't feel like firing up Jupyter. But this way of writing it is definitely less unreadable and works just as well:<p><pre><code> >>> def search(rs1, rs2, cs):
... for R1 in rs1:
... for R2 in rs2:
... for C in cs:
... f = 1/(math.log(2) * (R1 + 2*R2) * C)
... D = (R1 + R2) / (R1 + 2*R2)
... yield (abs(1000 - f)/f) * (abs(0.5 - D)/D), R1, R2, C, f, D</code></pre>