{ "cells": [ { "cell_type": "markdown", "id": "e727b286", "metadata": {}, "source": [ "# BoolForge Tutorial 2: Advanced Concepts for Boolean Functions\n", "\n", "Understanding the structure of a Boolean function is essential for analyzing\n", "the behavior of the Boolean networks they define. In this tutorial, we move\n", "beyond the basics of `BooleanFunction` and explore three core concepts:\n", "\n", "- **Symmetries** among inputs\n", "- **Activities** of inputs\n", "- **Average sensitivity** of a Boolean function\n", "\n", "These quantities are tied to redundancy, robustness, and dynamical behavior –\n", "concepts that will play a central role in later tutorials on canalization and\n", "network dynamics.\n", "\n", "## What you will learn\n", "In this tutorial you will learn how to:\n", "\n", "- identify symmetry groups of Boolean functions,\n", "- compute activities and sensitivities,\n", "- choose between exact and Monte Carlo computation,\n", "- interpret these quantities in terms of robustness and redundancy.\n", "\n", "---\n", "## 0. Setup" ] }, { "cell_type": "code", "execution_count": 1, "id": "939220e9", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.031394Z", "iopub.status.busy": "2026-01-15T17:23:50.031168Z", "iopub.status.idle": "2026-01-15T17:23:50.560785Z", "shell.execute_reply": "2026-01-15T17:23:50.560452Z" }, "lines_to_next_cell": 2 }, "outputs": [], "source": [ "import boolforge\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "6f1198ca", "metadata": {}, "source": [ "---\n", "## 1. Symmetries in Boolean Functions\n", "\n", "In gene regulation, symmetric variables might represent\n", "redundant transcription factor binding sites or functionally equivalent \n", "repressors. Identifying symmetries can:\n", "- Reduce model complexity\n", "- Suggest evolutionary mechanisms (gene duplication)\n", "- Identify potential drug targets (symmetric inputs may compensate)\n", "\n", "### 1.1 What is a symmetry?\n", "\n", "A symmetry of a Boolean function is a permutation of input variables that does\n", "**not** change its output.\n", "\n", "- Inputs in the same symmetry group can be swapped freely.\n", "- Inputs in different groups cannot.\n", "\n", "### 1.2 Examples\n", "\n", "Below we define three Boolean functions demonstrating full, partial, and no\n", "symmetry." ] }, { "cell_type": "code", "execution_count": 2, "id": "84ba582b", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.562767Z", "iopub.status.busy": "2026-01-15T17:23:50.562570Z", "iopub.status.idle": "2026-01-15T17:23:50.587518Z", "shell.execute_reply": "2026-01-15T17:23:50.587240Z" }, "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "x0\tx1\tx2\t|\tf\tg\th\n", "-------------------------------------------------\n", "0\t0\t0\t|\t0\t0\t0\n", "0\t0\t1\t|\t1\t0\t0\n", "0\t1\t0\t|\t1\t0\t1\n", "0\t1\t1\t|\t0\t1\t0\n", "1\t0\t0\t|\t1\t1\t1\n", "1\t0\t1\t|\t0\t1\t1\n", "1\t1\t0\t|\t0\t1\t1\n", "1\t1\t1\t|\t1\t1\t1\n" ] } ], "source": [ "# Fully symmetric (parity / XOR)\n", "f = boolforge.BooleanFunction(\"(x0 + x1 + x2) % 2\")\n", "\n", "# Partially symmetric\n", "g = boolforge.BooleanFunction(\"x0 | (x1 & x2)\")\n", "\n", "# No symmetry\n", "h = boolforge.BooleanFunction(\"x0 | (x1 & ~x2)\")\n", "\n", "labels = [\"f\", \"g\", \"h\"]\n", "boolforge.display_truth_table(f, g, h, labels=labels)" ] }, { "cell_type": "code", "execution_count": 3, "id": "bfeb97ee", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.588609Z", "iopub.status.busy": "2026-01-15T17:23:50.588527Z", "iopub.status.idle": "2026-01-15T17:23:50.590571Z", "shell.execute_reply": "2026-01-15T17:23:50.590363Z" }, "lines_to_next_cell": 2 }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Symmetry groups of f:\n", " ['x0' 'x1' 'x2']\n", "\n", "Symmetry groups of g:\n", " ['x0']\n", " ['x1' 'x2']\n", "\n", "Symmetry groups of h:\n", " ['x0']\n", " ['x1']\n", " ['x2']\n", "\n" ] } ], "source": [ "for func, label in zip([f, g, h], labels):\n", " print(f\"Symmetry groups of {label}:\")\n", " for group in func.get_symmetry_groups():\n", " print(\" \", func.variables[np.array(group)])\n", " print()" ] }, { "cell_type": "markdown", "id": "f0cb77c4", "metadata": {}, "source": [ "**Interpretation**\n", "\n", "- `f` is fully symmetric: all variables are interchangeable.\n", "- `g` has partial symmetry: `x1` and `x2` are equivalent but `x0` is distinct.\n", "- `h` has no symmetries: all inputs play unique roles.\n", "\n", "These patterns foreshadow the concepts of canalization, and specifically\n", "canalizing layers, explored in later tutorials.\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "id": "8a0cbb1a", "metadata": {}, "source": [ "---\n", "## 2. Degenerate functions\n", "\n", "A function is **degenerate** if one or more inputs do not matter at all. " ] }, { "cell_type": "code", "execution_count": 4, "id": "b0d03492", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.591597Z", "iopub.status.busy": "2026-01-15T17:23:50.591533Z", "iopub.status.idle": "2026-01-15T17:23:50.786864Z", "shell.execute_reply": "2026-01-15T17:23:50.786624Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "f.is_degenerate() False\n", "k.is_degenerate() True\n" ] } ], "source": [ "print(\"f.is_degenerate()\", f.is_degenerate())\n", "k = boolforge.BooleanFunction(\"(x AND y) OR x\")\n", "print(\"k.is_degenerate()\", k.is_degenerate())" ] }, { "cell_type": "markdown", "id": "81c6ae2d", "metadata": {}, "source": [ "Detecting degeneracy is NP-hard in general.\n", "However, such functions are extremely rare unless intentionally created.\n", "\n", "BoolForge therefore:\n", "\n", "- allows degenerate functions by default,\n", "- avoids expensive essential-variable checks unless requested.\n", "\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "id": "3e0997ef", "metadata": {}, "source": [ "---\n", "## 3. Activities and Sensitivities\n", "\n", "Activities and sensitivity quantify how much each input affects the output of\n", "a Boolean function.\n", "\n", "### 3.1 Activity\n", "\n", "The activity of input $x_i$ is the probability that flipping $x_i$ changes the\n", "function’s output:\n", "\n", "$$\n", "a(f,x_i) = \\Pr[f(\\mathbf{x}) \\neq f(\\mathbf{x} \\oplus e_i)],\n", "$$\n", "where $e_i=(0,\\ldots,0,1,0,\\ldots,0)$ is the ith unit vector.\n", "\n", "- If $a = 1$: the variable always matters.\n", "- If $a = 0$: the variable is irrelevant (degenerate).\n", "- In large random Boolean functions, $a \\approx 0.5$ for all variables.\n", "\n", "### 3.2 Average sensitivity\n", "\n", "The *average sensitivity* of a Boolean function describes \n", "how sensitive its output is to changes in its inputs, specifically to a random single-bit flip. \n", "The (unnormalized) average sensitivity is the sum of all its activities:\n", "\n", "$$\n", "S(f) = \\sum_i a(f,x_i).\n", "$$\n", "\n", "Division by $n$ yields the *normalized average sensitivity* $s(f)$, \n", "which can be readily compared between functions of different degree $n$:\n", "\n", "$$\n", "s(f) = \\frac{S(f)}{n}.\n", "$$\n", "\n", "**Interpretation**\n", "\n", "In Boolean network theory, the mean normalized average sensitivity $s(f)$\n", "determines how perturbations tend to propagate through the system.\n", "\n", "- If $s(f) < 1$, perturbations tend to die out (*ordered regime*).\n", "- If $s(f) > 1$, perturbations typically amplify (*chaotic regime*).\n", "- The boundary $s(f) = 1$ defines the *critical regime*.\n", "\n", "The critical regime is believed to characterize many biological \n", "networks (see later tutorials). It represents a balance between order and chaos. \n", "Operating at this \"edge of chaos\" may optimize information processing and evolvability.\n", "\n", "### 3.3 Exact vs Monte Carlo computation\n", "\n", "- Exact (`EXACT=True`) computation enumerates all $2^n$ states; feasible for small $n$.\n", "- Monte Carlo (`EXACT=False`, default) simulation approximates using random samples; scalable\n", " to large $n$.\n", "\n", "Computational cost guide:\n", "- Exact methods: $O(2^n)$ time and space, where $n =$ number of inputs.\n", "- Monte Carlo: $O(k)$ time, where $k =$ number of samples.\n", "\n", "Recommendation:\n", "- $n \\leq 10$: Use exact methods (fast, deterministic)\n", "- $10 < n \n", "\\leq 20$: Use exact if possible, Monte Carlo if repeated computation needed\n", "- n > 20: Use Monte Carlo (exact is infeasible)\n", "\n", "### 3.4 Computing activities and sensitivities\n", "\n", "To investigate how to compute the activities and the average sensitivity in `BoolForge`, \n", "we work with the linear function `f` from above, as well as with the function `g`." ] }, { "cell_type": "code", "execution_count": 5, "id": "aa75c5ee", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.788145Z", "iopub.status.busy": "2026-01-15T17:23:50.788013Z", "iopub.status.idle": "2026-01-15T17:23:50.790204Z", "shell.execute_reply": "2026-01-15T17:23:50.790015Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Activities of f: [1. 1. 1.]\n", "Activities of g: [0.75 0.25 0.25]\n", "Normalized average sensitivity of f: 1.0\n", "Normalized average sensitivity of g: 0.4166666666666667\n" ] } ], "source": [ "EXACT = True\n", "\n", "print(\"Activities of f:\", f.get_activities(EXACT=EXACT))\n", "print(\"Activities of g:\", g.get_activities(EXACT=EXACT))\n", "\n", "print(\"Normalized average sensitivity of f:\", f.get_average_sensitivity(EXACT=EXACT))\n", "print(\"Normalized average sensitivity of g:\", g.get_average_sensitivity(EXACT=EXACT))" ] }, { "cell_type": "markdown", "id": "5a45c397", "metadata": {}, "source": [ "**Interpretation**\n", "\n", "- For `f` (XOR), flipping any input always flips the output, so $s(f) = 1$.\n", "- For `g`, $x_0$ influences the output more often than $x_1$ or $x_2$. 75% of $x_0$ flips and 25% of $x_1$ or $x_2$ flips change the output of `g`. Thus, the normalized average sensitivity of `g` is $\\frac 13*75\\% + \\frac 23 25\\% = \\frac{5}{12}$.\n", "\n", "This unequal influence is a precursor to canalization, \n", "a property investigated in depth in the next tutorial.\n", "\n", "Exact computation is infeasible for large $n$, so Monte Carlo simulation must\n", "be used.\n", "\n", "### Example: random 25-input function\n", "\n", "When generating such a large function randomly (see Tutorial 4) it not recommended to require that all inputs are essential, as (i) this is almost certainly the case anyways (the probability that an n-input function does not depend on input $x_i$ is given $1/2^{n-1}$), and (ii) checking for input degeneracy is NP-hard (i.e., very computationally expensive). We thus set `ALLOW_DEGENERATE_FUNCTIONS=True`. You find more on this and the `random_function` method in Tutorial 4. " ] }, { "cell_type": "code", "execution_count": 6, "id": "0b1006ea", "metadata": { "execution": { "iopub.execute_input": "2026-01-15T17:23:50.791206Z", "iopub.status.busy": "2026-01-15T17:23:50.791141Z", "iopub.status.idle": "2026-01-15T17:23:51.050530Z", "shell.execute_reply": "2026-01-15T17:23:51.050275Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mean activity: 0.4989\n", "Normalized average sensitivity: 0.5001\n" ] } ], "source": [ "EXACT = False\n", "n = 25\n", "\n", "h = boolforge.random_function(n=n, ALLOW_DEGENERATE_FUNCTIONS=True)\n", "\n", "activities = h.get_activities(EXACT=EXACT)\n", "print(f\"Mean activity: {np.mean(activities):.4f}\")\n", "print(\n", " f\"Normalized average sensitivity: \"\n", " f\"{h.get_average_sensitivity(EXACT=EXACT):.4f}\"\n", ")" ] }, { "cell_type": "markdown", "id": "0e7bb4d9", "metadata": { "lines_to_next_cell": 2 }, "source": [ "**Interpretation**\n", "\n", "Random Boolean functions satisfy:\n", "\n", "- mean activity $\\approx 0.5$,\n", "- normalized average sensitivity $\\approx 0.5$.\n", "\n", "Thus, the results for `h` align with known theoretical results. \n", "More generally, random Boolean function results define the typical behavior\n", "against which biological functions can be compared (see Tutorial 5)." ] }, { "cell_type": "markdown", "id": "c6d1b278", "metadata": {}, "source": [ "---\n", "## 4. Summary\n", "\n", "In this tutorial you learned:\n", "\n", "- how to compute symmetry groups,\n", "- how to test for input degeneracy,\n", "- how to compute activities and sensitivities,\n", "- how these quantities relate to robustness and structure.\n", "\n", "These concepts provide essential foundations for understanding \n", "\n", "- canalization, the core concept of Tutorial 3,\n", "- and the robustness of Boolean networks, explored in Tutorial 8." ] } ], "metadata": { "jupytext": { "cell_metadata_filter": "-all", "main_language": "python", "notebook_metadata_filter": "-all" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }