In my dayjob, I'm an electrical engineer. Though, I'm mostly involved in writing firmware that runs on baremetal systems. If you're designing electrical circuits for a living or just tinkered with them, you will have heard of E-Series preferred values. In particular, resistors, capacitors and inductors are generally available in ratings derived from these numbers. The basic idea is to fit a number of values into one order of magnitude. For examples in E3, there are three values in the same decimal power: 100, 220 and 470. The next value would be 1000 in the next decimal power. Likewise in E12 there are twelve such values.

Roughly, E-Series follow an exponential definition. The series generally don't follow the exact mathematical expression. For an actual implementation you either adjust to the specified values or you simply use the actual tables from the spec.

Now looking up values in tables is a boring task; especially if the tables are relatively small. Finding combinations, though, is more interesting. For example, if you'd like a resistor rated at 50Ω (which is a rather important value in radio frequency design), you'll notice that the exact value is not available in any of the E-Series. But it's easy to combine two 100Ω resistors in parallel to produce a value of 50Ω. …sure, in an actual design you might use 49.9Ω from E96 or E192, or use a specially made resistor of the desired rating. But you get the idea. Combining components in parallel and series circuits allows the parts from E-Series to cover lots of ground.

I've written a library that implements E-Series in
Scheme. Its main modules are **(e-series
adjacency)**, which allows looking up values from an E-Series that are in the
vicinity of a given value. Then there's **(e-series combine)** which produces
combinations of values from a certain E-Series to approximate a given value.
And finally there's the top-level **(e-series)** module, that implements
frontends to the other mentioned modules, to make it possible to easily use the
library at a Scheme REPL.

To see if the library finds a combination from E12 that matches 50Ω:

```
scheme@(guile-user)> (resistor 12 50)
------------+-------------------------+-------------+-------------+--------------
Desired | Actual (Error) | Part A | Part B | Circuit
------------+-------------------------+-------------+-------------+--------------
50.0000Ω | 50.0000Ω ( exact ) | 100.000Ω | 100.000Ω | parallel
50.0000Ω | 50.0380Ω (+7.605E-4) | 56.0000Ω | 470.000Ω | parallel
50.0000Ω | 49.7000Ω (-6.000E-3) | 47.0000Ω | 2.70000Ω | series
50.0000Ω | 50.3000Ω (+6.000E-3) | 47.0000Ω | 3.30000Ω | series
------------+-------------------------+-------------+-------------+--------------
```

Those aren't *all* the combinations that are possible. By default the module
produces tables, that contain combinations that match the desired value at
least as well as 1%. Now, to see values in the vicinity of 50Ω all E-Series,
you can do this:

```
scheme@(guile-user)> (resistor 50)
---------+--------------------------+-------------+--------------------------
Series | Below (Error) | Exact | Above (Error)
---------+--------------------------+-------------+--------------------------
E3 | 47.0000Ω (-6.000E-2) | | 100.000Ω (+1.000E+0)
E6 | 47.0000Ω (-6.000E-2) | | 68.0000Ω (+3.600E-1)
E12 | 47.0000Ω (-6.000E-2) | | 56.0000Ω (+1.200E-1)
E24 | 47.0000Ω (-6.000E-2) | | 51.0000Ω (+2.000E-2)
E48 | 48.7000Ω (-2.600E-2) | | 51.1000Ω (+2.200E-2)
E96 | 49.9000Ω (-2.000E-3) | | 51.1000Ω (+2.200E-2)
E192 | 49.9000Ω (-2.000E-3) | | 50.5000Ω (+1.000E-2)
---------+--------------------------+-------------+--------------------------
```

And here you see that E96 and E192 have pretty close matches as single components.

With combinations, the library allows the for user to specify arbitrarily complex predicates to choose from the generated combinations. For example, to only return parallel circuits that approximate 50Ω from E12:

```
scheme@(guile-user)> (resistor 12 50 #:predicate (circuit 'parallel))
...
```

And to limit those results to those that have an eror of 0.001 or better, here's a way:

```
scheme@(guile-user)> (resistor 12 50 #:predicate (all-of (max-error 1e-3)
(circuit 'parallel)))
------------+-------------------------+-------------+-------------+--------------
Desired | Actual (Error) | Part A | Part B | Circuit
------------+-------------------------+-------------+-------------+--------------
50.0000Ω | 50.0000Ω ( exact ) | 100.000Ω | 100.000Ω | parallel
50.0000Ω | 50.0380Ω (+7.605E-4) | 56.0000Ω | 470.000Ω | parallel
------------+-------------------------+-------------+-------------+--------------
```

There are frontends for inductors and capacitors as well, so you don't have to mentally strain yourself too much about with combination corresponds to which circuit. To find a 9.54μF capacitor approximation from E24 with an error better than 0.002:

```
scheme@(guile-user)> (capacitor 24 #e9.54e-6 #:predicate (max-error #e2e-3))
------------+-------------------------+-------------+-------------+--------------
Desired | Actual (Error) | Part A | Part B | Circuit
------------+-------------------------+-------------+-------------+--------------
9.54000µF | 9.53000µF (-1.048E-3) | 9.10000µF | 430.000nF | parallel
9.54000µF | 9.55102µF (+1.155E-3) | 13.0000µF | 36.0000µF | series
9.54000µF | 9.52381µF (-1.697E-3) | 10.0000µF | 200.000µF | series
------------+-------------------------+-------------+-------------+--------------
```

The test-suite and documentation coverage could be better, but the front-end module is easy enough to use, I think.