# Function Operations Guide BooFun supports building complex functions from simpler ones using Boolean operators and other operations. > **Note:** For creating functions from data (truth tables, strings, files), see the [Representations Guide](representations.md). ## Boolean Operators Combine functions using Python operators: | Operator | Name | Result | |----------|------|--------| | `f & g` | AND | (f ∧ g)(x) = f(x) ∧ g(x) | | `f \| g` | OR | (f ∨ g)(x) = f(x) ∨ g(x) | | `f ^ g` | XOR | (f ⊕ g)(x) = f(x) ⊕ g(x) | | `~f` | NOT | (¬f)(x) = ¬f(x) | ### Basic Examples ```python import boofun as bf # Built-in functions and3 = bf.AND(3) or3 = bf.OR(3) parity3 = bf.parity(3) maj3 = bf.majority(3) # Combine with operators f1 = and3 ^ parity3 # AND XOR PARITY f2 = maj3 & ~and3 # MAJORITY AND (NOT AND) f3 = (and3 | or3) ^ parity3 # Complex composition # All results are BooleanFunction objects print(f1.n_vars) # 3 print(f1.evaluate(5)) # Evaluate at input 5 ``` ### Operator Precedence Python's operator precedence applies: 1. `~` (NOT) - highest 2. `&` (AND) 3. `^` (XOR) 4. `|` (OR) - lowest Use parentheses for clarity: ```python # These are different! f1 = a | b & c # Same as: a | (b & c) f2 = (a | b) & c # Explicit grouping ``` ### Composite Functions and Fourier Composite functions work seamlessly with analysis: ```python import numpy as np f = bf.AND(3) ^ bf.parity(3) # Fourier coefficients (NumPy array) fourier = f.fourier() print(f"Type: {type(fourier)}") # numpy.ndarray # Parseval's identity still holds print(f"‖f̂‖² = {fourier @ fourier}") # 1.0 # All analysis methods work print(f"Degree: {f.degree()}") print(f"Influences: {f.influences()}") ``` ## Input Negation Negate inputs (flip all bits) using unary minus: ```python f = bf.majority(3) # Input negation: g(x) = f(-x) where -x flips all bits g = -f # In Fourier: ĝ(S) = (-1)^|S| · f̂(S) # (odd-degree coefficients flip sign) ``` Note: `-f` negates *inputs*, while `~f` negates *output*. ```python f = bf.AND(3) neg_input = -f # g(x) = f(flip all bits of x) neg_output = ~f # g(x) = NOT f(x) ``` ## Chainable Methods Alternative syntax using method chaining: ```python f = bf.AND(3) g = bf.OR(3) # These pairs are equivalent: f & g == f.and_(g) f | g == f.or_(g) f ^ g == f.xor(g) ~f == f.not_() ``` ## Variable Restriction Fix variables to constant values: ```python f = bf.majority(5) # Fix variable 0 to value 1 g = f.fix(0, 1) print(g.n_vars) # 4 # Fix multiple variables h = f.fix([0, 2], [1, 0]) # x₀=1, x₂=0 print(h.n_vars) # 3 # Shannon cofactor cofactor_1 = f.cofactor(0, 1) # f|_{x₀=1} cofactor_0 = f.cofactor(0, 0) # f|_{x₀=0} ``` ## Variable Permutation Reorder input variables: ```python f = bf.create([0, 0, 0, 1, 0, 1, 1, 1]) # Some 3-variable function # Permute: new variable i gets old variable perm[i] g = f.permute_variables([2, 0, 1]) # Cycle: x₀→x₂→x₁→x₀ # Swap two variables h = f.permute_variables([1, 0, 2]) # Swap x₀ and x₁ ``` ## Dual Function The dual swaps AND and OR (De Morgan dual for monotone functions): ```python f = bf.AND(3) f_dual = f.dual() # f*(x) = NOT f(NOT x) # For AND: dual is OR # For monotone: f* swaps roles of AND/OR ``` ## Extending Functions Add dummy variables: ```python f = bf.majority(3) # Extend to 5 variables (new vars are dummy) g = f.extend(5, method="dummy") print(g.n_vars) # 5 # New variables don't affect output for x in range(8): assert f.evaluate(x) == g.evaluate(x) # Same for first 8 inputs ``` ## Composing with Noise Apply noise operator (sampling-based): ```python f = bf.parity(5) # Apply noise: each input bit flips with prob (1-ρ)/2 noisy_f = f.apply_noise(rho=0.9, samples=100) # Get exact noise expectations expectations = f.noise_expectation(rho=0.9) # Returns E[f(y)|x] for all x, where y is ρ-correlated with x ``` ## Building Custom Functions Combine operations to build complex functions: ```python # Threshold-2 function: at least 2 of 4 variables are 1 x0 = bf.dictator(4, 0) x1 = bf.dictator(4, 1) x2 = bf.dictator(4, 2) x3 = bf.dictator(4, 3) threshold_2 = ( (x0 & x1) | (x0 & x2) | (x0 & x3) | (x1 & x2) | (x1 & x3) | (x2 & x3) ) # Verify print(threshold_2.evaluate(0b0011)) # 1 (two bits set) print(threshold_2.evaluate(0b0001)) # 0 (one bit set) ``` ## NumPy Integration Fourier coefficients are NumPy arrays: ```python import numpy as np f = bf.majority(5) fourier = f.fourier() # All NumPy operations work print(f"Shape: {fourier.shape}") print(f"Dtype: {fourier.dtype}") print(f"L2 norm: {np.linalg.norm(fourier)}") print(f"Dot product: {fourier @ fourier}") print(f"Max coefficient: {np.max(np.abs(fourier))}") # Find heavy coefficients heavy = np.where(np.abs(fourier) > 0.1)[0] print(f"Heavy coefficient indices: {heavy}") ``` ## Performance Notes - Boolean operations create new truth tables (O(2^n) time and space) - For n > 14, consider lazy evaluation or symbolic representations - Composite functions cache their representations after first computation ## See Also - [Representations Guide](representations.md) - Creating functions - [Spectral Analysis Guide](spectral_analysis.md) - Fourier analysis - [Query Complexity Guide](query_complexity.md) - Sensitivity and certificates