Discrete Mathematics YMT211 · Fall 2026 Assoc. Prof. Ferhat Uçar

>_ Lab 2 · Week 3 · Monday, October 5, 2026

Gates, Adders and Knights & Knaves

Build logic gates, check equivalences by brute force, add binary numbers with a ripple-carry adder made of gates, prove that NAND is enough, and write a solver for Knights and Knaves puzzles.

week
3 · Mon, Oct 5
due
Fri, Oct 9 · 23:59
assignment
lab02
points
100 (autograded)
time
75 min + finish at home
files you edit
circuits.pypuzzles.py

Goal

This lab turns the week 3 notes into working code. By the end you have:

  • the five logic gates and a brute-force equivalence checker that finds counterexamples,
  • a half adder, a full adder and a ripple-carry adder that adds binary numbers using nothing but gates,
  • NOT, AND, OR and XOR rebuilt from NAND alone,
  • a Knights and Knaves solver that cracks any puzzle you can write down, and a consistency checker for requirements.

Steps

1

Join the course organization (once)

⏱ 2 min

You received an email from GitHub at your university address (your-student-number@firat.edu.tr), titled “… invited you to join the FiratUniversity-IJDP-SoftEng organization”.

  1. Open the email and click Join FiratUniversity-IJDP-SoftEng.
  2. Sign in with your GitHub account (the one you used for Lab 1) and click Join.

That is all. You are now on the course roster.

2

Accept Lab 2

⏱ 1 min
  1. Click the orange Accept lab lab02 button at the top of this page.
  2. If asked, click Sign in with GitHub and authorize Classroom 50.
  3. Click Accept assignment and wait for the checklist to finish (about 20 seconds).
  4. Click Open repository.

You now have a private repository called discrete-mathematics-lab02-yourusername inside the course organization, with the starter code, the tests and the notebook. Only you and the instructor can see it.

3

Open it in Codespaces

⏱ 2 min

On your repository page: green Code button → Codespaces tab → Create codespace on main. Exactly like Lab 1; the first start takes a minute or two.

4

Look around and run the checks

⏱ 3 min
FileWhat it is
circuits.pyyour work, part 1: gates, equivalence, adders, NAND
puzzles.pyyour work, part 2: Knights & Knaves, requirements
lab02.ipynbplayground notebook, not graded
check.py, tests/the checks (do not edit the tests)
>_ terminal
$ python check.py:| the gate matches its truth table and returns 0/1  [AND]
  not implemented yet (still raises NotImplementedError)
...
0/34 checks passed

In circuits.py a bit is the integer 1 or 0, like a wire. In puzzles.py truth values are True and False. Keep that in mind; it is the most common source of surprises.

5

Part A: logic gates

⏱ 5 min

Write NOT, AND, OR, XOR and NAND in circuits.py. Each takes bits and must return 0 or 1, not True or False.

Two ways to think about it: with conditions (return 1 if a and b else 0) or with arithmetic on bits (1 - a is NOT). For NAND, reuse your own gates: NAND is NOT of AND.

6

Part B: equivalence by brute force

⏱ 10 min

counterexample(f, g, n) walks through every row of n bits and returns the first row where f and g disagree, or None. It is the equivalence checker from the notes, in eight lines.

from itertools import product
for row in product([1, 0], repeat=2):
    print(row)          # (1, 1), (1, 0), (0, 1), (0, 0)

Then equivalent(f, g, n) is one line: two formulas are equivalent exactly when there is no counterexample.

>_ terminal
$ python circuits.py...
p→q ≡ ¬q→¬p ? True
p→q ≡ q→p   ? counterexample: (1, 0)
7

Part C: adders from gates

⏱ 20 min

Use only your gate functions: the checks reject +, -, // and % in the adders.

  1. half_adder(a, b) returns (sum, carry): sum is XOR, carry is AND. One line.
  2. full_adder(a, b, cin) returns (sum, carry_out). Either use the two formulas in the docstring, or chain two half adders and one OR (the circuit on the week 3 page).
  3. add_binary(x, y) adds two bit strings like a CPU does:
    • pad the shorter string with "0" on the left (str.zfill helps),
    • walk the columns from right to left with reversed(...), starting with carry 0,
    • each column: s, carry = full_adder(int(a), int(b), carry),
    • if a carry is left at the end, it becomes an extra leading "1".
>_ terminal
$ python circuits.pyhalf_adder(1, 1) = (0, 1)
11 + 6 = 10001 (binary)

The checks add all 256 pairs of 4-bit numbers with your adder. If one pair fails, the message tells you which.

8

Part D: NAND is enough

⏱ 10 min

Rebuild NOT, AND, OR and XOR using only calls to NAND: no other gates, no and/or/not, no operators, no if. The checks read your code to make sure. The identities are in the notes:

¬p≡p↑p,p∧q≡(p↑q)↑(p↑q),p∨q≡(p↑p)↑(q↑q)\neg p \equiv p \uparrow p, \qquad p \land q \equiv (p \uparrow q) \uparrow (p \uparrow q), \qquad p \lor q \equiv (p \uparrow p) \uparrow (q \uparrow q)

For XOR, start with n = NAND(a, b); four NAND calls are enough.

9

Part E: a Knights and Knaves solver

⏱ 15 min

Open puzzles.py. A world says who is a knight: {"A": True, "B": False}. A statement is a function of the world:

"A": lambda w: w["B"]                  # A says: "B is a knight"
"B": lambda w: w["A"] != w["B"]        # B says: "We are of opposite types"

solve(people, statements) tries every world and keeps the consistent ones. The one rule from the lecture: a speaker is a knight if and only if their statement is true, so for every speaker s, w[s] == statements[s](w) must hold.

  1. Loop over product([True, False], repeat=len(people)).
  2. Turn each tuple into a world with dict(zip(people, values)).
  3. Keep the world if the rule holds for all speakers (all(...) is ∀ again).
10

Part F: write the puzzles

⏱ 10 min

Translate each puzzle into a statements dict. Then run python puzzles.py and check the answers make sense.

  • Puzzle 1 is the one from the notes: both should be knaves.
  • Puzzle 2: A says “at least one of us is a knave”. B says nothing, so B has no entry.
  • Puzzle 3: A says “all three of us are knaves”, B says “exactly one of us is a knight”. Hint: [w["A"], w["B"], w["C"]].count(True) == 1.
11

Part G: are the requirements consistent?

⏱ 10 min

satisfying_row(formulas, n) returns the first row that makes every formula true, or None. login_requirements() returns the four requirements from the week 3 notes as functions of (p, q, r).

Translate carefully: requirement 1 says access is granted only if the user is authenticated. Which way does the arrow point? (Week 2: “pp only if qq” is p→qp \to q.)

>_ terminal
$ python puzzles.py...
requirements 1–3 consistent: (True, True, False)
requirements 1–4 consistent: None
12

Submit

⏱ 1 min

Submitting is pushing. Every push is graded automatically.

>_ terminal
$ git add -A$ git commit -m "Lab 2"$ git push

About two minutes later:

  1. On your repository page, look at Releases on the right. The newest submit/… release shows your score and a table of passed and failed checks.
  2. Or open our classroom on Classroom 50 → Lab 2 → My submission.

Push as often as you like before Friday 23:59; the last push before the deadline counts.

Other ways to work and submit

Codespaces is the easiest route, but it is not the only one. You can write your code anywhere and hand it in from the browser. Grading is exactly the same: every change that lands in your repository is graded automatically.

Where you can work

OptionWhat to do
Your own computerInstall Python 3.12+ and an editor such as VS Code. Download circuits.py, puzzles.py, check.py and the tests folder from your repository (green Code button → Download ZIP), unzip, then run pip install pytest and python check.py.
cs50.devOpen cs50.dev, create circuits.py and puzzles.py, paste the starter code from your repository and work there. Run python circuits.py and python puzzles.py to try your functions.
Google Colab or any online PythonPaste one function at a time into a cell and test it on a few inputs. Good for experimenting; the official checks run on GitHub.
github.devOn your repository page press the . key. A light VS Code opens in the browser for editing (it cannot run Python, but you can commit from it).

Submit from the browser (no terminal, no git)

When your solution is ready in a file on your computer or in another editor, put it into your repository on github.com in one of two ways.

A. Paste into the file

  1. Open your repository discrete-mathematics-lab02-yourusername on github.com.
  2. Click circuits.py, then the pencil icon ✎ (Edit this file) at the top right of the file.
  3. Select everything (Ctrl + A, or ⌘ + A on a Mac) and paste your complete file.
  4. Click Commit changes…, write a short message such as Lab 2 circuits, keep Commit directly to the main branch selected, and click Commit changes.
  5. Do the same for puzzles.py.

B. Upload the files

  1. On your repository page click Add file → Upload files.
  2. Drag circuits.py and puzzles.py from your computer into the box. Files with the same name replace the starter files; that is what you want.
  3. Click Commit changes.

Check your grade the same way. About two minutes after each commit, a new Release with your score appears on the right of your repository page. Every commit counts as a submission, the last one before Friday 23:59 is the one that matters, and you may mix methods (for example, push from Codespaces on Monday and paste a fix from home on Wednesday).

Explore: the notebook

lab02.ipynb is not graded. Encrypt a message with XOR and see why reusing a key is fatal, rebuild a dead RAID disk from parity, count how many NAND gates a full adder costs, and watch brute force grow exponentially.

Lab 2 · Explore notebook

Read it here, run it in Colab or in your cs50.dev workspace.

Challenges (optional, not graded)

  1. Subtraction. Two’s complement turns x−yx - y into x+¬y+1x + \lnot y + 1. Write sub_binary(x, y) with your gates and add_binary.
  2. Comparator. Write greater(x, y) for 4-bit strings using only gates.
  3. A harder island. C says “A and B are of the same type”. A says “C is a knave”. What are they? Let your solver answer.

Troubleshooting