{
  "category": "python-advanced",
  "displayName": "Advanced Python",
  "description": "Advanced Python programming patterns in TouchDesigner including asyncio for non-blocking operations, tdu.Dependency reactive patterns, threading safety, and numpy/scipy/OpenCV integration for image and data processing.",
  "techniques": [
    {
      "id": "asyncio_in_td",
      "name": "asyncio in TouchDesigner",
      "subcategory": "asyncio",
      "description": "Using Python's asyncio for non-blocking operations inside TouchDesigner. Enables HTTP requests, file I/O, and long-running computations without stalling the cook thread.",
      "difficulty": "advanced",
      "operators": ["Execute DAT", "Script CHOP", "Script DAT"],
      "tags": ["asyncio", "async", "await", "non-blocking", "concurrent", "HTTP", "coroutine"],
      "notes": "TouchDesigner runs its own event loop. Use td.asyncio or run coroutines with run() on the existing loop. Blocking calls inside cook() stall the entire TD cook thread — use asyncio for I/O-bound work.",
      "code": {
        "language": "python",
        "filename": "asyncio_td.py",
        "snippet": "# asyncio in TouchDesigner\n# TD maintains its own event loop — integrate carefully\n\nimport asyncio\nimport urllib.request\nimport threading\n\n# --- Pattern 1: Run async task in background thread ---\n# TD's cook thread must not be blocked\n# Best practice: run a background thread with its own event loop\n\n_bg_loop = None\n_bg_thread = None\n_results = {}\n\ndef start_background_loop():\n    \"\"\"Start a background asyncio event loop in a daemon thread.\"\"\"\n    global _bg_loop, _bg_thread\n    \n    def run_loop():\n        global _bg_loop\n        _bg_loop = asyncio.new_event_loop()\n        asyncio.set_event_loop(_bg_loop)\n        _bg_loop.run_forever()\n    \n    _bg_thread = threading.Thread(target=run_loop, daemon=True)\n    _bg_thread.start()\n    print('[Async] Background event loop started')\n\nasync def fetch_url_async(url, key):\n    \"\"\"Async HTTP fetch — runs in background loop.\"\"\"\n    import urllib.request\n    loop = asyncio.get_event_loop()\n    # Run blocking urllib in executor to keep it truly async\n    response = await loop.run_in_executor(\n        None,\n        lambda: urllib.request.urlopen(url).read().decode('utf-8')\n    )\n    _results[key] = response\n    print(f'[Async] Fetched {url}: {len(response)} chars')\n\ndef fetch_url(url, key='last'):\n    \"\"\"Schedule an async fetch from TD code (non-blocking).\"\"\"\n    if _bg_loop is None:\n        start_background_loop()\n    asyncio.run_coroutine_threadsafe(fetch_url_async(url, key), _bg_loop)\n\ndef get_result(key='last'):\n    \"\"\"Get fetch result (may be None if not yet complete).\"\"\"\n    return _results.get(key)\n\n# Usage in Execute DAT onStart:\n# fetch_url('https://api.example.com/data', 'api_data')\n# In a Script CHOP cook(): result = get_result('api_data')\n\n\n# --- Pattern 2: Using td.ui.undo() context async-safely ---\nasync def delayed_operation(delay_seconds, callback):\n    \"\"\"Execute callback after a delay without using run().\"\"\"\n    await asyncio.sleep(delay_seconds)\n    callback()\n\n# --- Pattern 3: Async WebSocket client ---\nasync def websocket_client(uri, on_message):\n    \"\"\"\n    Maintain a persistent WebSocket connection asynchronously.\n    Call from background loop for non-blocking operation.\n    \"\"\"\n    try:\n        import websockets\n        async with websockets.connect(uri) as ws:\n            print(f'[WS] Connected to {uri}')\n            while True:\n                msg = await ws.recv()\n                on_message(msg)  # call TD code to process message\n    except Exception as e:\n        print(f'[WS] Disconnected: {e}')"
      }
    },
    {
      "id": "tdu_dependency",
      "name": "tdu.Dependency Reactive Patterns",
      "subcategory": "dependency",
      "description": "Using tdu.Dependency to create reactive data bindings in TouchDesigner. Operators and scripts automatically re-cook when dependency values change, enabling clean data-flow architectures.",
      "difficulty": "intermediate",
      "operators": ["Execute DAT", "Script CHOP", "DAT Execute"],
      "tags": ["tdu", "Dependency", "reactive", "binding", "data-flow", "observer"],
      "notes": "tdu.Dependency is a built-in TD class that wraps values with change-notification. Any cook() that reads a Dependency will be dirtied automatically when the value changes. Store Dependency objects at module level.",
      "code": {
        "language": "python",
        "filename": "tdu_dependency.py",
        "snippet": "# tdu.Dependency Reactive Patterns in TouchDesigner\n\n# --- Basic Dependency ---\n# Create reactive value at module level\nmy_color = tdu.Dependency([1.0, 0.0, 0.0, 1.0])  # RGBA\nmy_scale = tdu.Dependency(1.0)\n\ndef update_color(r, g, b, a=1.0):\n    \"\"\"Update the reactive color — all dependents auto-recook.\"\"\"\n    my_color.val = [r, g, b, a]\n\ndef update_scale(s):\n    my_scale.val = float(s)\n\n# In a Script TOP or GLSL TOP's beforeCook:\n# Read the dependency — this registers a cook dependency\n# col = parent().mod('state_module').my_color.val\n\n\n# --- Advanced: Dependency Graph ---\n# Build a reactive computation graph\n\nclass ReactiveState:\n    \"\"\"\n    Central state store using tdu.Dependency.\n    Any cook that reads from this state will re-cook on changes.\n    \"\"\"\n    def __init__(self):\n        # Primary data\n        self.audio_level = tdu.Dependency(0.0)\n        self.beat        = tdu.Dependency(False)\n        self.bpm         = tdu.Dependency(120.0)\n        \n        # Derived reactive values\n        self.bass_color  = tdu.Dependency([0.0, 0.0, 0.0])\n        self.scale       = tdu.Dependency(1.0)\n        \n        # Configuration\n        self.base_color  = tdu.Dependency([0.2, 0.5, 1.0])\n    \n    def update_audio(self, level):\n        self.audio_level.val = float(level)\n        # Update derived values\n        self._recompute_color()\n        self._recompute_scale()\n    \n    def _recompute_color(self):\n        base = self.base_color.val\n        level = self.audio_level.val\n        self.bass_color.val = [\n            min(1.0, base[0] + level * 0.5),\n            base[1] * (1.0 - level * 0.3),\n            base[2]\n        ]\n    \n    def _recompute_scale(self):\n        self.scale.val = 0.5 + self.audio_level.val * 2.0\n    \n    def on_beat(self):\n        self.beat.val = True\n        run('me.parent().mod(\"state\").state.beat.val = False', delayFrames=2)\n\n# Instantiate in a module DAT\n# state = ReactiveState()\n\n# In a Script CHOP cook():\n# level = parent().mod('state_module').state.audio_level.val  # registers dep\n# scriptOp['scale'][0] = parent().mod('state_module').state.scale.val\n\n\n# --- Dependency as event bus ---\nclass EventBus:\n    \"\"\"Simple pub/sub using tdu.Dependency.\"\"\"\n    def __init__(self):\n        self._events = {}\n    \n    def get_event(self, name):\n        if name not in self._events:\n            self._events[name] = tdu.Dependency(None)\n        return self._events[name]\n    \n    def emit(self, name, data=None):\n        self.get_event(name).val = data\n    \n    def subscribe(self, name):\n        \"\"\"Read in cook() to register dependency on this event.\"\"\"\n        return self.get_event(name).val\n\n# Global event bus\nbus = EventBus()\n# emit:     bus.emit('user_input', {'x': 0.5, 'y': 0.3})\n# receive:  data = parent().mod('bus_module').bus.subscribe('user_input')"
      }
    },
    {
      "id": "threading_safety",
      "name": "Threading Safety in TouchDesigner",
      "subcategory": "threading",
      "description": "Safely using Python threads alongside TouchDesigner's main cook thread. Patterns for producer-consumer queues, thread-safe data sharing, and avoiding common race conditions.",
      "difficulty": "advanced",
      "operators": ["Script CHOP", "Execute DAT", "Script TOP"],
      "tags": ["threading", "thread-safe", "queue", "concurrent", "producer-consumer", "safety"],
      "notes": "TouchDesigner's main cook thread must never be blocked. Do I/O-bound work on background threads. Use queue.Queue for thread-safe communication. Never modify TD parameters from a background thread — schedule via run().",
      "code": {
        "language": "python",
        "filename": "threading_safety.py",
        "snippet": "# Thread-Safe Patterns for TouchDesigner\nimport threading\nimport queue\nfrom collections import deque\n\n# --- Thread-Safe Result Queue ---\n# Background thread posts results; TD main thread reads them each frame\n\n_result_queue = queue.Queue(maxsize=100)\n_worker_thread = None\n\ndef start_worker():\n    global _worker_thread\n    \n    def worker():\n        while True:\n            try:\n                # Process work items\n                work_item = _work_queue.get(timeout=1.0)\n                if work_item is None:\n                    break  # shutdown signal\n                \n                result = do_heavy_work(work_item)\n                _result_queue.put_nowait(result)\n            except queue.Empty:\n                continue\n            except queue.Full:\n                pass  # drop result if queue full\n    \n    _worker_thread = threading.Thread(target=worker, daemon=True)\n    _worker_thread.start()\n    print('[Thread] Worker started')\n\n_work_queue = queue.Queue(maxsize=10)\n\ndef submit_work(item):\n    \"\"\"Submit work from main TD thread (non-blocking).\"\"\"\n    try:\n        _work_queue.put_nowait(item)\n    except queue.Full:\n        pass  # drop if worker is busy\n\ndef do_heavy_work(item):\n    \"\"\"Runs in background thread — can block freely.\"\"\"\n    import time\n    time.sleep(0.01)  # simulate work\n    return {'processed': item, 'result': item * 2}\n\n# In Script CHOP cook() — drain results non-blocking:\ndef cook(scriptOp):\n    # Process up to N results per frame\n    for _ in range(5):\n        try:\n            result = _result_queue.get_nowait()\n            # Apply result to parameters (safe: we're in main thread)\n            scriptOp['output'][0] = result.get('result', 0.0)\n        except queue.Empty:\n            break\n\n\n# --- Read-Write Lock for shared data structures ---\nclass RWLock:\n    \"\"\"Allow multiple concurrent readers or one writer.\"\"\"\n    def __init__(self):\n        self._read_ready = threading.Condition(threading.Lock())\n        self._readers = 0\n    \n    def acquire_read(self):\n        with self._read_ready:\n            self._readers += 1\n    \n    def release_read(self):\n        with self._read_ready:\n            self._readers -= 1\n            if self._readers == 0:\n                self._read_ready.notify_all()\n    \n    def acquire_write(self):\n        self._read_ready.acquire()\n        while self._readers > 0:\n            self._read_ready.wait()\n    \n    def release_write(self):\n        self._read_ready.release()\n\n\n# --- CRITICAL: Schedule UI/parameter changes on main thread ---\ndef safe_set_param(op_path, param_name, value):\n    \"\"\"\n    Safely set a TD parameter from a background thread.\n    run() schedules execution on the main cook thread.\n    \"\"\"\n    run(f'op(\"{op_path}\").par.{param_name} = {repr(value)}')"
      }
    },
    {
      "id": "numpy_image_processing",
      "name": "NumPy Image Processing in TouchDesigner",
      "subcategory": "numpy",
      "description": "High-performance image processing using NumPy arrays in Script TOP and Script CHOP. Enables custom filters, compositing, data visualization, and real-time image analysis.",
      "difficulty": "intermediate",
      "operators": ["Script TOP", "Script CHOP"],
      "tags": ["numpy", "image", "array", "processing", "matrix", "pixel", "filter"],
      "notes": "TOP.numpyArray() returns float32 RGBA arrays with shape (H, W, 4). Values are 0.0..1.0. Use scriptOp.copyNumpyArray() to write output. NumPy is pre-installed in TD's Python.",
      "code": {
        "language": "python",
        "filename": "numpy_processing.py",
        "snippet": "# NumPy Image Processing in Script TOP\nimport numpy as np\n\ndef cook(scriptOp):\n    if not scriptOp.inputs:\n        return\n    \n    # Read input as float32 RGBA numpy array\n    frame = scriptOp.inputs[0].numpyArray(delayed=False)\n    if frame is None:\n        return\n    \n    # frame.shape = (height, width, 4)  — RGBA, float32, 0..1\n    \n    # --- Convolution Kernel (box blur 3x3) ---\n    def box_blur(img):\n        # Pad image for border handling\n        padded = np.pad(img, ((1,1),(1,1),(0,0)), mode='edge')\n        out = np.zeros_like(img)\n        for dy in range(3):\n            for dx in range(3):\n                out += padded[dy:dy+img.shape[0], dx:dx+img.shape[1]] / 9.0\n        return out\n    \n    # --- Channel Splitting and Processing ---\n    r = frame[:,:,0]\n    g = frame[:,:,1]\n    b = frame[:,:,2]\n    a = frame[:,:,3]\n    \n    # Luminance\n    luma = 0.2126 * r + 0.7152 * g + 0.0722 * b\n    \n    # Sobel Edge Detection on luminance\n    def sobel(img):\n        # Horizontal\n        kx = np.array([[-1,0,1],[-2,0,2],[-1,0,1]], dtype=np.float32)\n        ky = kx.T\n        from scipy.ndimage import convolve\n        gx = convolve(img, kx)\n        gy = convolve(img, ky)\n        return np.sqrt(gx**2 + gy**2)\n    \n    # --- Color Matrix Transform ---\n    def color_matrix(img, matrix):\n        \"\"\"Apply 3x3 color matrix to RGB channels.\"\"\"\n        rgb = img[:,:,:3].reshape(-1, 3)\n        rgb_out = rgb @ matrix.T\n        out = img.copy()\n        out[:,:,:3] = np.clip(rgb_out.reshape(img.shape[:2] + (3,)), 0, 1)\n        return out\n    \n    # Sepia matrix\n    sepia = np.array([\n        [0.393, 0.769, 0.189],\n        [0.349, 0.686, 0.168],\n        [0.272, 0.534, 0.131]\n    ], dtype=np.float32)\n    \n    # result = color_matrix(frame, sepia)\n    \n    # --- Vectorized Threshold ---\n    threshold = 0.5\n    mask = (luma > threshold).astype(np.float32)\n    result = frame * mask[:,:,np.newaxis]\n    \n    scriptOp.copyNumpyArray(result)"
      }
    },
    {
      "id": "scipy_signal_processing",
      "name": "SciPy Signal Processing",
      "subcategory": "scipy",
      "description": "Using SciPy for advanced signal processing in TouchDesigner: custom digital filters, FFT analysis, spectrogram generation, curve fitting, and statistical analysis of CHOP data.",
      "difficulty": "advanced",
      "operators": ["Script CHOP", "Script TOP"],
      "tags": ["scipy", "signal", "filter", "FFT", "IIR", "FIR", "spectrogram"],
      "notes": "SciPy is pre-installed in TD's Python. Use scipy.signal for digital filters, scipy.fft for Fourier analysis. Apply IIR/FIR filters to CHOP data for custom smoothing and frequency shaping.",
      "code": {
        "language": "python",
        "filename": "scipy_signal.py",
        "snippet": "# SciPy Signal Processing in Script CHOP\nimport numpy as np\nfrom scipy import signal\n\n# --- Pre-compute filter coefficients at setup time ---\n_butter_b = None\n_butter_a = None\n_filter_state = None\n\ndef onSetupParameters(scriptOp):\n    global _butter_b, _butter_a, _filter_state\n    # Design a 4th-order Butterworth low-pass filter\n    # cutoff = 10 Hz, sample rate = 30 fps\n    fs = 30.0\n    cutoff = 5.0  # Hz\n    _butter_b, _butter_a = signal.butter(4, cutoff / (fs / 2.0), btype='low')\n    _filter_state = signal.lfilter_zi(_butter_b, _butter_a)\n    print(f'[SciPy] Butterworth filter: {cutoff}Hz cutoff at {fs}fps')\n\ndef cook(scriptOp):\n    global _filter_state\n    \n    rawChop = op('raw_input')  # CHOP with raw noisy data\n    if not rawChop or _butter_b is None:\n        return\n    \n    raw_vals = np.array([rawChop[0][i] for i in range(rawChop.numSamples)])\n    \n    # Apply causal IIR filter\n    filtered, _filter_state = signal.lfilter(_butter_b, _butter_a, raw_vals, zi=_filter_state)\n    \n    for i in range(len(filtered)):\n        scriptOp['filtered'][i] = float(filtered[i])\n\n\n# --- Spectrogram Generation (for visualization) ---\ndef generate_spectrogram(audio_signal, fs=48000, nperseg=1024):\n    \"\"\"\n    Generate a mel-scaled spectrogram from audio data.\n    Returns frequency bins, time bins, and power spectrum.\n    \"\"\"\n    freqs, times, Sxx = signal.spectrogram(\n        audio_signal, fs=fs, nperseg=nperseg,\n        noverlap=nperseg//2, scaling='spectrum'\n    )\n    # Convert to dB scale\n    Sxx_db = 10 * np.log10(np.maximum(Sxx, 1e-10))\n    # Normalize to 0..1\n    Sxx_norm = (Sxx_db - Sxx_db.min()) / (Sxx_db.max() - Sxx_db.min() + 1e-8)\n    return freqs, times, Sxx_norm\n\n\n# --- Curve Fitting ---\ndef fit_gaussian_to_spectrum(freqs, magnitudes):\n    \"\"\"\n    Fit a Gaussian to a spectrum peak to find precise frequency and width.\n    \"\"\"\n    from scipy.optimize import curve_fit\n    \n    def gaussian(x, amplitude, mean, sigma):\n        return amplitude * np.exp(-0.5 * ((x - mean) / sigma) ** 2)\n    \n    # Initial guess: peak amplitude, peak frequency, width=200Hz\n    peak_idx = np.argmax(magnitudes)\n    p0 = [magnitudes[peak_idx], freqs[peak_idx], 200.0]\n    \n    try:\n        popt, _ = curve_fit(gaussian, freqs, magnitudes, p0=p0)\n        amplitude, center_freq, bandwidth = popt\n        return {'amplitude': amplitude, 'center': center_freq, 'bandwidth': bandwidth}\n    except RuntimeError:\n        return None"
      }
    },
    {
      "id": "opencv_integration",
      "name": "OpenCV Integration",
      "subcategory": "opencv",
      "description": "Integrating OpenCV (cv2) into TouchDesigner for computer vision: feature detection, optical flow, background subtraction, contour finding, and camera calibration.",
      "difficulty": "intermediate",
      "operators": ["Script TOP", "Script CHOP"],
      "tags": ["OpenCV", "cv2", "computer-vision", "feature-detection", "optical-flow", "contour"],
      "notes": "Install OpenCV into TD's Python: [TD_PYTHON_EXE] -m pip install opencv-python-headless. Note: use headless version to avoid conflicts with TD's own OpenGL/Qt context. OpenCV uses BGR channel order and uint8 by default.",
      "code": {
        "language": "python",
        "filename": "opencv_integration.py",
        "snippet": "# OpenCV Integration in Script TOP\n# Install: [TD_PYTHON] -m pip install opencv-python-headless\nimport numpy as np\n\n# --- Feature Detection ---\ndef cook_feature_detect(scriptOp):\n    if not scriptOp.inputs: return\n    \n    frame = scriptOp.inputs[0].numpyArray(delayed=False)\n    if frame is None: return\n    \n    import cv2\n    \n    # Convert float32 RGBA 0..1 -> uint8 BGR for OpenCV\n    rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n    bgr = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2BGR)\n    gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)\n    \n    # Harris Corner Detection\n    corners = cv2.cornerHarris(gray.astype(np.float32), blockSize=2, ksize=3, k=0.04)\n    corners_norm = cv2.normalize(corners, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    \n    # ORB Feature Points\n    orb = cv2.ORB_create(nfeatures=500)\n    keypoints = orb.detect(gray, None)\n    \n    # Draw keypoints on output\n    output_bgr = cv2.drawKeypoints(bgr, keypoints, None,\n                                    flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)\n    output_rgb = cv2.cvtColor(output_bgr, cv2.COLOR_BGR2RGB)\n    output_float = output_rgb.astype(np.float32) / 255.0\n    output_rgba = np.dstack([output_float, frame[:,:,3]])\n    \n    scriptOp.copyNumpyArray(output_rgba)\n    \n    # Store keypoints for CHOP use\n    scriptOp.store('keypoints', [(kp.pt[0], kp.pt[1], kp.response) for kp in keypoints])\n\n\n# --- Background Subtraction (MOG2) ---\n_bg_subtractor = None\n\ndef init_bg_subtractor():\n    global _bg_subtractor\n    import cv2\n    _bg_subtractor = cv2.createBackgroundSubtractorMOG2(\n        history=500, varThreshold=16, detectShadows=True\n    )\n\ndef cook_bg_subtraction(scriptOp):\n    global _bg_subtractor\n    if _bg_subtractor is None:\n        init_bg_subtractor()\n    \n    if not scriptOp.inputs: return\n    frame = scriptOp.inputs[0].numpyArray(delayed=False)\n    if frame is None: return\n    \n    import cv2\n    rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n    bgr = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2BGR)\n    \n    fg_mask = _bg_subtractor.apply(bgr)\n    fg_float = fg_mask.astype(np.float32) / 255.0\n    output = np.stack([fg_float, fg_float, fg_float, np.ones_like(fg_float)], axis=-1)\n    \n    scriptOp.copyNumpyArray(output)\n\n\n# --- Optical Flow (Dense, Farneback) ---\n_prev_gray = None\n\ndef cook_optical_flow(scriptOp):\n    global _prev_gray\n    if not scriptOp.inputs: return\n    \n    frame = scriptOp.inputs[0].numpyArray(delayed=False)\n    if frame is None: return\n    \n    import cv2\n    rgb_uint8 = (frame[:,:,:3] * 255).astype(np.uint8)\n    gray = cv2.cvtColor(rgb_uint8, cv2.COLOR_RGB2GRAY)\n    \n    if _prev_gray is None:\n        _prev_gray = gray\n        return\n    \n    flow = cv2.calcOpticalFlowFarneback(\n        _prev_gray, gray, None,\n        pyr_scale=0.5, levels=3, winsize=15,\n        iterations=3, poly_n=5, poly_sigma=1.2, flags=0\n    )\n    _prev_gray = gray\n    \n    # flow shape: (H, W, 2) — x and y flow vectors\n    # Convert to HSV for visualization\n    mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1])\n    hsv = np.zeros((*gray.shape, 3), dtype=np.uint8)\n    hsv[...,0] = ang * 180 / np.pi / 2  # Hue = direction\n    hsv[...,1] = 255\n    hsv[...,2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)\n    rgb_out = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)\n    \n    output = rgb_out.astype(np.float32) / 255.0\n    output_rgba = np.dstack([output, np.ones(output.shape[:2], dtype=np.float32)])\n    scriptOp.copyNumpyArray(output_rgba)\n    \n    # Store flow statistics\n    scriptOp.store('flow_magnitude_mean', float(mag.mean()))\n    scriptOp.store('flow_magnitude_max', float(mag.max()))"
      }
    }
  ],
  "resources": [
    { "title": "tdu.Dependency Documentation", "url": "https://docs.derivative.ca/Dependency" },
    { "title": "NumPy Documentation", "url": "https://numpy.org/doc/stable/" },
    { "title": "SciPy Signal Processing", "url": "https://docs.scipy.org/doc/scipy/reference/signal.html" },
    { "title": "OpenCV Python Tutorials", "url": "https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html" },
    { "title": "Python asyncio Documentation", "url": "https://docs.python.org/3/library/asyncio.html" }
  ]
}
