The presented approach of automatic circuit generation is based on grammatical evolution [
22], which is an evolutionary computation technique. Basically, evolutionary computation is a family of algorithms for global optimization inspired by biological evolution. A typical evolutionary computation algorithm generates an initial set of candidate solutions and updates them iteratively. Each new generation is produced by applying selection and mutation operators. As a result, the overall fitness (specified by a special fitness function) of the population will gradually increase until an acceptable solution is obtained.
Unlike many conventional EA algorithms, grammatical evolution uses populations of variable-length binary string genomes (also known as chromosomes, where each codon is a consecutive group of eight bits, representing an integer value) to select production rules in a Backus–Naur form (BNF) grammar definition. Thus, GE does not perform the evolution process on the actual circuits but rather on binary stings, which increases the flexibility of the algorithm. By using appropriate BNF grammar, one can easily integrate the expert/domain knowledge in order to speed up the process (such as limiting the ZARC values as shown in [
4,
5,
23]).
The GE approach was implemented in the Python programming environment with the addition of the PyOpus package to access the SPICE circuit simulator [
24] and is described in-depth in articles [
21,
25]. Basically, one step of the GE algorithm produces a generation of binary strings, each of which is then used to select production rules in a BNF grammar to build an EEC. Thus, each of the obtained EECs is then evaluated in the SPICE simulator for its fitness.
2.1. The Grammar
The Backus–Naur form is a notation for specifying a language grammar in the form of so-called
production rules [
26]. Production rules include
terminals, which are elements that appear in the language (e.g., 0, 1, +, input, etc.), and
nonterminals, which must be expanded in one or more terminals and/or nonterminals. BNF grammar is often expressed by the tuple
, where
N and
T stand for the sets of terminals and nonterminals, respectively,
P is a set of production rules mapping nonterminals to terminals, and
S is a start symbol, which must be a nonterminal. When there is more than a single production that can be applied to a certain nonterminal, the different production rules are delimited by a vertical bar.
In order to be able to evaluate an EEC produced in the GE process, one needs to build a SPICE netlist from a binary string, which is then evaluated in the SPICE simulator. Standard components (i.e., resistors and capacitors) as well as the EIS specific ZARC element are employed to build EECs. Each component has at least two parameters—the numbers of the connecting ports and the element value(s) (i.e., resistances, capaticances, etc.). This is the BNF used to produce a SPICE netlist:
and
P is summarized in
Table 1.
The first row in the table contains a production rule for the <netlist> nonterminal. The rule ensures that a netlist is composed of up to twelve circuit elements (or parts), separated by spaces. This was performed to prevent bloating (i.e., the uncontrolled growth of the size of a circuit due to unnecessary parallel/serial elements). The second row shows that each of the twelve elements (parts) can become either one of the three actual elements (i.e., resistor, capacitor, or ZARC) or a nonexistent element (None). It can be observed from the next three rows how the three elements are specified. They contain two connecting ports (determined by the <gpair> rule) and the value(s) appropriate for a specific element. A ZARC element contains specialized numeric values (zexp and znum). These are obtained according to expert knowledge (as described in [
4,
5,
23]) in order to narrow down the solution search space. For example, as we are especially interested in the capacitive characteristic of electrochemical processes, we only allow ZARC elements with
n between
and
.
Table 2 shows the value ranges of the elements that can be produced using the rules from
Table 1.
The last row of
Table 1 contains another hard-wired optimization. Instead of specifying each connection port separately, the <gpair> production rule was created which holds all the feasible port combinations (thus, producing some so-called composite terminals made up from terminals from (
2)). In that way, two benefits were achieved—a higher chance of creating a working circuit (i.e., where one actually receives a signal from input to output) and a significantly lower chance of creating an illegal circuit with closed loops.
Note that there is still a certain (small) probability that the algorithm produces an invalid circuit when using the above rules—in some conditions, the process of mapping the codons to the actual circuit can result in an element that is connected to itself. Such a circuit can create serious problems during the simulation (i.e., the simulator could either crash or become stuck in an endless loop). In order to avoid such situations, the system was augmented with a special subroutine that detects such circuits and simply eliminates them from the evolutionary process even before they become evaluated for their fitness. Some may argue that this is not the best possible strategy since it may result in a loss of important genetic material at the beginning of the run. That being said, this was still the most efficient solution. Devising a set of production rules that would prevent this problem is simply not feasible.
2.3. Genetic Operations and Circuits
Evolutionary computation features several operators used to manipulate and combine individual solutions. For the purposes of working with EEC, these operators have to be adjusted accordingly, most noticeably mutation and crossover.
2.3.1. Circuit Mutation
Mutation results in a change in the chromosome of the individual, which is reflected in the final interpretation of the chromosome. The change can be minor or major depending on which codon is selected to mutate. A minor change occurs when, for example, a chromosome from
Section 2.2.1 changes from {12, 127, 209, 21, 76} to {12, 127, 209,
70, 76}. The effect of this change is that the resistor changes its value from two kiloohms to one kiloohm, as shown in
Figure 3.
A more significant mutation, on the other hand, happens if the chromosome changes to {
117, 127, 209, 21, 76}. That changes a resistor to a capacitor, as shown in
Figure 4, which can of course result in a completely different function of the resulting circuit (which is not necessarily a bad thing).
An even more drastic mutation occurs if the same element changes to {119, 127, 209, 21, 76}. This string now represents two empty elements (119 and 127 both map to rule number 3, None), and the next codon (209) is used to determine the next element in the circuit, which will be a capacitor connected to ports 1 and 3 (mapped from <gpair> using the codon with the value of 21).
2.3.2. Circuit Crossover
While mutation targets a single circuit, crossover takes two different circuits (parents) and swaps their elements in order to create new circuits (children) that might be better suited to solving the given problem.
The system first selects two appropriate circuits (usually the circuits that are amongst the better performing ones). In the example (shown in the top row of
Figure 5), there are two circuits with identical topology but different component values. In the first step, a random element from Parent 1 is selected—the 13
resistor drawn in solid black. The algorithm then searches for a similar element in the second circuit (Parent 2) and finds the 400
resistor. Since two elements of the same type could be found, the crossover can proceed and the two elements are swapped. The chromosomes are adjusted accordingly so as to reflect the change. The resulting circuits (show in the bottom row of
Figure 5) are then used as potential candidates in the next generation and (hopefully) perform better than their parents.
Note that we used a slightly modified version of crossover, which focuses on the phenotype instead of considering all available codons. Usually, crossover selects two completely random codons (or substrings of codons) and swaps them. This proved to be quite destructive for the circuits as it usually resulted in the circuit becoming nonfunctional either due to disconnected elements, impossible values, short circuits, or other similar drastic changes. Therefore, we limited the crossover to elements and values in order to preserve circuit integrity and increase the chance of converging towards a working solution.