Debugging a Logic Circuit in IDP-Z3
last updated 2025-05-03 16:12:06 by Simon Vandevelde
In my previous blog post, I showed how we can solve a puzzle in the IDP-Z3 reasoning engine. It was inspired by a post by Hillel Wayne, who modelled the puzzle in Prolog. He starts the article by stating how, for his book, he prefers showing off logic programming through practical examples instead of small puzzles, which I quite agree with. Here's the good news: in this post, I'll show you how the puzzle was actually a practical problem in disguise, and how it can be generalised to, e.g., the debugging of logic circuits1. :-)
Abstracting the puzzle
A brief re-cap of the puzzle: we are given test answers and corresponding results for three students, and have to use this information to deduce the score of a fourth student. Crucially, we are not given the correct answers -- this is the puzzle, of course! We can abstract it as follows:
Given a description of a system's internal workings, together with a series of input-output observations, deduce information on an unknown aspect of the system.It is easy to see how this applies to the Layton puzzle:
- System: a test containing of 10 A-B questions, each worth 10 points if correct.
- Observations: students' answers and their scores.
- Goal: deduce the correct answers, and calculate the fourth student's score.
This abstract version can be applied to many more problem domains. For instance, it could be used for machine diagnosis: consider a broken machine for which we can describe the expected inner workings, and for which we have a series of observations. Or, as we'll show in the next section, we can also apply it to the debugging of logic circuits.
Logic Circuit
Imagine the following logic circuit, consisting of five gates (A-E):

Somehow, it does not work as its supposed to, and our goal is to help diagnose the issue. Due to its design, however, it's not possible to read the pin-outs of the internal wires. Instead, we are given a set of three tests, consisting of the values of the input wires {K, L, M} and corresponding output values for output wires {P, R}. Does this look familiar, yet? :-)
Using the observations shown below, we should try to find a diagnosis for our system. As there will obviously be many possibilities, let's assume Occam's razor and try to look for the diagnosis in which the least amount of gates would need to be broken.
Test | K | L | M | P | R |
---|---|---|---|---|---|
1 | 1 | 0 | 0 | 1 | 1 |
2 | 0 | 1 | 0 | 1 | 1 |
3 | 0 | 0 | 1 | 1 | 0 |
Solving using IDP-Z3
We'll now show a solution in the IDP-Z3 reasoning engine. Like in the Layton puzzle, we'll start by creating a vocabulary block to declare the required symbols. Keep in mind that, in a normal setting, you'd build up such a vocabulary piece by piece, together with the structure and theory blocks. Unfortunately, though explaining this design proces is didactically interesting, it will make this post at least five times longer. If you'd like a short primer on IDP-Z3's FO(ยท) language, see the previous post or my Sudoku solver in IDP-Z3.
Vocabulary
To start our vocabulary, we need a way to represent the gates and the wires. In this case, we can straightforwardly use types and unary predicates as follows:
type Gate := {A, B, C, D, E} // Following three predicates are used to denote the Gate's type xor: Gate -> Bool and: Gate -> Bool or: Gate -> Bool type Wire := {K, L, M, N, O, P, Q, R} // Denote the in-/output wires of our circuit input_wire: Wire -> Bool output_wire: Wire -> BoolWe also introduce functions as a way to refer to each gate's in- and output wires.
output: Gate -> Wire first_input: Gate -> Wire second_input: Gate -> WireNext, we need a way to express the information related to the observations. For this purpose, we introduce a type
Test
together with some binary predicates to denote the value of wires during each test.
type Test := {1..3} // Value of each wire during each test. on: Wire * Test -> Bool // Value of the observed input and output wires for each test. input_on: Wire * Test -> Bool input_off: Wire * Test -> Bool observed_on: Wire * Test -> Bool observed_off: Wire * Test -> BoolLastly, we introduce a unary predicate to denote broken gates, and an Integer constant to count the number of broken gates present in a circuit.
broken: Gate -> Bool nb_broken: -> IntWith our vocabulary done, we can now move on to the structure.
Structure
In our structure, we can define the values for symbols that we already know. For instance, the circuit in the diagram shown above looks as follows:
xor := { A, B }. and := { C, D }. or := { E }. output := { A -> N, B -> P, C -> O, D -> Q, E -> R }. first_input := { A -> K, B -> N, C -> L, D -> M, E -> Q }. second_input := { A -> L, B -> M, C -> K, D -> N, E -> O }. input_wire := { K, L, M }. output_wire := { P, R }.To finish our structure, we now also interpret the symbols with the observed values of each test.
input_on := {(K, 1), (L, 2), (M, 3)}. input_off := {(L, 1), (M, 1), (K, 2), (M, 2), (K, 3), (L, 3)}. observed_on := {(P, 1), (R, 1), (P, 2), (R, 2), (P, 3)}. observed_off := {(R, 3)}.
Theory
Finally, we move on to the real meat and bones of our solution: the theory! Here, we express the formulas that actually describe the behavior of our system. In total, we want to express three pieces of knowledge.
- What the behavior of a correct gate looks like.
- How the observed input/output wires relate to the actual wires.
- How to count the number of broken gates.
!g in Gate, t in Test: and(g) & ~broken(g) => (on(first_input(g), t) & on(second_input(g), t) <=> on(output(g), t)).In words: "For every AND gate that's not broken must hold that there is an AND relationship between its input and output wires for each test".
Next, we express the relation between "input_on/off", "observed_on/off", and "on" as follows:
// Set the values of the inputwires for each test. !w in Wire, t in Test: input_on(w, t) => on(w, t). !w in Wire, t in Test: input_off(w, t) => ~on(w, t). // Set the observed output values for each test. !w in Wire, t in Test: observed_on(w, t) => on(w, t). !w in Wire, t in Test: observed_off(w, t) => ~on(w, t).Lastly, we define how to count the number of broken gates using a cardinality operator:
// Count the number of gates that are broken. nb_broken() = #{x in Gate: broken(x)}.
And we're done! We can now have IDP-Z3 look for the smallest set of broken gates, giving us the following solutions:
Model 1 ========== on := {(K, 1), (L, 2), (M, 3), (N, 1), (N, 2), (O, 1), (O, 2), (P, 1), (P, 2), (P, 3), (R, 1), (R, 2)}. broken := {C}. nb_broken := 1. Model 2 ========== on := {(K, 1), (L, 2), (M, 3), (N, 1), (N, 2), (P, 1), (P, 2), (P, 3), (Q, 1), (Q, 2), (R, 1), (R, 2)}. broken := {D}. nb_broken := 1. Model 3 ========== on := {(K, 1), (L, 2), (M, 3), (N, 1), (N, 2), (P, 1), (P, 2), (P, 3), (R, 1), (R, 2)}. broken := {E}. nb_broken := 1.
Relying on Occam's razor, there's three gates that could be the culprit: C, D, or E. This concludes our formal model. :-)
I hope this post convincingly showed that the prof. Layton puzzle is actually a real life problem in disguise. I'm quite certain there are many similar problems out there, waiting to be solved using a knowledge-based approach.
If you are interested to see the knowledge base for yourself, you can check it out in our online environment. Eager to learn more about IDP-Z3? Visit our website or our interactive tutorial site.
[1] Thanks to colleagues Marc Denecker and Joost Vennekens for helping me realise this.