3 Commits
Author SHA1 Message Date
Gitea Actions 0ee99ab2cc docs: deploy from 66feb91821 2026-07-04 13:19:43 +00:00
Gitea Actions 904f80af6e docs: deploy from a4de64ea04 2026-06-28 10:06:52 +00:00
Gitea Actions b278824b6b docs: deploy from 7c6a8be2b7 2026-06-20 08:00:33 +00:00
126 changed files with 18889 additions and 12906 deletions
-68
View File
@@ -1,68 +0,0 @@
name: '🧪 Test'
on:
push:
branches:
- master
- develop
paths-ignore:
- '**/*.md'
pull_request:
branches:
- master
- develop
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
test:
runs-on: linux/amd64
container:
image: gitea.tourolle.paris/dtourolle/kpnpp-builder:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
path: test-${{ github.run_id }}
- name: Cache FetchContent dependencies
uses: actions/cache@v3
with:
path: ~/.cmake/fetchcontent
key: cmake-fetchcontent-${{ hashFiles('**/CMakeLists.txt') }}
restore-keys: cmake-fetchcontent-
- name: Configure
working-directory: test-${{ github.run_id }}
run: |
cmake -S . -B build \
-G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DKPN_BUILD_TESTS=ON \
-DKPN_BUILD_EXAMPLES=OFF \
-DKPN_BUILD_PYTHON=ON \
-DFETCHCONTENT_BASE_DIR=$HOME/.cmake/fetchcontent
- name: Build
working-directory: test-${{ github.run_id }}
run: cmake --build build --parallel
- name: Run tests
working-directory: test-${{ github.run_id }}
run: |
cd build
ctest --output-on-failure --output-junit test-results.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-${{ github.run_id }}/build/test-results.xml
retention-days: 7
- name: Cleanup
if: always()
run: rm -rf test-${{ github.run_id }}
-27
View File
@@ -1,27 +0,0 @@
# Build output
build/
build_debug/
# Python
__pycache__/
*.py[cod]
*.pyd
*.pyo
*.egg-info/
dist/
*.egg
.venv/
venv/
# Editors
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Claude Code local settings
.claude/settings.local.json
View File
+830
View File
@@ -0,0 +1,830 @@
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="A C++20 Kahn Process Network library">
<link rel="icon" href="/assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>KPN++</title>
<link rel="stylesheet" href="/assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="/assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("/",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="/." title="KPN++" class="md-header__button md-logo" aria-label="KPN++" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
KPN++
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.tourolle.paris/dtourolle/KPN" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
dtourolle/KPN
</div>
</a>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="/." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="/getting-started/" class="md-tabs__link">
Getting Started
</a>
</li>
<li class="md-tabs__item">
<a href="/nodes/" class="md-tabs__link">
Concepts
</a>
</li>
<li class="md-tabs__item">
<a href="/error-handling/" class="md-tabs__link">
Error Handling & Events
</a>
</li>
<li class="md-tabs__item">
<a href="/static-network/" class="md-tabs__link">
Advanced
</a>
</li>
<li class="md-tabs__item">
<a href="/examples/" class="md-tabs__link">
Examples
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="/." title="KPN++" class="md-nav__button md-logo" aria-label="KPN++" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
KPN++
</label>
<div class="md-nav__source">
<a href="https://gitea.tourolle.paris/dtourolle/KPN" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
dtourolle/KPN
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="/." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/getting-started/" class="md-nav__link">
<span class="md-ellipsis">
Getting Started
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_3" >
<label class="md-nav__link" for="__nav_3" id="__nav_3_label" tabindex="0">
<span class="md-ellipsis">
Concepts
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_3">
<span class="md-nav__icon md-icon"></span>
Concepts
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="/nodes/" class="md-nav__link">
<span class="md-ellipsis">
Nodes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/network/" class="md-nav__link">
<span class="md-ellipsis">
Networks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/channels/" class="md-nav__link">
<span class="md-ellipsis">
Channels
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="/error-handling/" class="md-nav__link">
<span class="md-ellipsis">
Error Handling & Events
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" >
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="0">
<span class="md-ellipsis">
Advanced
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Advanced
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="/static-network/" class="md-nav__link">
<span class="md-ellipsis">
Static Networks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/shared-resource/" class="md-nav__link">
<span class="md-ellipsis">
Shared Resources
</span>
</a>
</li>
<li class="md-nav__item">
<a href="/fanout/" class="md-nav__link">
<span class="md-ellipsis">
Fan-out & Routing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="/examples/" class="md-nav__link">
<span class="md-ellipsis">
Examples
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1>404 - Not found</h1>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "/", "features": ["navigation.tabs", "navigation.sections", "navigation.top", "content.code.copy", "content.code.annotate"], "search": "/assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="/assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>
-88
View File
@@ -1,88 +0,0 @@
cmake_minimum_required(VERSION 3.21)
project(kpnpp VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(KPN_BUILD_TESTS "Build tests" ON)
option(KPN_BUILD_PYTHON "Build Python bindings (requires nanobind)" ON)
option(KPN_BUILD_EXAMPLES "Build examples" ON)
option(KPN_WEB_DEBUG "Enable web debug UI (cpp-httplib)" OFF)
# ── Core library (header-only) ────────────────────────────────────────────────
add_library(kpn INTERFACE)
target_include_directories(kpn INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(kpn INTERFACE cxx_std_20)
# Threads required by node/channel implementation
find_package(Threads REQUIRED)
target_link_libraries(kpn INTERFACE Threads::Threads)
# ── Web debug UI (optional) ───────────────────────────────────────────────────
if(KPN_WEB_DEBUG)
include(FetchContent)
FetchContent_Declare(
cpp-httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.18.0
)
FetchContent_MakeAvailable(cpp-httplib)
# httplib made available but NOT forced onto kpn interface — targets opt in
# by defining KPN_WEB_DEBUG=1 and linking httplib::httplib themselves.
# This prevents tests and other examples from pulling in the HTTP server.
endif()
# Convenience function for targets that want web debug
function(kpn_target_enable_web_debug target)
target_compile_definitions(${target} PRIVATE KPN_WEB_DEBUG=1)
target_link_libraries(${target} PRIVATE httplib::httplib)
endfunction()
# ── Tests ─────────────────────────────────────────────────────────────────────
if(KPN_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# ── Benchmarks ────────────────────────────────────────────────────────────────
option(KPN_BUILD_BENCHMARKS "Build benchmarks" OFF)
if(KPN_BUILD_BENCHMARKS)
add_subdirectory(benchmarks)
endif()
# ── Python bindings ───────────────────────────────────────────────────────────
if(KPN_BUILD_PYTHON)
find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED)
find_package(nanobind CONFIG QUIET)
if(NOT nanobind_FOUND)
# Fall back to FetchContent if nanobind not installed system-wide
include(FetchContent)
FetchContent_Declare(
nanobind
GIT_REPOSITORY https://github.com/wjakob/nanobind.git
GIT_TAG v2.12.0
)
FetchContent_MakeAvailable(nanobind)
endif()
add_subdirectory(python)
endif()
# ── Examples ──────────────────────────────────────────────────────────────────
if(KPN_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
# ── Docs (README generation) ──────────────────────────────────────────────────
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_FOUND)
add_custom_target(docs
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/render_readme.py
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Rendering README.md from README.md.in"
VERBATIM
)
endif()
-19
View File
@@ -1,19 +0,0 @@
# KPN++ Builder Image
# Pre-built image with GCC, CMake, Ninja, and Python dev headers for building and testing KPN++
# Build: docker build -f Dockerfile.builder -t gitea.tourolle.paris/dtourolle/kpnpp-builder:latest .
# Push: docker push gitea.tourolle.paris/dtourolle/kpnpp-builder:latest
FROM gcc:14
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake \
ninja-build \
python3 \
python3-dev \
python3-pip \
git \
ca-certificates \
nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
-580
View File
@@ -1,580 +0,0 @@
# KPN++
A C++20 Kahn Process Network (KPN) library. Each node wraps a function and runs in its own thread, communicating with downstream nodes via bounded FIFO channels. Includes Python bindings via nanobind.
---
## Requirements
| Dependency | Version | Notes |
|---|---|---|
| CMake | ≥ 3.21 | |
| C++ compiler | GCC ≥ 11, Clang ≥ 13, MSVC 19.29 | C++20 required |
| Threads | system | `find_package(Threads)` |
| nanobind | ≥ 2.1 | auto-fetched if not installed; Python ≥ 3.8 |
| Catch2 | v3 | auto-fetched for tests |
| Google Test | v1.14 | auto-fetched for tests |
| OpenCV | ≥ 4 | optional; only for example 09 |
---
## Build
```bash
cmake -B build -DKPN_BUILD_PYTHON=OFF # core + tests + C++ examples
cmake --build build --parallel
ctest --test-dir build
```
Enable Python bindings (requires nanobind and Python dev headers):
```bash
cmake -B build -DKPN_BUILD_PYTHON=ON
cmake --build build --parallel
```
Disable examples:
```bash
cmake -B build -DKPN_BUILD_EXAMPLES=OFF
```
---
## Core Concepts
### Nodes
A node wraps any callable. Its input types are taken from the function's parameter list; its output types from the return type. Multi-output nodes return `std::tuple<...>`.
```cpp
#include <kpn/kpn.hpp>
using namespace kpn;
```
Source, transform, and sink — from [`examples/01_hello_pipeline/main.cpp`](examples/01_hello_pipeline/main.cpp):
```cpp
static int produce() { return 42; }
static int double_it(int x) { return x * 2; }
static void print_it(int x) { std::cout << "result: " << x << '\n'; }
```
Multi-output node returning a tuple — from [`examples/03_multi_output/main.cpp`](examples/03_multi_output/main.cpp):
```cpp
// Multi-output: returns (key, value) as a tuple — KPN++ routes each element
// to its own output port automatically.
static std::tuple<std::string, std::string> parse(std::string kv) {
auto sep = kv.find('=');
if (sep == std::string::npos) return {kv, ""};
return {kv.substr(0, sep), kv.substr(sep + 1)};
}
```
### Creating Nodes
**Index-only ports** (from [`examples/01_hello_pipeline/main.cpp`](examples/01_hello_pipeline/main.cpp)):
```cpp
auto src = make_node<produce>(5);
auto dbl = make_node<double_it>(5);
auto sink = make_node<print_it>(5);
```
**Named ports** (from [`examples/02_named_ports/main.cpp`](examples/02_named_ports/main.cpp)):
```cpp
// tokenise: no inputs, one named output "words"
auto tok = make_node<tokenise>(out<"words">{}, 4);
// count_words: named input "words", named outputs "count" and "words"
auto cnt = make_node<count_words>(in<"words">{}, out<"count", "words">{}, 4);
// report: two named inputs
auto snk = make_node<report>(in<"count", "words">{}, 4);
```
**Multi-named output source** (from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp)):
```cpp
auto src = make_node<capture>(out<"colour","grey">{}, 8);
```
Port names are NTTP `fixed_string` values — resolved entirely at compile time, zero runtime cost.
### Building a Network
`Network` is **non-owning** — declare nodes first, then register them. Nodes must outlive the network.
From [`examples/02_named_ports/main.cpp`](examples/02_named_ports/main.cpp):
```cpp
Network net;
net.add("tok", tok)
.add("cnt", cnt)
.add("snk", snk)
.connect("tok", tok.template output<"words">(), "cnt", cnt.template input<"words">())
.connect("cnt", cnt.template output<"count">(), "snk", snk.template input<"count">())
.connect("cnt", cnt.template output<"words">(), "snk", snk.template input<"words">())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
net.stop();
```
`.build()` runs cycle detection — throws `NetworkCycleError` on cycles.
> **Named port syntax in template context:** when the node variable is `auto`-deduced, use `.template output<"name">()` and `.template input<"name">()` to help the parser.
### Channel Semantics
- **Bounded FIFO**: default capacity 5, configurable per-node at construction.
- **Blocking `pop()`**: consumer blocks until data is available (KPN semantics).
- **Throwing `push()`**: throws `ChannelOverflowError` if the channel is full and accepting.
- **Silent drop on disabled channel**: after `node.stop()`, its input channels are disabled — producers that push into them have the value silently dropped. No exception, no blocking.
- **Source throttling**: source nodes (no inputs) must sleep or yield to avoid overflowing downstream FIFOs. See example 09.
### Storage Policy
Large types (`sizeof > 8` or non-trivially-copyable) are stored as `std::shared_ptr<const T>` inside the channel — no copies, shared immutable ownership. Small trivially-copyable types are stored by value.
Override the policy for a specific type (from [`examples/04_storage_policy/main.cpp`](examples/04_storage_policy/main.cpp)):
```cpp
// Override: store Tag by value despite being a struct
// (it's trivially copyable and small — this just makes the policy explicit)
template<>
struct kpn::channel_storage_policy<Tag> {
static constexpr bool by_value = true;
};
```
### Diagnostics & Error Handling
Custom diagnostics handler — fires on the watchdog interval (from [`examples/05_error_handling/main.cpp`](examples/05_error_handling/main.cpp)):
```cpp
// Custom diagnostics handler — fires on the watchdog interval.
// Print a concise one-liner rather than the full table.
net.set_diagnostics_handler([](const std::vector<NodeSnapshot>& nodes,
const std::vector<ChannelSnapshot>& channels) {
std::cout << "[diag] ";
for (auto& n : nodes)
std::cout << n.name << "=" << n.throughput_fps << "fps ";
for (auto& c : channels)
std::cout << "channel fill=" << static_cast<int>(c.fill_pct()) << "% "
<< "overflows=" << c.overflows;
std::cout << '\n';
});
```
### Shutdown
`node.stop()` / `net.stop()`:
1. Sets `accepting_ = false` on all input channels (drops in-flight pushes silently).
2. Clears any queued items from those channels.
3. Unblocks any thread blocked on `pop()` (throws `ChannelClosedError` inside `run_loop`, which exits cleanly).
4. Joins the node thread.
---
## Named Ports — Design Notes
Port names use C++20 NTTP `fixed_string`. The deduction guide is required:
```cpp
template<std::size_t N>
fixed_string(const char (&)[N]) -> fixed_string<N>;
```
`fixed_string<4>` and `fixed_string<7>` are distinct types — `input<"img">()` and `input<"sigma">()` resolve to different template instantiations at compile time. Wrong names produce a `static_assert` at the call site with a readable message.
---
## Sub-Networks
`Network` implements `INode`, so it can be nested inside a larger `Network`:
```cpp
// Inner sub-network
Network pipe;
pipe.add("pre", pre_node)
.add("enh", enh_node)
.connect("pre", pre_node.output<0>(), "enh", enh_node.input<0>())
.expose_input("img", pre_node.input<0>())
.expose_output("result", enh_node.output<0>())
.build();
// Outer network
Network top;
top.add("pipe", pipe)
.add("sink", sink_node)
.connect("pipe", pipe.output<"result">(), "sink", sink_node.input<0>())
.build();
top.start();
```
---
## Display / GUI Nodes
**Do not wrap `imshow`/`waitKey` as a KPN node.** Qt and Wayland require these to run on the main thread (the thread that owns the event loop). Instead, derive from `MainThreadNode<>` — it owns the input channels, implements `INode`, and exposes a `step()` method to call on the main thread.
`DisplayNode` from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp):
```cpp
class DisplayNode : public kpn::MainThreadNode<DisplayNode,
kpn::in<"composite", "edges">,
cv::Mat, cv::Mat> {
public:
DisplayNode() : MainThreadNode(8) {
cv::namedWindow("Cell Shade", cv::WINDOW_NORMAL);
cv::namedWindow("Edge Mask", cv::WINDOW_NORMAL);
cv::resizeWindow("Cell Shade", 1280, 720);
cv::resizeWindow("Edge Mask", 640, 360);
}
~DisplayNode() { cv::destroyAllWindows(); }
bool operator()(cv::Mat composite, cv::Mat edges) {
cv::imshow("Cell Shade", composite);
cv::Mat edges_bgr;
cv::cvtColor(edges, edges_bgr, cv::COLOR_GRAY2BGR);
cv::imshow("Edge Mask", edges_bgr);
int key = cv::waitKey(1);
if (key == 'q' || key == 27) return false;
return window_open("Cell Shade") && window_open("Edge Mask");
}
private:
static bool window_open(const char* name) {
try { return cv::getWindowProperty(name, cv::WND_PROP_VISIBLE) >= 1; }
catch (const cv::Exception&) { return false; }
}
};
```
Wire it into the network and drive it from the main thread:
```cpp
net.start();
// Main thread drives display — imshow/waitKey stay on the GUI thread.
// step() returns false when operator() returns false (q pressed / window closed).
while (disp.step())
cv::waitKey(8); // yield event loop when no frame ready
net.stop();
```
---
## OpenCV Cell-Shading Example
Real-time cell-shading pipeline from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp).
**Source node** — returns two frames (colour + grey) as a tuple, routing them to separate downstream branches:
```cpp
static std::tuple<cv::Mat, cv::Mat> capture() {
constexpr int W = 640, H = 480;
static cv::VideoCapture cap;
static bool opened = false;
if (!opened) {
opened = true;
cap.open(0, cv::CAP_V4L2);
if (cap.isOpened()) {
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
} else {
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
}
}
cv::Mat frame;
if (cap.isOpened()) {
auto t0 = std::chrono::steady_clock::now();
cap >> frame;
auto elapsed = std::chrono::steady_clock::now() - t0;
if (elapsed < std::chrono::milliseconds(20))
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
} else {
static int tick = 0;
static cv::Mat grad = make_gradient(W, H);
++tick;
frame = grad.clone();
int r = 150 + (tick % 80) * 4;
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
}
return {frame.clone(), frame.clone()};
}
```
**Full network wiring:**
```cpp
auto src = make_node<capture> (out<"colour","grey">{}, 8);
auto gray_node = make_node<to_gray> (in<"bgr">{}, out<"gray">{}, 8);
auto edge_node = make_node<edges_fn> (in<"gray">{}, out<"edges">{}, 8);
auto quant = make_node<quantise> (in<"bgr">{}, out<"quantised">{}, 8);
auto comp = make_node<composite>(in<"edges","colour">{}, out<"result","edges">{}, 8);
// DisplayNode: two windows opened in constructor, step() drives main thread.
DisplayNode disp;
Network net;
net.add("src", src)
.add("gray", gray_node)
.add("edges", edge_node)
.add("quant", quant)
.add("comp", comp)
.add("display", disp)
.connect("src", src.template output<"colour">(), "quant", quant.template input<"bgr">())
.connect("quant", quant.template output<"quantised">(), "comp", comp.template input<"colour">())
.connect("src", src.template output<"grey">(), "gray", gray_node.template input<"bgr">())
.connect("gray", gray_node.template output<"gray">(), "edges", edge_node.template input<"gray">())
.connect("edges", edge_node.template output<"edges">(), "comp", comp.template input<"edges">())
.connect("comp", comp.template output<"result">(), "display", disp.template input<"composite">())
.connect("comp", comp.template output<"edges">(), "display", disp.template input<"edges">())
.build();
```
---
## Fan-Out (Multi-Output)
From [`examples/03_multi_output/main.cpp`](examples/03_multi_output/main.cpp) — one node fans out to two independent sinks via a tuple return:
```cpp
auto gen = make_node<generate>(out<"kv">{}, 4);
auto par = make_node<parse> (in<"kv">{}, out<"key", "value">{}, 4);
auto keys = make_node<print_key> (in<"key">{}, 4);
auto vals = make_node<print_value>(in<"value">{}, 4);
Network net;
net.add("gen", gen)
.add("par", par)
.add("keys", keys)
.add("vals", vals)
.connect("gen", gen.template output<"kv">(), "par", par.template input<"kv">())
.connect("par", par.template output<"key">(), "keys", keys.template input<"key">())
.connect("par", par.template output<"value">(), "vals", vals.template input<"value">())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(600));
net.stop();
```
---
## Python Bindings
> Python bindings are scaffolded but not yet fully implemented. See `python/kpn_python.cpp` and `include/kpn/python/bindings.hpp`.
A `PyNetwork` is constructed from a closed list of C++ node types. The variant of all port types is derived at compile time — no runtime type registration needed.
**GIL rules (non-negotiable):**
- Acquire the GIL only for the duration of a Python callable invocation.
- Release the GIL before any blocking channel operation (`pop()`, `push()`, `net.read()`, `net.write()`).
Violating the second rule deadlocks.
---
## Examples
| Example | What it shows |
|---|---|
| `01_hello_pipeline` | Linear pipeline, index-based port wiring |
| `02_named_ports` | `in<>`/`out<>` name tags, named port access |
| `03_multi_output` | Tuple-returning node, per-element sub-port routing |
| `04_storage_policy` | `channel_storage_policy` default and specialisation |
| `05_error_handling` | `ChannelOverflowError`, `ErrorHandler` |
| `06_watchdog` | Watchdog interval, stall detection |
| `07_python_network` | PyNetwork, pure Python node *(pending)* |
| `08_python_subport` | `net.read`, `net.write`, sub-port tap *(pending)* |
| `09_opencv_cellshade` | Real-time cell-shading on webcam/pattern; requires OpenCV ≥ 4 |
Run the cell-shading example:
```bash
./build/examples/09_opencv_cellshade
# Press 'q' or close the window to stop.
# Falls back to an animated synthetic pattern if no webcam is found.
```
---
## Performance
Measured on Linux (x86-64, `-O3 -march=native`) with `benchmarks/bench_pipeline`.
Each topology pushes N items through the graph; `overhead_us/item` strips out the
per-node compute time to isolate framework cost.
Overhead formula: `(elapsed (N + depth 1) × work_us) / N` removes the expected
pipeline-fill cost so the number reflects pure framework latency.
### Baseline overhead (private pools, 100 µs/node)
| Topology | items/sec | overhead µs/item |
|---|---|---|
| chain depth-1 | 9 797 | ~2 |
| chain depth-4 | 9 448 | ~4 |
| chain depth-8 | 9 078 | ~7 |
| chain depth-16 | 7 004 | ~13 ← oversubscription |
| chain depth-32 | 4 179 | ~77 ← oversubscription |
| wide fanout-1 | 9 751 | ~3 |
| wide fanout-4 | 9 668 | ~3 |
| diamond (2×2) | 9 607 | ~4 |
Chain overhead is flat at **~27 µs/hop** for depths within the machine's core count,
then rises once threads compete for CPU. Wide and diamond topologies add no measurable
overhead as fanout increases — all branches run in parallel.
### Scheduling modes
`Node<>` gives each node a private `ThreadPool(1)`. `PoolNode<>` lets multiple
nodes share one pool. The right choice depends on the graph shape:
| Scenario | Recommended |
|---|---|
| Work per node < 100 µs, deep chain | Private pools — lower per-hop latency |
| Work per node ≥ 100 µs, wide/diamond | Shared pool, `threads = hardware_concurrency` |
| Any graph, bounded thread count required | Shared pool, `threads ≥ max parallel nodes` |
A shared single-thread pool (`threads=1`) fully serialises the graph — throughput
divides by depth for chains and by width for fanout topologies. A shared pool with
`threads ≥ max_concurrent_nodes` matches private-pool throughput while keeping the
OS thread count bounded.
### vs. TBB flow graph
Benchmarked against `tbb::flow::function_node<int,int>` (serial concurrency) with
`tbb::flow::broadcast_node<int>` for fanout. Run with `cmake -DKPN_BUILD_BENCHMARKS=ON`
— TBB benchmarks are included automatically when `find_package(TBB)` succeeds.
**Channel implementation:** lock-free SPSC ring buffer with `std::atomic::wait/notify_one`
(C++20 portable futex) plus a configurable spin-before-sleep window (default ~4 µs).
Large types are stored as `shared_ptr<const T>` — fanout copies reference counts,
not data.
Overhead µs/item at **work_us = 10** (framework overhead dominates):
| Topology | KPN private | TBB |
|---|---|---|
| chain depth-1 | 1.7 | **1.4** |
| chain depth-4 | 2.5 | **2.2** |
| chain depth-8 | **3.0** | 3.6 |
| chain depth-16 | **9.3** | 13.0 |
| chain depth-32 | 23.2 | **14.2** |
| wide fanout-4 | 2.5 | **1.4** |
| diamond (2×2) | 3.4 | **1.9** |
Overhead µs/item at **work_us = 100** (moderate compute, KPN wins):
| Topology | KPN private | TBB |
|---|---|---|
| chain depth-1 | **2.1** | 3.5 |
| chain depth-4 | **4.3** | 5.2 |
| chain depth-8 | **6.7** | 8.5 |
| chain depth-16 | **12.8** | 17.4 |
| chain depth-32 | **77** | 81 |
| wide fanout-4 | 3.4 | **1.9** |
| diamond (2×2) | **4.1** | 6.1 |
KPN private pools beat TBB for every chain and diamond topology at 100 µs/node, and
match TBB within ~20% at 10 µs/node for shallow chains. TBB retains an edge on wide
fanout (serial dispatch loop vs. work-stealing pool) and at extreme oversubscription
depths (chain-32 at 10 µs). The remaining gap at light work is the cost of
`atomic::wait` vs. TBB's continuously-spinning worker threads.
### vs. TBB — API
The function signature is the node. KPN infers input and output types automatically;
there is no graph object to manage.
**Single-output node:**
```cpp
// KPN — 1 line
int scale(int x) { return x * 2; }
// TBB — must state types, concurrency policy, and carry a graph reference
tbb::flow::function_node<int,int> n(g, tbb::flow::serial, [](int x){ return x*2; });
```
**Multi-output node:**
```cpp
// KPN — return a tuple
std::tuple<cv::Mat,cv::Mat> split(cv::Mat f) { return {f, f}; }
// TBB — multifunction_node + explicit try_put per port
tbb::flow::multifunction_node<cv::Mat, std::tuple<cv::Mat,cv::Mat>> n(
g, tbb::flow::serial,
[](cv::Mat f, auto& ports) {
std::get<0>(ports).try_put(f);
std::get<1>(ports).try_put(f);
});
```
**Named ports** — compile-time checked, zero runtime cost, not available in TBB:
```cpp
auto node = make_node<split>(in<"frame">{}, out<"colour","grey">{}, 5);
net.connect("cam", cam.output<"frame">(), "split", node.input<"frame">());
// ^^^^^^^ typo → compile error
```
| | KPN | TBB |
|---|---|---|
| Node definition | plain function | `function_node<In,Out>` + explicit types |
| Multi-output | `return std::tuple<A,B>` | `multifunction_node` + `try_put` × N |
| Named ports | `in<"name">` / `out<"name">` compile-time | none |
| Graph lifetime | none | `graph g` must outlive all nodes |
| Shutdown | `net.stop()` | `g.wait_for_all()` + manual |
| Python bindings | designed-in | none |
Build the benchmarks with:
```bash
cmake -B build -DKPN_BUILD_BENCHMARKS=ON
cmake --build build --target bench_pipeline
./build/benchmarks/bench_pipeline | tee results.csv
```
---
## Project Structure
```
include/kpn/
fixed_string.hpp — NTTP string, in<>/out<> tags, index_of
traits.hpp — function_traits, normalised_return_t, output_count_v
channel.hpp — Channel<T>, channel_storage_policy, exceptions
port.hpp — InputPort<N,I>, OutputPort<N,I>
node.hpp — Node<Func,in<...>,out<...>>, make_node, INode
network.hpp — Network (builder, cycle detection, watchdog)
variant_node.hpp — VariantNode, PythonConverter<T>, unique_types (Python layer)
python/
bindings.hpp — nanobind helpers, GIL rule documentation
kpn.hpp — umbrella header
src/
network.cpp — non-template Network implementation
tests/
test_fixed_string.cpp
test_traits.cpp
test_channel.cpp
test_node.cpp
test_network.cpp
python/
kpn_python.cpp — nanobind module entry point
examples/
01_hello_pipeline/ … 09_opencv_cellshade/
scripts/
render_readme.py — regenerates README.md from README.md.in
```
-402
View File
@@ -1,402 +0,0 @@
# KPN++
A C++20 Kahn Process Network (KPN) library. Each node wraps a function and runs in its own thread, communicating with downstream nodes via bounded FIFO channels. Includes Python bindings via nanobind.
---
## Requirements
| Dependency | Version | Notes |
|---|---|---|
| CMake | ≥ 3.21 | |
| C++ compiler | GCC ≥ 11, Clang ≥ 13, MSVC 19.29 | C++20 required |
| Threads | system | `find_package(Threads)` |
| nanobind | ≥ 2.1 | auto-fetched if not installed; Python ≥ 3.8 |
| Catch2 | v3 | auto-fetched for tests |
| Google Test | v1.14 | auto-fetched for tests |
| OpenCV | ≥ 4 | optional; only for example 09 |
---
## Build
```bash
cmake -B build -DKPN_BUILD_PYTHON=OFF # core + tests + C++ examples
cmake --build build --parallel
ctest --test-dir build
```
Enable Python bindings (requires nanobind and Python dev headers):
```bash
cmake -B build -DKPN_BUILD_PYTHON=ON
cmake --build build --parallel
```
Disable examples:
```bash
cmake -B build -DKPN_BUILD_EXAMPLES=OFF
```
---
## Core Concepts
### Nodes
A node wraps any callable. Its input types are taken from the function's parameter list; its output types from the return type. Multi-output nodes return `std::tuple<...>`.
```cpp
#include <kpn/kpn.hpp>
using namespace kpn;
```
Source, transform, and sink — from [`examples/01_hello_pipeline/main.cpp`](examples/01_hello_pipeline/main.cpp):
<!-- @snippet examples/01_hello_pipeline/main.cpp basic_node_fns -->
Multi-output node returning a tuple — from [`examples/03_multi_output/main.cpp`](examples/03_multi_output/main.cpp):
<!-- @snippet examples/03_multi_output/main.cpp multi_output_fn -->
### Creating Nodes
**Index-only ports** (from [`examples/01_hello_pipeline/main.cpp`](examples/01_hello_pipeline/main.cpp)):
<!-- @snippet examples/01_hello_pipeline/main.cpp index_only_nodes -->
**Named ports** (from [`examples/02_named_ports/main.cpp`](examples/02_named_ports/main.cpp)):
<!-- @snippet examples/02_named_ports/main.cpp named_port_creation -->
**Multi-named output source** (from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp)):
```cpp
auto src = make_node<capture>(out<"colour","grey">{}, 8);
```
Port names are NTTP `fixed_string` values — resolved entirely at compile time, zero runtime cost.
### Building a Network
`Network` is **non-owning** — declare nodes first, then register them. Nodes must outlive the network.
From [`examples/02_named_ports/main.cpp`](examples/02_named_ports/main.cpp):
<!-- @snippet examples/02_named_ports/main.cpp named_port_network -->
`.build()` runs cycle detection — throws `NetworkCycleError` on cycles.
> **Named port syntax in template context:** when the node variable is `auto`-deduced, use `.template output<"name">()` and `.template input<"name">()` to help the parser.
### Channel Semantics
- **Bounded FIFO**: default capacity 5, configurable per-node at construction.
- **Blocking `pop()`**: consumer blocks until data is available (KPN semantics).
- **Throwing `push()`**: throws `ChannelOverflowError` if the channel is full and accepting.
- **Silent drop on disabled channel**: after `node.stop()`, its input channels are disabled — producers that push into them have the value silently dropped. No exception, no blocking.
- **Source throttling**: source nodes (no inputs) must sleep or yield to avoid overflowing downstream FIFOs. See example 09.
### Storage Policy
Large types (`sizeof > 8` or non-trivially-copyable) are stored as `std::shared_ptr<const T>` inside the channel — no copies, shared immutable ownership. Small trivially-copyable types are stored by value.
Override the policy for a specific type (from [`examples/04_storage_policy/main.cpp`](examples/04_storage_policy/main.cpp)):
<!-- @snippet examples/04_storage_policy/main.cpp storage_policy_spec -->
### Diagnostics & Error Handling
Custom diagnostics handler — fires on the watchdog interval (from [`examples/05_error_handling/main.cpp`](examples/05_error_handling/main.cpp)):
<!-- @snippet examples/05_error_handling/main.cpp diagnostics_handler -->
### Shutdown
`node.stop()` / `net.stop()`:
1. Sets `accepting_ = false` on all input channels (drops in-flight pushes silently).
2. Clears any queued items from those channels.
3. Unblocks any thread blocked on `pop()` (throws `ChannelClosedError` inside `run_loop`, which exits cleanly).
4. Joins the node thread.
---
## Named Ports — Design Notes
Port names use C++20 NTTP `fixed_string`. The deduction guide is required:
```cpp
template<std::size_t N>
fixed_string(const char (&)[N]) -> fixed_string<N>;
```
`fixed_string<4>` and `fixed_string<7>` are distinct types — `input<"img">()` and `input<"sigma">()` resolve to different template instantiations at compile time. Wrong names produce a `static_assert` at the call site with a readable message.
---
## Sub-Networks
`Network` implements `INode`, so it can be nested inside a larger `Network`:
```cpp
// Inner sub-network
Network pipe;
pipe.add("pre", pre_node)
.add("enh", enh_node)
.connect("pre", pre_node.output<0>(), "enh", enh_node.input<0>())
.expose_input("img", pre_node.input<0>())
.expose_output("result", enh_node.output<0>())
.build();
// Outer network
Network top;
top.add("pipe", pipe)
.add("sink", sink_node)
.connect("pipe", pipe.output<"result">(), "sink", sink_node.input<0>())
.build();
top.start();
```
---
## Display / GUI Nodes
**Do not wrap `imshow`/`waitKey` as a KPN node.** Qt and Wayland require these to run on the main thread (the thread that owns the event loop). Instead, derive from `MainThreadNode<>` — it owns the input channels, implements `INode`, and exposes a `step()` method to call on the main thread.
`DisplayNode` from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp):
<!-- @snippet examples/09_opencv_cellshade/main.cpp display_node -->
Wire it into the network and drive it from the main thread:
<!-- @snippet examples/09_opencv_cellshade/main.cpp main_thread_step -->
---
## OpenCV Cell-Shading Example
Real-time cell-shading pipeline from [`examples/09_opencv_cellshade/main.cpp`](examples/09_opencv_cellshade/main.cpp).
**Source node** — returns two frames (colour + grey) as a tuple, routing them to separate downstream branches:
<!-- @snippet examples/09_opencv_cellshade/main.cpp capture_fn -->
**Full network wiring:**
<!-- @snippet examples/09_opencv_cellshade/main.cpp opencv_network -->
---
## Fan-Out (Multi-Output)
From [`examples/03_multi_output/main.cpp`](examples/03_multi_output/main.cpp) — one node fans out to two independent sinks via a tuple return:
<!-- @snippet examples/03_multi_output/main.cpp fanout_network -->
---
## Python Bindings
> Python bindings are scaffolded but not yet fully implemented. See `python/kpn_python.cpp` and `include/kpn/python/bindings.hpp`.
A `PyNetwork` is constructed from a closed list of C++ node types. The variant of all port types is derived at compile time — no runtime type registration needed.
**GIL rules (non-negotiable):**
- Acquire the GIL only for the duration of a Python callable invocation.
- Release the GIL before any blocking channel operation (`pop()`, `push()`, `net.read()`, `net.write()`).
Violating the second rule deadlocks.
---
## Examples
| Example | What it shows |
|---|---|
| `01_hello_pipeline` | Linear pipeline, index-based port wiring |
| `02_named_ports` | `in<>`/`out<>` name tags, named port access |
| `03_multi_output` | Tuple-returning node, per-element sub-port routing |
| `04_storage_policy` | `channel_storage_policy` default and specialisation |
| `05_error_handling` | `ChannelOverflowError`, `ErrorHandler` |
| `06_watchdog` | Watchdog interval, stall detection |
| `07_python_network` | PyNetwork, pure Python node *(pending)* |
| `08_python_subport` | `net.read`, `net.write`, sub-port tap *(pending)* |
| `09_opencv_cellshade` | Real-time cell-shading on webcam/pattern; requires OpenCV ≥ 4 |
Run the cell-shading example:
```bash
./build/examples/09_opencv_cellshade
# Press 'q' or close the window to stop.
# Falls back to an animated synthetic pattern if no webcam is found.
```
---
## Performance
Measured on Linux (x86-64, `-O3 -march=native`) with `benchmarks/bench_pipeline`.
Each topology pushes N items through the graph; `overhead_us/item` strips out the
per-node compute time to isolate framework cost.
Overhead formula: `(elapsed (N + depth 1) × work_us) / N` removes the expected
pipeline-fill cost so the number reflects pure framework latency.
### Baseline overhead (private pools, 100 µs/node)
| Topology | items/sec | overhead µs/item |
|---|---|---|
| chain depth-1 | 9 797 | ~2 |
| chain depth-4 | 9 448 | ~4 |
| chain depth-8 | 9 078 | ~7 |
| chain depth-16 | 7 004 | ~13 ← oversubscription |
| chain depth-32 | 4 179 | ~77 ← oversubscription |
| wide fanout-1 | 9 751 | ~3 |
| wide fanout-4 | 9 668 | ~3 |
| diamond (2×2) | 9 607 | ~4 |
Chain overhead is flat at **~27 µs/hop** for depths within the machine's core count,
then rises once threads compete for CPU. Wide and diamond topologies add no measurable
overhead as fanout increases — all branches run in parallel.
### Scheduling modes
`Node<>` gives each node a private `ThreadPool(1)`. `PoolNode<>` lets multiple
nodes share one pool. The right choice depends on the graph shape:
| Scenario | Recommended |
|---|---|
| Work per node < 100 µs, deep chain | Private pools — lower per-hop latency |
| Work per node ≥ 100 µs, wide/diamond | Shared pool, `threads = hardware_concurrency` |
| Any graph, bounded thread count required | Shared pool, `threads ≥ max parallel nodes` |
A shared single-thread pool (`threads=1`) fully serialises the graph — throughput
divides by depth for chains and by width for fanout topologies. A shared pool with
`threads ≥ max_concurrent_nodes` matches private-pool throughput while keeping the
OS thread count bounded.
### vs. TBB flow graph
Benchmarked against `tbb::flow::function_node<int,int>` (serial concurrency) with
`tbb::flow::broadcast_node<int>` for fanout. Run with `cmake -DKPN_BUILD_BENCHMARKS=ON`
— TBB benchmarks are included automatically when `find_package(TBB)` succeeds.
**Channel implementation:** lock-free SPSC ring buffer with `std::atomic::wait/notify_one`
(C++20 portable futex) plus a configurable spin-before-sleep window (default ~4 µs).
Large types are stored as `shared_ptr<const T>` — fanout copies reference counts,
not data.
Overhead µs/item at **work_us = 10** (framework overhead dominates):
| Topology | KPN private | TBB |
|---|---|---|
| chain depth-1 | 1.7 | **1.4** |
| chain depth-4 | 2.5 | **2.2** |
| chain depth-8 | **3.0** | 3.6 |
| chain depth-16 | **9.3** | 13.0 |
| chain depth-32 | 23.2 | **14.2** |
| wide fanout-4 | 2.5 | **1.4** |
| diamond (2×2) | 3.4 | **1.9** |
Overhead µs/item at **work_us = 100** (moderate compute, KPN wins):
| Topology | KPN private | TBB |
|---|---|---|
| chain depth-1 | **2.1** | 3.5 |
| chain depth-4 | **4.3** | 5.2 |
| chain depth-8 | **6.7** | 8.5 |
| chain depth-16 | **12.8** | 17.4 |
| chain depth-32 | **77** | 81 |
| wide fanout-4 | 3.4 | **1.9** |
| diamond (2×2) | **4.1** | 6.1 |
KPN private pools beat TBB for every chain and diamond topology at 100 µs/node, and
match TBB within ~20% at 10 µs/node for shallow chains. TBB retains an edge on wide
fanout (serial dispatch loop vs. work-stealing pool) and at extreme oversubscription
depths (chain-32 at 10 µs). The remaining gap at light work is the cost of
`atomic::wait` vs. TBB's continuously-spinning worker threads.
### vs. TBB — API
The function signature is the node. KPN infers input and output types automatically;
there is no graph object to manage.
**Single-output node:**
```cpp
// KPN — 1 line
int scale(int x) { return x * 2; }
// TBB — must state types, concurrency policy, and carry a graph reference
tbb::flow::function_node<int,int> n(g, tbb::flow::serial, [](int x){ return x*2; });
```
**Multi-output node:**
```cpp
// KPN — return a tuple
std::tuple<cv::Mat,cv::Mat> split(cv::Mat f) { return {f, f}; }
// TBB — multifunction_node + explicit try_put per port
tbb::flow::multifunction_node<cv::Mat, std::tuple<cv::Mat,cv::Mat>> n(
g, tbb::flow::serial,
[](cv::Mat f, auto& ports) {
std::get<0>(ports).try_put(f);
std::get<1>(ports).try_put(f);
});
```
**Named ports** — compile-time checked, zero runtime cost, not available in TBB:
```cpp
auto node = make_node<split>(in<"frame">{}, out<"colour","grey">{}, 5);
net.connect("cam", cam.output<"frame">(), "split", node.input<"frame">());
// ^^^^^^^ typo → compile error
```
| | KPN | TBB |
|---|---|---|
| Node definition | plain function | `function_node<In,Out>` + explicit types |
| Multi-output | `return std::tuple<A,B>` | `multifunction_node` + `try_put` × N |
| Named ports | `in<"name">` / `out<"name">` compile-time | none |
| Graph lifetime | none | `graph g` must outlive all nodes |
| Shutdown | `net.stop()` | `g.wait_for_all()` + manual |
| Python bindings | designed-in | none |
Build the benchmarks with:
```bash
cmake -B build -DKPN_BUILD_BENCHMARKS=ON
cmake --build build --target bench_pipeline
./build/benchmarks/bench_pipeline | tee results.csv
```
---
## Project Structure
```
include/kpn/
fixed_string.hpp — NTTP string, in<>/out<> tags, index_of
traits.hpp — function_traits, normalised_return_t, output_count_v
channel.hpp — Channel<T>, channel_storage_policy, exceptions
port.hpp — InputPort<N,I>, OutputPort<N,I>
node.hpp — Node<Func,in<...>,out<...>>, make_node, INode
network.hpp — Network (builder, cycle detection, watchdog)
variant_node.hpp — VariantNode, PythonConverter<T>, unique_types (Python layer)
python/
bindings.hpp — nanobind helpers, GIL rule documentation
kpn.hpp — umbrella header
src/
network.cpp — non-template Network implementation
tests/
test_fixed_string.cpp
test_traits.cpp
test_channel.cpp
test_node.cpp
test_network.cpp
python/
kpn_python.cpp — nanobind module entry point
examples/
01_hello_pipeline/ … 09_opencv_cellshade/
scripts/
render_readme.py — regenerates README.md from README.md.in
```
-1373
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
/*!
* Lunr languages, `Danish` language
* https://github.com/MihaiValentin/lunr-languages
*
* Copyright 2014, Mihai Valentin
* http://www.mozilla.org/MPL/
*/
/*!
* based on
* Snowball JavaScript Library v0.3
* http://code.google.com/p/urim/
* http://snowball.tartarus.org/
*
* Copyright 2010, Oleg Mazko
* http://www.mozilla.org/MPL/
*/
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.da=function(){this.pipeline.reset(),this.pipeline.add(e.da.trimmer,e.da.stopWordFilter,e.da.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.da.stemmer))},e.da.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.da.trimmer=e.trimmerSupport.generateTrimmer(e.da.wordCharacters),e.Pipeline.registerFunction(e.da.trimmer,"trimmer-da"),e.da.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){var e,r=f.cursor+3;if(d=f.limit,0<=r&&r<=f.limit){for(a=r;;){if(e=f.cursor,f.in_grouping(w,97,248)){f.cursor=e;break}if(f.cursor=e,e>=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d<a&&(d=a)}}function n(){var e,r;if(f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Z-zA-0-9-",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hy=function(){this.pipeline.reset(),this.pipeline.add(e.hy.trimmer,e.hy.stopWordFilter)},e.hy.wordCharacters="[A-Za-z԰-֏ff-ﭏ]",e.hy.trimmer=e.trimmerSupport.generateTrimmer(e.hy.wordCharacters),e.Pipeline.registerFunction(e.hy.trimmer,"trimmer-hy"),e.hy.stopWordFilter=e.generateStopWordFilter("դու և եք էիր էիք հետո նաև նրանք որը վրա է որ պիտի են այս մեջ ն իր ու ի այդ որոնք այն կամ էր մի ես համար այլ իսկ էին ենք հետ ին թ էինք մենք նրա նա դուք եմ էի ըստ որպես ում".split(" ")),e.Pipeline.registerFunction(e.hy.stopWordFilter,"stopWordFilter-hy"),e.hy.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}(),e.Pipeline.registerFunction(e.hy.stemmer,"stemmer-hy")}});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n<p.length;n++)r?a.push(new e.Token(p[n],{position:[f,p[n].length],index:a.length})):a.push(p[n]),f+=p[n].length;l=c+1}return a},e.ja.stemmer=function(){return function(e){return e}}(),e.Pipeline.registerFunction(e.ja.stemmer,"stemmer-ja"),e.ja.wordCharacters="一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Z-zA-0-9-",e.ja.trimmer=e.trimmerSupport.generateTrimmer(e.ja.wordCharacters),e.Pipeline.registerFunction(e.ja.trimmer,"trimmer-ja"),e.ja.stopWordFilter=e.generateStopWordFilter("これ それ あれ この その あの ここ そこ あそこ こちら どこ だれ なに なん 何 私 貴方 貴方方 我々 私達 あの人 あのかた 彼女 彼 です あります おります います は が の に を で え から まで より も どの と し それで しかし".split(" ")),e.Pipeline.registerFunction(e.ja.stopWordFilter,"stopWordFilter-ja"),e.jp=e.ja,e.Pipeline.registerFunction(e.jp.stemmer,"stemmer-jp"),e.Pipeline.registerFunction(e.jp.trimmer,"trimmer-jp"),e.Pipeline.registerFunction(e.jp.stopWordFilter,"stopWordFilter-jp")}});
+1
View File
@@ -0,0 +1 @@
module.exports=require("./lunr.ja");
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.kn=function(){this.pipeline.reset(),this.pipeline.add(e.kn.trimmer,e.kn.stopWordFilter,e.kn.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.kn.stemmer))},e.kn.wordCharacters="ಀ-಄ಅ-ಔಕ-ಹಾ-ೌ಼-ಽೕ-ೖೝ-ೞೠ-ೡೢ-ೣ೤೥೦-೯ೱ-ೳ",e.kn.trimmer=e.trimmerSupport.generateTrimmer(e.kn.wordCharacters),e.Pipeline.registerFunction(e.kn.trimmer,"trimmer-kn"),e.kn.stopWordFilter=e.generateStopWordFilter("ಮತ್ತು ಈ ಒಂದು ರಲ್ಲಿ ಹಾಗೂ ಎಂದು ಅಥವಾ ಇದು ರ ಅವರು ಎಂಬ ಮೇಲೆ ಅವರ ತನ್ನ ಆದರೆ ತಮ್ಮ ನಂತರ ಮೂಲಕ ಹೆಚ್ಚು ನ ಆ ಕೆಲವು ಅನೇಕ ಎರಡು ಹಾಗು ಪ್ರಮುಖ ಇದನ್ನು ಇದರ ಸುಮಾರು ಅದರ ಅದು ಮೊದಲ ಬಗ್ಗೆ ನಲ್ಲಿ ರಂದು ಇತರ ಅತ್ಯಂತ ಹೆಚ್ಚಿನ ಸಹ ಸಾಮಾನ್ಯವಾಗಿ ನೇ ಹಲವಾರು ಹೊಸ ದಿ ಕಡಿಮೆ ಯಾವುದೇ ಹೊಂದಿದೆ ದೊಡ್ಡ ಅನ್ನು ಇವರು ಪ್ರಕಾರ ಇದೆ ಮಾತ್ರ ಕೂಡ ಇಲ್ಲಿ ಎಲ್ಲಾ ವಿವಿಧ ಅದನ್ನು ಹಲವು ರಿಂದ ಕೇವಲ ದ ದಕ್ಷಿಣ ಗೆ ಅವನ ಅತಿ ನೆಯ ಬಹಳ ಕೆಲಸ ಎಲ್ಲ ಪ್ರತಿ ಇತ್ಯಾದಿ ಇವು ಬೇರೆ ಹೀಗೆ ನಡುವೆ ಇದಕ್ಕೆ ಎಸ್ ಇವರ ಮೊದಲು ಶ್ರೀ ಮಾಡುವ ಇದರಲ್ಲಿ ರೀತಿಯ ಮಾಡಿದ ಕಾಲ ಅಲ್ಲಿ ಮಾಡಲು ಅದೇ ಈಗ ಅವು ಗಳು ಎ ಎಂಬುದು ಅವನು ಅಂದರೆ ಅವರಿಗೆ ಇರುವ ವಿಶೇಷ ಮುಂದೆ ಅವುಗಳ ಮುಂತಾದ ಮೂಲ ಬಿ ಮೀ ಒಂದೇ ಇನ್ನೂ ಹೆಚ್ಚಾಗಿ ಮಾಡಿ ಅವರನ್ನು ಇದೇ ಯ ರೀತಿಯಲ್ಲಿ ಜೊತೆ ಅದರಲ್ಲಿ ಮಾಡಿದರು ನಡೆದ ಆಗ ಮತ್ತೆ ಪೂರ್ವ ಆತ ಬಂದ ಯಾವ ಒಟ್ಟು ಇತರೆ ಹಿಂದೆ ಪ್ರಮಾಣದ ಗಳನ್ನು ಕುರಿತು ಯು ಆದ್ದರಿಂದ ಅಲ್ಲದೆ ನಗರದ ಮೇಲಿನ ಏಕೆಂದರೆ ರಷ್ಟು ಎಂಬುದನ್ನು ಬಾರಿ ಎಂದರೆ ಹಿಂದಿನ ಆದರೂ ಆದ ಸಂಬಂಧಿಸಿದ ಮತ್ತೊಂದು ಸಿ ಆತನ ".split(" ")),e.kn.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.kn.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var n=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(n).split("|")},e.Pipeline.registerFunction(e.kn.stemmer,"stemmer-kn"),e.Pipeline.registerFunction(e.kn.stopWordFilter,"stopWordFilter-kn")}});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){e.multiLanguage=function(){for(var t=Array.prototype.slice.call(arguments),i=t.join("-"),r="",n=[],s=[],p=0;p<t.length;++p)"en"==t[p]?(r+="\\w",n.unshift(e.stopWordFilter),n.push(e.stemmer),s.push(e.stemmer)):(r+=e[t[p]].wordCharacters,e[t[p]].stopWordFilter&&n.unshift(e[t[p]].stopWordFilter),e[t[p]].stemmer&&(n.push(e[t[p]].stemmer),s.push(e[t[p]].stemmer)));var o=e.trimmerSupport.generateTrimmer(r);return e.Pipeline.registerFunction(o,"lunr-multi-trimmer-"+i),n.unshift(o),function(){this.pipeline.reset(),this.pipeline.add.apply(this.pipeline,n),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add.apply(this.searchPipeline,s))}}}});
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
/*!
* Lunr languages, `Norwegian` language
* https://github.com/MihaiValentin/lunr-languages
*
* Copyright 2014, Mihai Valentin
* http://www.mozilla.org/MPL/
*/
/*!
* based on
* Snowball JavaScript Library v0.3
* http://code.google.com/p/urim/
* http://snowball.tartarus.org/
*
* Copyright 2010, Oleg Mazko
* http://www.mozilla.org/MPL/
*/
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a<s&&(a=s)}}function i(){var e,r,n;if(w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sa=function(){this.pipeline.reset(),this.pipeline.add(e.sa.trimmer,e.sa.stopWordFilter,e.sa.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sa.stemmer))},e.sa.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿ꣠-꣱ꣲ-ꣷ꣸-ꣻ꣼-ꣽꣾ-ꣿᆰ0-ᆰ9",e.sa.trimmer=e.trimmerSupport.generateTrimmer(e.sa.wordCharacters),e.Pipeline.registerFunction(e.sa.trimmer,"trimmer-sa"),e.sa.stopWordFilter=e.generateStopWordFilter('तथा अयम्‌ एकम्‌ इत्यस्मिन्‌ तथा तत्‌ वा अयम्‌ इत्यस्य ते आहूत उपरि तेषाम्‌ किन्तु तेषाम्‌ तदा इत्यनेन अधिकः इत्यस्य तत्‌ केचन बहवः द्वि तथा महत्वपूर्णः अयम्‌ अस्य विषये अयं अस्ति तत्‌ प्रथमः विषये इत्युपरि इत्युपरि इतर अधिकतमः अधिकः अपि सामान्यतया ठ इतरेतर नूतनम्‌ द न्यूनम्‌ कश्चित्‌ वा विशालः द सः अस्ति तदनुसारम् तत्र अस्ति केवलम्‌ अपि अत्र सर्वे विविधाः तत्‌ बहवः यतः इदानीम्‌ द दक्षिण इत्यस्मै तस्य उपरि नथ अतीव कार्यम्‌ सर्वे एकैकम्‌ इत्यादि। एते सन्ति उत इत्थम्‌ मध्ये एतदर्थं . स कस्य प्रथमः श्री. करोति अस्मिन् प्रकारः निर्मिता कालः तत्र कर्तुं समान अधुना ते सन्ति स एकः अस्ति सः अर्थात् तेषां कृते . स्थितम् विशेषः अग्रिम तेषाम्‌ समान स्रोतः ख म समान इदानीमपि अधिकतया करोतु ते समान इत्यस्य वीथी सह यस्मिन् कृतवान्‌ धृतः तदा पुनः पूर्वं सः आगतः किम्‌ कुल इतर पुरा मात्रा स विषये उ अतएव अपि नगरस्य उपरि यतः प्रतिशतं कतरः कालः साधनानि भूत तथापि जात सम्बन्धि अन्यत्‌ ग अतः अस्माकं स्वकीयाः अस्माकं इदानीं अन्तः इत्यादयः भवन्तः इत्यादयः एते एताः तस्य अस्य इदम् एते तेषां तेषां तेषां तान् तेषां तेषां तेषां समानः सः एकः च तादृशाः बहवः अन्ये च वदन्ति यत् कियत् कस्मै कस्मै यस्मै यस्मै यस्मै यस्मै न अतिनीचः किन्तु प्रथमं सम्पूर्णतया ततः चिरकालानन्तरं पुस्तकं सम्पूर्णतया अन्तः किन्तु अत्र वा इह इव श्रद्धाय अवशिष्यते परन्तु अन्ये वर्गाः सन्ति ते सन्ति शक्नुवन्ति सर्वे मिलित्वा सर्वे एकत्र"'.split(" ")),e.sa.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.sa.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var i=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(i).split("|")},e.Pipeline.registerFunction(e.sa.stemmer,"stemmer-sa"),e.Pipeline.registerFunction(e.sa.stopWordFilter,"stopWordFilter-sa")}});
@@ -0,0 +1 @@
!function(r,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(r.lunr)}(this,function(){return function(r){r.stemmerSupport={Among:function(r,t,i,s){if(this.toCharArray=function(r){for(var t=r.length,i=new Array(t),s=0;s<t;s++)i[s]=r.charCodeAt(s);return i},!r&&""!=r||!t&&0!=t||!i)throw"Bad Among initialisation: s:"+r+", substring_i: "+t+", result: "+i;this.s_size=r.length,this.s=this.toCharArray(r),this.substring_i=t,this.result=i,this.method=s},SnowballProgram:function(){var r;return{bra:0,ket:0,limit:0,cursor:0,limit_backward:0,setCurrent:function(t){r=t,this.cursor=0,this.limit=t.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},getCurrent:function(){var t=r;return r=null,t},in_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e>s||e<i)return this.cursor++,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e<i)return this.cursor--,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor+s)!=i.charCodeAt(s))return!1;return this.cursor+=t,!0},eq_s_b:function(t,i){if(this.cursor-this.limit_backward<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor-t+s)!=i.charCodeAt(s))return!1;return this.cursor-=t,!0},find_among:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=l;m<_.s_size;m++){if(n+l==u){f=-1;break}if(f=r.charCodeAt(n+l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=_.s_size-1-l;m>=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}});
+18
View File
@@ -0,0 +1,18 @@
/*!
* Lunr languages, `Swedish` language
* https://github.com/MihaiValentin/lunr-languages
*
* Copyright 2014, Mihai Valentin
* http://www.mozilla.org/MPL/
*/
/*!
* based on
* Snowball JavaScript Library v0.3
* http://code.google.com/p/urim/
* http://snowball.tartarus.org/
*
* Copyright 2010, Oleg Mazko
* http://www.mozilla.org/MPL/
*/
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o<a&&(o=a)}}function t(){var e,r=w.limit_backward;if(w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}});
+1
View File
@@ -0,0 +1 @@
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ta=function(){this.pipeline.reset(),this.pipeline.add(e.ta.trimmer,e.ta.stopWordFilter,e.ta.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ta.stemmer))},e.ta.wordCharacters="஀-உஊ-ஏஐ-ஙச-ட஠-னப-யர-ஹ஺-ிீ-௉ொ-௏ௐ-௙௚-௟௠-௩௪-௯௰-௹௺-௿a-zA-Z-zA-0-9-",e.ta.trimmer=e.trimmerSupport.generateTrimmer(e.ta.wordCharacters),e.Pipeline.registerFunction(e.ta.trimmer,"trimmer-ta"),e.ta.stopWordFilter=e.generateStopWordFilter("அங்கு அங்கே அது அதை அந்த அவர் அவர்கள் அவள் அவன் அவை ஆக ஆகவே ஆகையால் ஆதலால் ஆதலினால் ஆனாலும் ஆனால் இங்கு இங்கே இது இதை இந்த இப்படி இவர் இவர்கள் இவள் இவன் இவை இவ்வளவு உனக்கு உனது உன் உன்னால் எங்கு எங்கே எது எதை எந்த எப்படி எவர் எவர்கள் எவள் எவன் எவை எவ்வளவு எனக்கு எனது எனவே என் என்ன என்னால் ஏது ஏன் தனது தன்னால் தானே தான் நாங்கள் நாம் நான் நீ நீங்கள்".split(" ")),e.ta.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.ta.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.ta.stemmer,"stemmer-ta"),e.Pipeline.registerFunction(e.ta.stopWordFilter,"stopWordFilter-ta")}});
+1
View File
@@ -0,0 +1 @@
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.te=function(){this.pipeline.reset(),this.pipeline.add(e.te.trimmer,e.te.stopWordFilter,e.te.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.te.stemmer))},e.te.wordCharacters="ఀ-ఄఅ-ఔక-హా-ౌౕ-ౖౘ-ౚౠ-ౡౢ-ౣ౦-౯౸-౿఼ఽ్ౝ౷౤౥",e.te.trimmer=e.trimmerSupport.generateTrimmer(e.te.wordCharacters),e.Pipeline.registerFunction(e.te.trimmer,"trimmer-te"),e.te.stopWordFilter=e.generateStopWordFilter("అందరూ అందుబాటులో అడగండి అడగడం అడ్డంగా అనుగుణంగా అనుమతించు అనుమతిస్తుంది అయితే ఇప్పటికే ఉన్నారు ఎక్కడైనా ఎప్పుడు ఎవరైనా ఎవరో ఏ ఏదైనా ఏమైనప్పటికి ఒక ఒకరు కనిపిస్తాయి కాదు కూడా గా గురించి చుట్టూ చేయగలిగింది తగిన తర్వాత దాదాపు దూరంగా నిజంగా పై ప్రకారం ప్రక్కన మధ్య మరియు మరొక మళ్ళీ మాత్రమే మెచ్చుకో వద్ద వెంట వేరుగా వ్యతిరేకంగా సంబంధం".split(" ")),e.te.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.te.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.te.stemmer,"stemmer-te"),e.Pipeline.registerFunction(e.te.stopWordFilter,"stopWordFilter-te")}});
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.th=function(){this.pipeline.reset(),this.pipeline.add(e.th.trimmer),r?this.tokenizer=e.th.tokenizer:(e.tokenizer&&(e.tokenizer=e.th.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.th.tokenizer))},e.th.wordCharacters="[฀-๿]",e.th.trimmer=e.trimmerSupport.generateTrimmer(e.th.wordCharacters),e.Pipeline.registerFunction(e.th.trimmer,"trimmer-th");var t=e.wordcut;t.init(),e.th.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t):t});var n=i.toString().replace(/^\s+/,"");return t.cut(n).split("|")}}});
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}});
+1
View File
@@ -0,0 +1 @@
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r(require("@node-rs/jieba")):r()(e.lunr)}(this,function(e){return function(r,t){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i="2"==r.version[0];r.zh=function(){this.pipeline.reset(),this.pipeline.add(r.zh.trimmer,r.zh.stopWordFilter,r.zh.stemmer),i?this.tokenizer=r.zh.tokenizer:(r.tokenizer&&(r.tokenizer=r.zh.tokenizer),this.tokenizerFn&&(this.tokenizerFn=r.zh.tokenizer))},r.zh.tokenizer=function(n){if(!arguments.length||null==n||void 0==n)return[];if(Array.isArray(n))return n.map(function(e){return i?new r.Token(e.toLowerCase()):e.toLowerCase()});t&&e.load(t);var o=n.toString().trim().toLowerCase(),s=[];e.cut(o,!0).forEach(function(e){s=s.concat(e.split(" "))}),s=s.filter(function(e){return!!e});var u=0;return s.map(function(e,t){if(i){var n=o.indexOf(e,u),s={};return s.position=[n,e.length],s.index=t,u=n,new r.Token(e,s)}return e})},r.zh.wordCharacters="\\w一-龥",r.zh.trimmer=r.trimmerSupport.generateTrimmer(r.zh.wordCharacters),r.Pipeline.registerFunction(r.zh.trimmer,"trimmer-zh"),r.zh.stemmer=function(){return function(e){return e}}(),r.Pipeline.registerFunction(r.zh.stemmer,"stemmer-zh"),r.zh.stopWordFilter=r.generateStopWordFilter("的 一 不 在 人 有 是 为 為 以 于 於 上 他 而 后 後 之 来 來 及 了 因 下 可 到 由 这 這 与 與 也 此 但 并 並 个 個 其 已 无 無 小 我 们 們 起 最 再 今 去 好 只 又 或 很 亦 某 把 那 你 乃 它 吧 被 比 别 趁 当 當 从 從 得 打 凡 儿 兒 尔 爾 该 該 各 给 給 跟 和 何 还 還 即 几 幾 既 看 据 據 距 靠 啦 另 么 麽 每 嘛 拿 哪 您 凭 憑 且 却 卻 让 讓 仍 啥 如 若 使 谁 誰 虽 雖 随 隨 同 所 她 哇 嗡 往 些 向 沿 哟 喲 用 咱 则 則 怎 曾 至 致 着 著 诸 諸 自".split(" ")),r.Pipeline.registerFunction(r.zh.stopWordFilter,"stopWordFilter-zh")}});
+206
View File
@@ -0,0 +1,206 @@
/**
* export the module via AMD, CommonJS or as a browser global
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
*/
;(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(factory)
} else if (typeof exports === 'object') {
/**
* Node. Does not work with strict CommonJS, but
* only CommonJS-like environments that support module.exports,
* like Node.
*/
module.exports = factory()
} else {
// Browser globals (root is window)
factory()(root.lunr);
}
}(this, function () {
/**
* Just return a value to define the module export.
* This example returns an object, but the module
* can return a function as the exported value.
*/
return function(lunr) {
// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript
// (c) 2008 Taku Kudo <taku@chasen.org>
// TinySegmenter is freely distributable under the terms of a new BSD licence.
// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt
function TinySegmenter() {
var patterns = {
"[一二三四五六七八九十百千万億兆]":"M",
"[一-龠々〆ヵヶ]":"H",
"[ぁ-ん]":"I",
"[ァ-ヴーア-ン゙ー]":"K",
"[a-zA-Z-zA-]":"A",
"[0-9-]":"N"
}
this.chartype_ = [];
for (var i in patterns) {
var regexp = new RegExp(i);
this.chartype_.push([regexp, patterns[i]]);
}
this.BIAS__ = -332
this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378};
this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920};
this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266};
this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352};
this.BP2__ = {"BO":60,"OO":-1762};
this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965};
this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146};
this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699};
this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973};
this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682};
this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669};
this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990};
this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832};
this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649};
this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393};
this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841};
this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68};
this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591};
this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685};
this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156};
this.TW1__ = {"につい":-4681,"東京都":2026};
this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216};
this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287};
this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865};
this.UC1__ = {"A":484,"K":93,"M":645,"O":-505};
this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646};
this.UC3__ = {"A":-1370,"I":2311};
this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646};
this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831};
this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387};
this.UP1__ = {"O":-214};
this.UP2__ = {"B":69,"O":935};
this.UP3__ = {"B":189};
this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422};
this.UQ2__ = {"BH":216,"BI":113,"OK":1759};
this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212};
this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135};
this.UW2__ = {",":-829,"、":-829,"":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568};
this.UW3__ = {",":4889,"1":-800,"":-1723,"、":4889,"々":-2311,"":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278};
this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637};
this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343};
this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"":-270,"E1":306,"ル":-673,"ン":-496};
return this;
}
TinySegmenter.prototype.ctype_ = function(str) {
for (var i in this.chartype_) {
if (str.match(this.chartype_[i][0])) {
return this.chartype_[i][1];
}
}
return "O";
}
TinySegmenter.prototype.ts_ = function(v) {
if (v) { return v; }
return 0;
}
TinySegmenter.prototype.segment = function(input) {
if (input == null || input == undefined || input == "") {
return [];
}
var result = [];
var seg = ["B3","B2","B1"];
var ctype = ["O","O","O"];
var o = input.split("");
for (i = 0; i < o.length; ++i) {
seg.push(o[i]);
ctype.push(this.ctype_(o[i]))
}
seg.push("E1");
seg.push("E2");
seg.push("E3");
ctype.push("O");
ctype.push("O");
ctype.push("O");
var word = seg[3];
var p1 = "U";
var p2 = "U";
var p3 = "U";
for (var i = 4; i < seg.length - 3; ++i) {
var score = this.BIAS__;
var w1 = seg[i-3];
var w2 = seg[i-2];
var w3 = seg[i-1];
var w4 = seg[i];
var w5 = seg[i+1];
var w6 = seg[i+2];
var c1 = ctype[i-3];
var c2 = ctype[i-2];
var c3 = ctype[i-1];
var c4 = ctype[i];
var c5 = ctype[i+1];
var c6 = ctype[i+2];
score += this.ts_(this.UP1__[p1]);
score += this.ts_(this.UP2__[p2]);
score += this.ts_(this.UP3__[p3]);
score += this.ts_(this.BP1__[p1 + p2]);
score += this.ts_(this.BP2__[p2 + p3]);
score += this.ts_(this.UW1__[w1]);
score += this.ts_(this.UW2__[w2]);
score += this.ts_(this.UW3__[w3]);
score += this.ts_(this.UW4__[w4]);
score += this.ts_(this.UW5__[w5]);
score += this.ts_(this.UW6__[w6]);
score += this.ts_(this.BW1__[w2 + w3]);
score += this.ts_(this.BW2__[w3 + w4]);
score += this.ts_(this.BW3__[w4 + w5]);
score += this.ts_(this.TW1__[w1 + w2 + w3]);
score += this.ts_(this.TW2__[w2 + w3 + w4]);
score += this.ts_(this.TW3__[w3 + w4 + w5]);
score += this.ts_(this.TW4__[w4 + w5 + w6]);
score += this.ts_(this.UC1__[c1]);
score += this.ts_(this.UC2__[c2]);
score += this.ts_(this.UC3__[c3]);
score += this.ts_(this.UC4__[c4]);
score += this.ts_(this.UC5__[c5]);
score += this.ts_(this.UC6__[c6]);
score += this.ts_(this.BC1__[c2 + c3]);
score += this.ts_(this.BC2__[c3 + c4]);
score += this.ts_(this.BC3__[c4 + c5]);
score += this.ts_(this.TC1__[c1 + c2 + c3]);
score += this.ts_(this.TC2__[c2 + c3 + c4]);
score += this.ts_(this.TC3__[c3 + c4 + c5]);
score += this.ts_(this.TC4__[c4 + c5 + c6]);
// score += this.ts_(this.TC5__[c4 + c5 + c6]);
score += this.ts_(this.UQ1__[p1 + c1]);
score += this.ts_(this.UQ2__[p2 + c2]);
score += this.ts_(this.UQ3__[p3 + c3]);
score += this.ts_(this.BQ1__[p2 + c2 + c3]);
score += this.ts_(this.BQ2__[p2 + c3 + c4]);
score += this.ts_(this.BQ3__[p3 + c2 + c3]);
score += this.ts_(this.BQ4__[p3 + c3 + c4]);
score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]);
score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]);
score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]);
score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]);
var p = "O";
if (score > 0) {
result.push(word);
word = "";
p = "B";
}
p1 = p2;
p2 = p3;
p3 = p;
word += seg[i];
}
result.push(word);
return result;
}
lunr.TinySegmenter = TinySegmenter;
};
}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"version":3,"sources":["src/templates/assets/stylesheets/palette/_scheme.scss","../../../../src/templates/assets/stylesheets/palette.scss","src/templates/assets/stylesheets/palette/_accent.scss","src/templates/assets/stylesheets/palette/_primary.scss","src/templates/assets/stylesheets/utilities/_break.scss"],"names":[],"mappings":"AA2BA,cAGE,6BAME,sDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CACA,mDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAGA,mDAAA,CACA,gDAAA,CACA,yDAAA,CACA,4DAAA,CAGA,0BAAA,CACA,mCAAA,CAGA,iCAAA,CACA,kCAAA,CACA,mCAAA,CACA,mCAAA,CACA,kCAAA,CACA,iCAAA,CACA,+CAAA,CACA,6DAAA,CACA,gEAAA,CACA,4DAAA,CACA,4DAAA,CACA,6DAAA,CAGA,6CAAA,CAGA,+CAAA,CAGA,uDAAA,CACA,6DAAA,CACA,2DAAA,CAGA,iCAAA,CAGA,yDAAA,CACA,iEAAA,CAGA,mDAAA,CACA,mDAAA,CAGA,qDAAA,CACA,uDAAA,CAGA,8DAAA,CAKA,8DAAA,CAKA,0DAAA,CAzEA,iBCiBF,CD6DE,kHAEE,YC3DJ,CDkFE,yDACE,4BChFJ,CD+EE,2DACE,4BC7EJ,CD4EE,gEACE,4BC1EJ,CDyEE,2DACE,4BCvEJ,CDsEE,yDACE,4BCpEJ,CDmEE,0DACE,4BCjEJ,CDgEE,gEACE,4BC9DJ,CD6DE,0DACE,4BC3DJ,CD0DE,2OACE,4BC/CJ,CDsDA,+FAGE,iCCpDF,CACF,CCjDE,2BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD6CN,CCvDE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDoDN,CC9DE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD2DN,CCrEE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDkEN,CC5EE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDyEN,CCnFE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDgFN,CC1FE,kCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDuFN,CCjGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD8FN,CCxGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDqGN,CC/GE,6BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD4GN,CCtHE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDmHN,CC7HE,4BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD6HN,CCpIE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDoIN,CC3IE,6BACE,yBAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD2IN,CClJE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDkJN,CCzJE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDsJN,CE3JE,4BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwJN,CEnKE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgKN,CE3KE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwKN,CEnLE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgLN,CE3LE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwLN,CEnME,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgMN,CE3ME,mCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwMN,CEnNE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgNN,CE3NE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwNN,CEnOE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgON,CE3OE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwON,CEnPE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmPN,CE3PE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2PN,CEnQE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmQN,CE3QE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2QN,CEnRE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgRN,CE3RE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwRN,CEnSE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BF4RN,CE5SE,kCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BFqSN,CEtRE,sEACE,4BFyRJ,CE1RE,+DACE,4BF6RJ,CE9RE,iEACE,4BFiSJ,CElSE,gEACE,4BFqSJ,CEtSE,iEACE,4BFySJ,CEhSA,8BACE,mDAAA,CACA,4DAAA,CACA,0DAAA,CACA,oDAAA,CACA,2DAAA,CAGA,4BFiSF,CE9RE,yCACE,+BFgSJ,CE7RI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCFiSN,CG7MI,mCD1EA,+CACE,8CF0RJ,CEvRI,qDACE,8CFyRN,CEpRE,iEACE,mCFsRJ,CACF,CGxNI,sCDvDA,uCACE,oCFkRJ,CACF,CEzQA,8BACE,kDAAA,CACA,4DAAA,CACA,wDAAA,CACA,oDAAA,CACA,6DAAA,CAGA,4BF0QF,CEvQE,yCACE,+BFyQJ,CEtQI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCF0QN,CEnQE,yCACE,6CFqQJ,CG9NI,0CDhCA,8CACE,gDFiQJ,CACF,CGnOI,0CDvBA,iFACE,6CF6PJ,CACF,CG3PI,sCDKA,uCACE,6CFyPJ,CACF","file":"palette.css"}
-14
View File
@@ -1,14 +0,0 @@
cmake_minimum_required(VERSION 3.21)
add_executable(bench_pipeline bench_pipeline.cpp)
target_link_libraries(bench_pipeline PRIVATE kpn)
target_compile_options(bench_pipeline PRIVATE -O3 -march=native)
find_package(TBB QUIET)
if(TBB_FOUND)
target_link_libraries(bench_pipeline PRIVATE TBB::tbb)
target_compile_definitions(bench_pipeline PRIVATE KPN_BENCH_TBB=1)
message(STATUS "TBB found — enabling TBB benchmarks")
else()
message(STATUS "TBB not found — TBB benchmarks disabled")
endif()
-526
View File
@@ -1,526 +0,0 @@
// Throughput benchmark: items/second vs. graph topology and size.
//
// Topologies:
// chain — linear depth D: push → n[0..D-1] → pop
// wide — fanout<W>: push → fanout → W parallel nodes → W pops
// diamond — push → fanout<2> → 2×2 nodes → 2 pops
//
// Two scheduling modes for each topology:
// private — each node owns a private ThreadPool(1) [Node<>]
// pool — all nodes share one ThreadPool(T) [PoolNode<> + shared pool]
//
// Usage: ./bench_pipeline | tee results.csv
#include <kpn/kpn.hpp>
#ifdef KPN_BENCH_TBB
#include <oneapi/tbb/flow_graph.h>
namespace tbb_flow = oneapi::tbb::flow;
#endif
#include <array>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <memory>
#include <string>
#include <thread>
#include <vector>
using namespace kpn;
using namespace std::chrono_literals;
using sclock = std::chrono::steady_clock;
// ── configurable work ─────────────────────────────────────────────────────────
static std::atomic<int> g_work_us{0};
static int chain_fn(int x) {
int us = g_work_us.load(std::memory_order_relaxed);
if (us > 0) {
auto end = sclock::now() + std::chrono::microseconds(us);
while (sclock::now() < end);
}
return x;
}
using ChainNode = Node<chain_fn, in<>, out<>>;
using PoolChainNode = PoolNode<chain_fn, in<>, out<>>;
// ── push helper: yield-spin on overflow (no artificial sleep latency) ─────────
static void push_retry(Channel<int>& ch, int val) {
while (true) {
try { ch.push(val); return; }
catch (const ChannelOverflowError&) { std::this_thread::yield(); }
catch (const ChannelClosedError&) { return; }
}
}
// ── result ────────────────────────────────────────────────────────────────────
struct Result {
const char* topology;
int size;
int work_us;
int threads; // 0 = private (1 thread per node), N = shared pool size
double items_per_sec;
double overhead_us;
};
// ── chain ─────────────────────────────────────────────────────────────────────
static int items_for(int work_us, int depth = 1) {
int effective = std::max(1, work_us) * std::max(1, depth);
if (effective <= 1) return 5000;
if (effective <= 10) return 3000;
if (effective <= 100) return 1000;
if (effective <= 1000) return 200;
return 50;
}
static Result bench_chain(int depth, int work_us) {
const int N = items_for(work_us, depth);
const int CAP = N;
std::vector<std::shared_ptr<Channel<int>>> chs;
for (int i = 0; i <= depth; ++i)
chs.push_back(std::make_shared<Channel<int>>(CAP));
std::vector<std::unique_ptr<ChainNode>> nodes;
for (int i = 0; i < depth; ++i) {
nodes.push_back(std::make_unique<ChainNode>(CAP));
nodes.back()->set_input_channel<0>(chs[i]);
nodes.back()->set_output_channel<0>(chs[i + 1].get());
}
for (auto& n : nodes) n->start();
std::atomic<sclock::time_point> t1;
std::thread reader([&] {
for (int i = 0; i < N; ++i) chs.back()->pop();
t1.store(sclock::now(), std::memory_order_release);
});
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*chs[0], i);
});
pusher.join();
reader.join();
for (auto& n : nodes) n->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
// Subtract theoretical pipeline fill cost (depth-1)*W so that overhead
// reflects only framework latency, not the expected pipeline startup time.
double pipeline_us = static_cast<double>(work_us) * (N + depth - 1);
double wus = (elapsed * 1e6 - pipeline_us) / N;
return {"chain", depth, work_us, 0, N / elapsed, wus};
}
static Result bench_chain_pool(int depth, int work_us, int pool_threads) {
const int N = items_for(work_us, depth);
const int CAP = N;
auto pool = std::make_shared<ThreadPool>(pool_threads);
std::vector<std::shared_ptr<Channel<int>>> chs;
for (int i = 0; i <= depth; ++i)
chs.push_back(std::make_shared<Channel<int>>(CAP));
std::vector<std::unique_ptr<PoolChainNode>> nodes;
for (int i = 0; i < depth; ++i) {
nodes.push_back(std::make_unique<PoolChainNode>(pool, CAP));
nodes.back()->set_input_channel<0>(chs[i]);
nodes.back()->set_output_channel<0>(chs[i + 1].get());
}
pool->start();
for (auto& n : nodes) n->start();
std::atomic<sclock::time_point> t1;
std::thread reader([&] {
for (int i = 0; i < N; ++i) chs.back()->pop();
t1.store(sclock::now(), std::memory_order_release);
});
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*chs[0], i);
});
pusher.join();
reader.join();
for (auto& n : nodes) n->stop();
pool->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
double pipeline_us = static_cast<double>(work_us) * (N + depth - 1);
double wus = (elapsed * 1e6 - pipeline_us) / N;
return {"chain", depth, work_us, pool_threads, N / elapsed, wus};
}
// ── wide (fanout<W>) ──────────────────────────────────────────────────────────
template<std::size_t W>
static Result bench_wide(int work_us) {
const int N = items_for(work_us);
const int CAP = N;
auto src_ch = std::make_shared<Channel<int>>(CAP);
auto fan = std::make_unique<FanoutNode<int, W>>(CAP);
fan->template set_input_channel<0>(src_ch);
std::array<std::unique_ptr<ChainNode>, W> nodes;
std::array<std::shared_ptr<Channel<int>>, W> sink_chs;
for (std::size_t i = 0; i < W; ++i) {
nodes[i] = std::make_unique<ChainNode>(CAP);
sink_chs[i] = std::make_shared<Channel<int>>(CAP);
nodes[i]->template set_output_channel<0>(sink_chs[i].get());
}
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
(fan->template set_output_channel<Is>(
&nodes[Is]->template input_channel<0>()), ...);
}(std::make_index_sequence<W>{});
fan->start();
for (auto& n : nodes) n->start();
std::array<std::thread, W> readers;
std::atomic<sclock::time_point> t1;
std::atomic<int> readers_done{0};
for (std::size_t w = 0; w < W; ++w) {
readers[w] = std::thread([&, w] {
for (int i = 0; i < N; ++i) sink_chs[w]->pop();
if (readers_done.fetch_add(1, std::memory_order_acq_rel) + 1
== static_cast<int>(W))
t1.store(sclock::now(), std::memory_order_release);
});
}
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*src_ch, i);
});
pusher.join();
for (auto& r : readers) r.join();
fan->stop();
for (auto& n : nodes) n->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"wide", static_cast<int>(W), work_us, 0, N / elapsed, wus};
}
template<std::size_t W>
static Result bench_wide_pool(int work_us, int pool_threads) {
const int N = items_for(work_us);
const int CAP = N;
auto pool = std::make_shared<ThreadPool>(pool_threads);
auto src_ch = std::make_shared<Channel<int>>(CAP);
auto fan = std::make_unique<FanoutNode<int, W>>(CAP);
fan->template set_input_channel<0>(src_ch);
std::array<std::unique_ptr<PoolChainNode>, W> nodes;
std::array<std::shared_ptr<Channel<int>>, W> sink_chs;
for (std::size_t i = 0; i < W; ++i) {
nodes[i] = std::make_unique<PoolChainNode>(pool, CAP);
sink_chs[i] = std::make_shared<Channel<int>>(CAP);
nodes[i]->template set_output_channel<0>(sink_chs[i].get());
}
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
(fan->template set_output_channel<Is>(
&nodes[Is]->template input_channel<0>()), ...);
}(std::make_index_sequence<W>{});
fan->start();
pool->start();
for (auto& n : nodes) n->start();
std::array<std::thread, W> readers;
std::atomic<sclock::time_point> t1;
std::atomic<int> readers_done{0};
for (std::size_t w = 0; w < W; ++w) {
readers[w] = std::thread([&, w] {
for (int i = 0; i < N; ++i) sink_chs[w]->pop();
if (readers_done.fetch_add(1, std::memory_order_acq_rel) + 1
== static_cast<int>(W))
t1.store(sclock::now(), std::memory_order_release);
});
}
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*src_ch, i);
});
pusher.join();
for (auto& r : readers) r.join();
fan->stop();
for (auto& n : nodes) n->stop();
pool->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"wide", static_cast<int>(W), work_us, pool_threads, N / elapsed, wus};
}
// ── diamond ───────────────────────────────────────────────────────────────────
static Result bench_diamond(int work_us) {
const int N = items_for(work_us, 2);
const int CAP = N;
auto src_ch = std::make_shared<Channel<int>>(CAP);
auto fan = std::make_unique<FanoutNode<int, 2>>(CAP);
fan->template set_input_channel<0>(src_ch);
auto nL = std::make_unique<ChainNode>(CAP);
auto nR = std::make_unique<ChainNode>(CAP);
auto nL2 = std::make_unique<ChainNode>(CAP);
auto nR2 = std::make_unique<ChainNode>(CAP);
auto chL = std::make_shared<Channel<int>>(CAP);
auto chR = std::make_shared<Channel<int>>(CAP);
auto snkL = std::make_shared<Channel<int>>(CAP);
auto snkR = std::make_shared<Channel<int>>(CAP);
fan->template set_output_channel<0>(&nL->template input_channel<0>());
fan->template set_output_channel<1>(&nR->template input_channel<0>());
nL->set_output_channel<0>(chL.get());
nR->set_output_channel<0>(chR.get());
nL2->set_input_channel<0>(chL);
nR2->set_input_channel<0>(chR);
nL2->set_output_channel<0>(snkL.get());
nR2->set_output_channel<0>(snkR.get());
fan->start(); nL->start(); nR->start(); nL2->start(); nR2->start();
std::atomic<sclock::time_point> t1;
std::atomic<int> done{0};
auto make_reader = [&](Channel<int>& ch) {
return std::thread([&] {
for (int i = 0; i < N; ++i) ch.pop();
if (done.fetch_add(1, std::memory_order_acq_rel) + 1 == 2)
t1.store(sclock::now(), std::memory_order_release);
});
};
auto rL = make_reader(*snkL);
auto rR = make_reader(*snkR);
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*src_ch, i);
});
pusher.join(); rL.join(); rR.join();
fan->stop(); nL->stop(); nR->stop(); nL2->stop(); nR2->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"diamond", 4, work_us, 0, N / elapsed, wus};
}
static Result bench_diamond_pool(int work_us, int pool_threads) {
const int N = items_for(work_us, 2);
const int CAP = N;
auto pool = std::make_shared<ThreadPool>(pool_threads);
auto src_ch = std::make_shared<Channel<int>>(CAP);
auto fan = std::make_unique<FanoutNode<int, 2>>(CAP);
fan->template set_input_channel<0>(src_ch);
auto nL = std::make_unique<PoolChainNode>(pool, CAP);
auto nR = std::make_unique<PoolChainNode>(pool, CAP);
auto nL2 = std::make_unique<PoolChainNode>(pool, CAP);
auto nR2 = std::make_unique<PoolChainNode>(pool, CAP);
auto chL = std::make_shared<Channel<int>>(CAP);
auto chR = std::make_shared<Channel<int>>(CAP);
auto snkL = std::make_shared<Channel<int>>(CAP);
auto snkR = std::make_shared<Channel<int>>(CAP);
fan->template set_output_channel<0>(&nL->template input_channel<0>());
fan->template set_output_channel<1>(&nR->template input_channel<0>());
nL->set_output_channel<0>(chL.get());
nR->set_output_channel<0>(chR.get());
nL2->set_input_channel<0>(chL);
nR2->set_input_channel<0>(chR);
nL2->set_output_channel<0>(snkL.get());
nR2->set_output_channel<0>(snkR.get());
fan->start();
pool->start();
nL->start(); nR->start(); nL2->start(); nR2->start();
std::atomic<sclock::time_point> t1;
std::atomic<int> done{0};
auto make_reader = [&](Channel<int>& ch) {
return std::thread([&] {
for (int i = 0; i < N; ++i) ch.pop();
if (done.fetch_add(1, std::memory_order_acq_rel) + 1 == 2)
t1.store(sclock::now(), std::memory_order_release);
});
};
auto rL = make_reader(*snkL);
auto rR = make_reader(*snkR);
auto t0 = sclock::now();
std::thread pusher([&] {
for (int i = 0; i < N; ++i) push_retry(*src_ch, i);
});
pusher.join(); rL.join(); rR.join();
fan->stop();
nL->stop(); nR->stop(); nL2->stop(); nR2->stop();
pool->stop();
double elapsed = std::chrono::duration<double>(
t1.load(std::memory_order_acquire) - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"diamond", 4, work_us, pool_threads, N / elapsed, wus};
}
// ── TBB flow graph ────────────────────────────────────────────────────────────
#ifdef KPN_BENCH_TBB
static Result bench_chain_tbb(int depth, int work_us) {
const int N = items_for(work_us, depth);
tbb_flow::graph g;
using FN = tbb_flow::function_node<int, int>;
std::vector<std::unique_ptr<FN>> nodes;
nodes.reserve(depth);
for (int i = 0; i < depth; ++i)
nodes.push_back(std::make_unique<FN>(g, tbb_flow::serial,
[](int x) -> int { return chain_fn(x); }));
for (int i = 0; i + 1 < depth; ++i)
tbb_flow::make_edge(*nodes[i], *nodes[i + 1]);
auto t0 = sclock::now();
for (int i = 0; i < N; ++i) nodes[0]->try_put(i);
g.wait_for_all();
auto t1 = sclock::now();
double elapsed = std::chrono::duration<double>(t1 - t0).count();
double pipeline_us = static_cast<double>(work_us) * (N + depth - 1);
double wus = (elapsed * 1e6 - pipeline_us) / N;
return {"chain_tbb", depth, work_us, -1, N / elapsed, wus};
}
template<std::size_t W>
static Result bench_wide_tbb(int work_us) {
const int N = items_for(work_us);
tbb_flow::graph g;
tbb_flow::broadcast_node<int> fan(g);
using FN = tbb_flow::function_node<int, int>;
std::array<std::unique_ptr<FN>, W> nodes;
for (auto& n : nodes) {
n = std::make_unique<FN>(g, tbb_flow::serial,
[](int x) -> int { return chain_fn(x); });
tbb_flow::make_edge(fan, *n);
}
auto t0 = sclock::now();
for (int i = 0; i < N; ++i) fan.try_put(i);
g.wait_for_all();
auto t1 = sclock::now();
double elapsed = std::chrono::duration<double>(t1 - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"wide_tbb", static_cast<int>(W), work_us, -1, N / elapsed, wus};
}
static Result bench_diamond_tbb(int work_us) {
const int N = items_for(work_us, 2);
tbb_flow::graph g;
tbb_flow::broadcast_node<int> fan(g);
using FN = tbb_flow::function_node<int, int>;
auto fn = [](int x) -> int { return chain_fn(x); };
FN nL(g, tbb_flow::serial, fn), nR(g, tbb_flow::serial, fn);
FN nL2(g, tbb_flow::serial, fn), nR2(g, tbb_flow::serial, fn);
tbb_flow::make_edge(fan, nL); tbb_flow::make_edge(fan, nR);
tbb_flow::make_edge(nL, nL2); tbb_flow::make_edge(nR, nR2);
auto t0 = sclock::now();
for (int i = 0; i < N; ++i) fan.try_put(i);
g.wait_for_all();
auto t1 = sclock::now();
double elapsed = std::chrono::duration<double>(t1 - t0).count();
double wus = (elapsed * 1e6) / N - static_cast<double>(work_us);
return {"diamond_tbb", 4, work_us, -1, N / elapsed, wus};
}
#endif // KPN_BENCH_TBB
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
const int work_amts[] = {10, 100, 1000};
const int pool_sizes[] = {1, 2, 4};
std::fprintf(stderr, "%-12s %-8s %-10s %-8s %-18s %-20s\n",
"topology", "size", "work_us", "threads", "items/sec", "overhead_us/item");
std::fprintf(stderr, "%s\n", std::string(78, '-').c_str());
std::printf("topology,size,work_us,threads,items_per_sec,overhead_us_per_item\n");
auto emit = [](const Result& r) {
std::string sched = r.threads < 0 ? "tbb"
: r.threads == 0 ? "priv"
: std::to_string(r.threads);
std::fprintf(stderr, "%-12s %-8d %-10d %-8s %-18.0f %-20.1f\n",
r.topology, r.size, r.work_us, sched.c_str(),
r.items_per_sec, r.overhead_us);
std::printf("%s,%d,%d,%s,%.0f,%.2f\n",
r.topology, r.size, r.work_us, sched.c_str(),
r.items_per_sec, r.overhead_us);
std::fflush(stdout);
};
for (int w : work_amts) {
g_work_us.store(w, std::memory_order_relaxed);
std::fprintf(stderr, "\n── work_us=%-4d private pools ───────────────────────────────────────\n", w);
for (int d : {1, 2, 4, 8, 16, 32}) emit(bench_chain(d, w));
emit(bench_wide<1>(w));
emit(bench_wide<2>(w));
emit(bench_wide<3>(w));
emit(bench_wide<4>(w));
emit(bench_diamond(w));
for (int pt : pool_sizes) {
std::fprintf(stderr, "\n── work_us=%-4d shared pool (%d thread%s) ─────────────────────────────\n",
w, pt, pt == 1 ? "" : "s");
for (int d : {1, 2, 4, 8, 16, 32}) emit(bench_chain_pool(d, w, pt));
emit(bench_wide_pool<1>(w, pt));
emit(bench_wide_pool<2>(w, pt));
emit(bench_wide_pool<3>(w, pt));
emit(bench_wide_pool<4>(w, pt));
emit(bench_diamond_pool(w, pt));
}
#ifdef KPN_BENCH_TBB
std::fprintf(stderr, "\n── work_us=%-4d TBB flow graph ──────────────────────────────────────\n", w);
for (int d : {1, 2, 4, 8, 16, 32}) emit(bench_chain_tbb(d, w));
emit(bench_wide_tbb<1>(w));
emit(bench_wide_tbb<2>(w));
emit(bench_wide_tbb<3>(w));
emit(bench_wide_tbb<4>(w));
emit(bench_diamond_tbb(w));
#endif
}
}
+1073
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-44
View File
@@ -1,44 +0,0 @@
#include <kpn/kpn.hpp>
#include <iostream>
#include <chrono>
#include <thread>
// A minimal linear pipeline: producer → double → print
//
// [produce] --int--> [double_it] --int--> [print_it]
// [snippet: basic_node_fns]
static int produce() { return 42; }
static int double_it(int x) { return x * 2; }
static void print_it(int x) { std::cout << "result: " << x << '\n'; }
// [/snippet: basic_node_fns]
int main() {
using namespace kpn;
// [snippet: index_only_nodes]
auto src = make_node<produce>(5);
auto dbl = make_node<double_it>(5);
auto sink = make_node<print_it>(5);
// [/snippet: index_only_nodes]
// Wire channels
auto& dbl_in = dbl.input_channel<0>();
auto& sink_in = sink.input_channel<0>();
src.set_output_channel<0>(&dbl_in);
dbl.set_output_channel<0>(&sink_in);
// [snippet: network_build]
Network net;
net.add("src", src)
.add("dbl", dbl)
.add("sink", sink)
.connect("src", src.output<0>(), "dbl", dbl.input<0>())
.connect("dbl", dbl.output<0>(), "sink", sink.input<0>())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
net.stop();
// [/snippet: network_build]
}
-82
View File
@@ -1,82 +0,0 @@
// Example 02 — Named Ports
//
// A three-stage text pipeline where port names carry semantic meaning:
//
// [tokenise] --"words"--> [count_words] --"count"--> [report]
// --"words"--> [report]
//
// Named ports let the connect() call read like documentation:
// connect("tok", tok.output<"words">(), "cnt", cnt.input<"words">())
// A typo in the name is a compile-time error, not a runtime surprise.
#include <kpn/kpn.hpp>
#include <chrono>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <tuple>
#include <vector>
// ── Node functions ────────────────────────────────────────────────────────────
static int sentence_index = 0;
static std::vector<std::string> tokenise() {
static const char* sentences[] = {
"the quick brown fox jumps over the lazy dog",
"kahn process networks are a model of concurrent computation",
"each node runs in its own thread communicating via channels",
"named ports catch wiring mistakes at compile time",
};
std::istringstream ss(sentences[sentence_index++ % 4]);
std::vector<std::string> words;
std::string w;
while (ss >> w) words.push_back(w);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return words;
}
// Returns (word_count, original_words) — two outputs via tuple
static std::tuple<int, std::vector<std::string>>
count_words(std::vector<std::string> words) {
return {static_cast<int>(words.size()), std::move(words)};
}
static void report(int count, std::vector<std::string> words) {
std::cout << "[" << count << " words] ";
for (auto& w : words) std::cout << w << ' ';
std::cout << '\n';
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// [snippet: named_port_creation]
// tokenise: no inputs, one named output "words"
auto tok = make_node<tokenise>(out<"words">{}, 4);
// count_words: named input "words", named outputs "count" and "words"
auto cnt = make_node<count_words>(in<"words">{}, out<"count", "words">{}, 4);
// report: two named inputs
auto snk = make_node<report>(in<"count", "words">{}, 4);
// [/snippet: named_port_creation]
// [snippet: named_port_network]
Network net;
net.add("tok", tok)
.add("cnt", cnt)
.add("snk", snk)
.connect("tok", tok.template output<"words">(), "cnt", cnt.template input<"words">())
.connect("cnt", cnt.template output<"count">(), "snk", snk.template input<"count">())
.connect("cnt", cnt.template output<"words">(), "snk", snk.template input<"words">())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
net.stop();
// [/snippet: named_port_network]
}
-79
View File
@@ -1,79 +0,0 @@
// Example 03 — Multi-Output (Fan-Out)
//
// A single "parse" node reads "KEY=VALUE" strings and fans out to two
// independent downstream sinks — one for keys, one for values.
//
// +--> [print_key]
// [generate] --string--> [parse]
// +--> [print_value]
//
// The parser returns std::tuple<std::string, std::string>.
// Network::connect() routes each element of the tuple to a different node.
#include <kpn/kpn.hpp>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <tuple>
// ── Node functions ────────────────────────────────────────────────────────────
static int gen_index = 0;
static std::string generate() {
static const char* pairs[] = {
"host=localhost",
"port=8080",
"timeout=30s",
"retries=3",
"protocol=http2",
};
std::this_thread::sleep_for(std::chrono::milliseconds(60));
return pairs[gen_index++ % 5];
}
// [snippet: multi_output_fn]
// Multi-output: returns (key, value) as a tuple — KPN++ routes each element
// to its own output port automatically.
static std::tuple<std::string, std::string> parse(std::string kv) {
auto sep = kv.find('=');
if (sep == std::string::npos) return {kv, ""};
return {kv.substr(0, sep), kv.substr(sep + 1)};
}
// [/snippet: multi_output_fn]
static void print_key(std::string key) {
std::cout << "KEY → " << key << '\n';
}
static void print_value(std::string value) {
std::cout << "VALUE → " << value << '\n';
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// [snippet: fanout_network]
auto gen = make_node<generate>(out<"kv">{}, 4);
auto par = make_node<parse> (in<"kv">{}, out<"key", "value">{}, 4);
auto keys = make_node<print_key> (in<"key">{}, 4);
auto vals = make_node<print_value>(in<"value">{}, 4);
Network net;
net.add("gen", gen)
.add("par", par)
.add("keys", keys)
.add("vals", vals)
.connect("gen", gen.template output<"kv">(), "par", par.template input<"kv">())
.connect("par", par.template output<"key">(), "keys", keys.template input<"key">())
.connect("par", par.template output<"value">(), "vals", vals.template input<"value">())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(600));
net.stop();
// [/snippet: fanout_network]
}
-120
View File
@@ -1,120 +0,0 @@
// Example 04 — Storage Policy
//
// Demonstrates how KPN++ chooses between by-value and shared_ptr storage
// inside channels depending on the type.
//
// Small trivially-copyable types (int, double, etc.) are stored by value.
// Large or non-trivially-copyable types are stored as shared_ptr<const T>
// — zero copies even across multiple downstream consumers.
//
// This example also shows how to override the policy for a specific type
// with a template specialisation.
//
// [produce_frame] --Frame--> [process_frame] --Frame--> [consume_frame]
// [produce_small] --int----> [double_it] --int----> [print_int]
#include <kpn/kpn.hpp>
#include <array>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <type_traits>
// ── Types ─────────────────────────────────────────────────────────────────────
// Large frame type — will be stored as shared_ptr<const Frame> by default
struct Frame {
std::array<uint8_t, 4096> pixels{};
int id = 0;
};
// A small struct we force to be stored by value via policy specialisation
struct Tag {
int value = 0;
};
// [snippet: storage_policy_spec]
// Override: store Tag by value despite being a struct
// (it's trivially copyable and small — this just makes the policy explicit)
template<>
struct kpn::channel_storage_policy<Tag> {
static constexpr bool by_value = true;
};
// [/snippet: storage_policy_spec]
// ── Node functions ────────────────────────────────────────────────────────────
static int frame_id = 0;
static int tag_id = 0;
static Frame produce_frame() {
std::this_thread::sleep_for(std::chrono::milliseconds(40));
Frame f;
f.id = frame_id++;
f.pixels.fill(static_cast<uint8_t>(f.id & 0xFF));
return f;
}
static Frame process_frame(Frame f) {
// Simulate some work
for (auto& p : f.pixels) p = static_cast<uint8_t>(255 - p);
return f;
}
static void consume_frame(Frame f) {
std::cout << "[frame] id=" << f.id
<< " first_px=" << static_cast<int>(f.pixels[0])
<< " storage=shared_ptr (sizeof Frame = " << sizeof(Frame) << " B)\n";
}
static Tag produce_tag() {
std::this_thread::sleep_for(std::chrono::milliseconds(40));
return {tag_id++};
}
static Tag double_tag(Tag t) { return {t.value * 2}; }
static void print_tag(Tag t) {
std::cout << "[tag] value=" << t.value
<< " storage=by_value (sizeof Tag = " << sizeof(Tag) << " B)\n";
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// Verify storage decisions at compile time
static_assert(!channel_storage_policy<Frame>::by_value,
"Frame should use shared_ptr storage");
static_assert(channel_storage_policy<Tag>::by_value,
"Tag should use by_value storage (via specialisation)");
static_assert(channel_storage_policy<int>::by_value,
"int should use by_value storage");
auto prod_f = make_node<produce_frame> (out<"frame">{}, 4);
auto proc_f = make_node<process_frame> (in<"frame">{}, out<"frame">{}, 4);
auto cons_f = make_node<consume_frame> (in<"frame">{}, 4);
auto prod_t = make_node<produce_tag> (out<"tag">{}, 4);
auto dbl_t = make_node<double_tag> (in<"tag">{}, out<"tag">{}, 4);
auto print_t = make_node<print_tag> (in<"tag">{}, 4);
Network net;
net.add("prod_f", prod_f)
.add("proc_f", proc_f)
.add("cons_f", cons_f)
.add("prod_t", prod_t)
.add("dbl_t", dbl_t)
.add("print_t", print_t)
.connect("prod_f", prod_f.template output<"frame">(), "proc_f", proc_f.template input<"frame">())
.connect("proc_f", proc_f.template output<"frame">(), "cons_f", cons_f.template input<"frame">())
.connect("prod_t", prod_t.template output<"tag">(), "dbl_t", dbl_t.template input<"tag">())
.connect("dbl_t", dbl_t.template output<"tag">(), "print_t", print_t.template input<"tag">())
.build();
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(400));
net.stop();
}
-80
View File
@@ -1,80 +0,0 @@
// Example 05 — Error Handling & Diagnostics
//
// Demonstrates observable failure modes and the diagnostics system:
//
// 1. ChannelOverflowError — a fast producer saturates a slow consumer.
// When the channel is full, push() throws ChannelOverflowError.
// The node's run_loop catches it and prints to stderr.
// Channel statistics (overflows, peak fill) accumulate for the report.
//
// 2. Custom diagnostics handler — instead of the default periodic table,
// install a handler that surfaces only the metrics you care about.
//
// 3. net.print_diagnostics() — print a full report at any time.
//
// Pipeline: [producer] --int--> [slow_consumer]
#include <kpn/kpn.hpp>
#include <chrono>
#include <iostream>
#include <thread>
// ── Node functions ────────────────────────────────────────────────────────────
// Produces at ~100/s — faster than the consumer can keep up (50 ms each)
static int producer() {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
static int n = 0;
return ++n;
}
// Slow consumer: 50 ms per item — will cause channel to fill and overflow
static void slow_consumer(int x) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::cout << "[consumed] " << x << '\n';
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// Capacity=4: fills up quickly when producer outpaces consumer 5:1
auto prod = make_node<producer> (out<"v">{}, /*capacity=*/4);
auto cons = make_node<slow_consumer> (in<"v">{}, /*capacity=*/4);
Network net;
// [snippet: diagnostics_handler]
// Custom diagnostics handler — fires on the watchdog interval.
// Print a concise one-liner rather than the full table.
net.set_diagnostics_handler([](const std::vector<NodeSnapshot>& nodes,
const std::vector<ChannelSnapshot>& channels) {
std::cout << "[diag] ";
for (auto& n : nodes)
std::cout << n.name << "=" << n.throughput_fps << "fps ";
for (auto& c : channels)
std::cout << "channel fill=" << static_cast<int>(c.fill_pct()) << "% "
<< "overflows=" << c.overflows;
std::cout << '\n';
});
// [/snippet: diagnostics_handler]
net.set_watchdog_interval(std::chrono::milliseconds(200));
net.add("prod", prod)
.add("cons", cons)
.connect("prod", prod.template output<"v">(), "cons", cons.template input<"v">())
.build();
std::cout << "producer: 10ms/item, consumer: 50ms/item, capacity=4\n"
<< "Overflow messages appear on stderr; diagnostics on stdout.\n\n";
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(800));
net.stop();
// Full report after shutdown — overflow and drop counts are preserved
std::cout << '\n';
net.print_diagnostics();
}
-90
View File
@@ -1,90 +0,0 @@
// Example 06 — Watchdog & Diagnostics (+ optional web debug UI)
//
// A four-stage pipeline with deliberately uneven node speeds:
//
// [source] --int--> [fast_filter] --int--> [slow_transform] --int--> [sink]
//
// The watchdog fires every second and prints a full diagnostics report:
// - frames processed, throughput fps, exec time, blocked time per node
// - channel fill %, peak fill %, pushes, overflows, bandwidth
// - bottleneck hint (node with highest avg exec time)
//
// "slow_transform" sleeps 30 ms per item, making it the obvious bottleneck.
// Watch the channel upstream of it saturate and the fps converge to ~33.
//
// Build with -DKPN_WEB_DEBUG=ON to also get a live D3 graph at localhost:9090.
#include <kpn/kpn.hpp>
#include <chrono>
#include <cmath>
#include <iostream>
#include <thread>
// ── Node functions ────────────────────────────────────────────────────────────
static int source() {
// Produces at ~50 fps — faster than slow_transform (33 fps) so the
// channel between filter and slow gradually fills, but not catastrophically
std::this_thread::sleep_for(std::chrono::milliseconds(20));
static int n = 0;
return ++n;
}
static int fast_filter(int x) {
// Trivial work: ~0.1 ms
return (x % 2 == 0) ? x : x + 1;
}
static int slow_transform(int x) {
// Simulates expensive processing (e.g. a neural net inference step)
std::this_thread::sleep_for(std::chrono::milliseconds(30));
return x * x;
}
static void sink(int x) {
// Print every 10th result to avoid flooding the terminal
if (x % 100 < 4)
std::cout << "[sink] " << x << '\n';
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// Larger capacity buffers so the fast nodes don't immediately overflow
auto src = make_node<source> (out<"v">{}, 16);
auto filt = make_node<fast_filter> (in<"v">{}, out<"v">{}, 16);
auto slow = make_node<slow_transform> (in<"v">{}, out<"v">{}, 8);
auto snk = make_node<sink> (in<"v">{}, 8);
Network net;
// Watchdog fires every 1 second — prints the built-in diagnostics table
// including the "Bottleneck hint" line
net.set_watchdog_interval(std::chrono::milliseconds(1000));
net.add("source", src)
.add("filter", filt)
.add("slow", slow)
.add("sink", snk)
.connect("source", src.template output<"v">(), "filter", filt.template input<"v">())
.connect("filter", filt.template output<"v">(), "slow", slow.template input<"v">())
.connect("slow", slow.template output<"v">(), "sink", snk.template input<"v">())
.build();
std::cout << "Running for 30 seconds — watch 'slow' become the bottleneck.\n"
<< "Watchdog diagnostics print every 1 second.\n";
#ifdef KPN_WEB_DEBUG
net.set_web_debug_port(9090);
std::cout << "Web debug UI: http://localhost:9090 (live graph, auto-updates every 500 ms)\n";
#endif
std::cout << '\n';
net.start();
std::this_thread::sleep_for(std::chrono::seconds(30));
net.stop();
std::cout << "\n=== Final diagnostics ===\n";
net.print_diagnostics();
}
-32
View File
@@ -1,32 +0,0 @@
"""
07_python_network hello pipeline with a Python node in the middle.
Graph:
[ProduceNode] --int--> [py_double] --int--> [PrintItNode]
ProduceNode and PrintItNode are C++ nodes wrapped in VariantNodeWrapper.
py_double is a pure Python callable doubles its input using Python arithmetic.
"""
import sys
import time
sys.path.insert(0, "build/python")
import kpn_python as kpn
def py_double(x: int) -> int:
return x * 2
net = kpn.Network()
net.add("src", kpn.make_produce())
net.add_node("dbl", py_double, inputs=["int"], outputs=["int"])
net.add("sink", kpn.make_print_it())
net.connect("src", 0, "dbl", 0)
net.connect("dbl", 0, "sink", 0)
net.build()
net.start()
time.sleep(0.1)
net.stop()
-38
View File
@@ -1,38 +0,0 @@
"""
08_python_subport tap a C++ node's output from Python using net.read().
Graph:
[ProduceNode] --int--> [DoubleItNode] --int--> (tapped by net.read())
The sink is Python: instead of connecting a PrintItNode, we call net.read()
to pull values out of DoubleItNode's output directly into Python.
We also demonstrate net.write() by injecting a value into DoubleItNode's input.
"""
import sys
import time
import threading
sys.path.insert(0, "build/python")
import kpn_python as kpn
net = kpn.Network()
net.add("src", kpn.make_produce())
net.add("dbl", kpn.make_double_it())
net.connect("src", 0, "dbl", 0)
net.build()
net.start()
# Collect a few values from DoubleItNode's output via Python tap
results = []
for _ in range(5):
val = net.read("dbl", 0)
results.append(val)
net.stop()
print("values read from C++ DoubleItNode output:", results)
assert all(v == 84 for v in results), f"expected all 84, got {results}"
print("all correct (42 * 2 = 84)")
@@ -1,53 +0,0 @@
#include <opencv2/videoio.hpp>
#include <chrono>
#include <algorithm>
#include <numeric>
#include <vector>
#include <cstdio>
static void bench(const char* name, int device, int backend, int W, int H, int frames) {
cv::VideoCapture cap(device, backend);
if (!cap.isOpened()) {
std::printf("%-12s failed to open /dev/video%d\n", name, device);
return;
}
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
// Warm up
for (int i = 0; i < 5; ++i) cap.grab();
std::vector<double> times;
times.reserve(frames);
for (int i = 0; i < frames; ++i) {
auto t0 = std::chrono::steady_clock::now();
bool ok = cap.grab();
double ms = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - t0).count();
if (ok) times.push_back(ms);
}
cap.release();
if (times.empty()) {
std::printf("%-12s no frames captured\n", name);
return;
}
std::sort(times.begin(), times.end());
double avg = std::accumulate(times.begin(), times.end(), 0.0) / times.size();
double mn = times.front();
double mx = times.back();
double p95 = times[times.size() * 95 / 100];
std::printf("%-12s avg=%6.1fms min=%5.1fms p95=%6.1fms max=%6.1fms (%zu frames)\n",
name, avg, mn, p95, mx, times.size());
}
int main(int argc, char** argv) {
int device = argc > 1 ? std::atoi(argv[1]) : 0;
int frames = argc > 2 ? std::atoi(argv[2]) : 60;
int W = 1920, H = 1080;
std::printf("Benchmarking /dev/video%d at %dx%d, %d frames each\n\n", device, W, H, frames);
bench("V4L2", device, cv::CAP_V4L2, W, H, frames);
bench("GStreamer", device, cv::CAP_GSTREAMER, W, H, frames);
return 0;
}
@@ -1,101 +0,0 @@
"""
09_opencv_cellshade/example_hybrid.py
Hybrid cell-shading pipeline: C++ nodes handle capture, grayscale conversion,
and edge detection; a Python/numpy function replaces the C++ quantise node;
Python drives the display loop using cv2.
Pipeline:
[py_quantise]
[CaptureNode] [CompositeNode]result cv2.imshow
out0=colour [ToGrayNode][EdgesNode] edges cv2.imshow
out1=grey
For a pure-C++ version see main.cpp; for the C++ static-network version see
12_static_cellshade/main.cpp.
Press 'q' or Esc to stop.
"""
import sys
import os
# Adjust path to wherever CMake placed the .so
BUILD_DIR = os.environ.get("KPN_BUILD_DIR",
os.path.join(os.path.dirname(__file__),
"../../build/examples"))
sys.path.insert(0, BUILD_DIR)
import numpy as np
import cv2
import kpn_opencv as kpn
# ── Python node: replace the C++ quantise with numpy ─────────────────────────
# Receives and returns a BGR numpy array (H×W×3 uint8).
def py_quantise(bgr: np.ndarray) -> np.ndarray:
levels = 4
step = 256 // levels
q = (bgr.astype(np.int32) // step) * step + (step // 2)
return q.clip(0, 255).astype(np.uint8)
# ── Build network ─────────────────────────────────────────────────────────────
net = kpn.Network()
net.add("src", kpn.make_capture()) # out0=colour, out1=grey
net.add_node("quant", py_quantise, # Python node — numpy in/out
inputs=["mat"], outputs=["mat"])
net.add("gray", kpn.make_to_gray()) # in0=bgr → out0=gray
net.add("edges", kpn.make_edges()) # in0=gray → out0=edge_mask
net.add("comp", kpn.make_composite()) # in0=edge_mask, in1=colour
# out0=result, out1=edge_mask
# src.colour → py_quantise
net.connect("src", 0, "quant", 0)
# src.grey → to_gray
net.connect("src", 1, "gray", 0)
# gray → edges
net.connect("gray", 0, "edges", 0)
# quantised colour → composite.colour (input slot 1)
net.connect("quant", 0, "comp", 1)
# edge mask → composite.edges (input slot 0)
net.connect("edges", 0, "comp", 0)
net.build()
net.start()
# ── Display loop (drives GUI on this thread) ──────────────────────────────────
cv2.namedWindow("Cell Shade (Python quant)", cv2.WINDOW_NORMAL)
cv2.namedWindow("Edge Mask", cv2.WINDOW_NORMAL)
cv2.resizeWindow("Cell Shade (Python quant)", 1280, 720)
cv2.resizeWindow("Edge Mask", 640, 360)
try:
while True:
# Blocking reads — GIL released while waiting so C++ threads can run
result = net.read("comp", 0) # composite frame (BGR numpy array)
edges = net.read("comp", 1) # edge mask (grayscale numpy array)
cv2.imshow("Cell Shade (Python quant)", result)
cv2.imshow("Edge Mask", cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR))
key = cv2.waitKey(1)
if key in (ord('q'), 27):
break
# Check windows still open
try:
if cv2.getWindowProperty("Cell Shade (Python quant)",
cv2.WND_PROP_VISIBLE) < 1:
break
except cv2.error:
break
finally:
net.stop()
cv2.destroyAllWindows()
del net # let C++ destructor run before nanobind tears down
-177
View File
@@ -1,177 +0,0 @@
#define KPN_BUILD_PYTHON
#include <kpn/python/auto_bind.hpp>
#include <nanobind/ndarray.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <chrono>
#include <cmath>
#include <iostream>
#include <thread>
#include <tuple>
namespace nb = nanobind;
using namespace kpn;
using namespace kpn::python;
// ── PythonConverter<cv::Mat> ──────────────────────────────────────────────────
// Converts cv::Mat ↔ numpy array (uint8, HxW or HxWxC shape).
//
// to_python: clones the mat onto the heap; the numpy array owns it via a
// capsule deleter — no shared cv::Mat refcount dangling after the Variant dies.
// from_python: calls numpy.ascontiguousarray, then clones into an owned cv::Mat.
namespace kpn {
template<> struct PythonConverter<cv::Mat> {
static constexpr const char* type_name = "mat";
static nb::object to_python(const cv::Mat& m) {
// Must be called with the GIL held (always true: called from read() or
// from within the gil_scoped_acquire block in PyNode::run_loop).
auto np = nb::module_::import_("numpy");
cv::Mat c = m.clone(); // ensure contiguous, independently owned
nb::bytes raw(reinterpret_cast<const char*>(c.data),
c.total() * c.elemSize());
nb::object arr = np.attr("frombuffer")(raw, "uint8");
int H = c.rows, W = c.cols, C = c.channels();
arr = arr.attr("reshape")(
C > 1 ? nb::make_tuple(H, W, C) : nb::make_tuple(H, W));
return arr.attr("copy")(); // writable, lifetime-independent copy
}
static cv::Mat from_python(nb::object o) {
auto np = nb::module_::import_("numpy");
// Ensure contiguous uint8 layout (in-place if already compatible)
nb::object arr = np.attr("ascontiguousarray")(o, "uint8");
auto shape = nb::cast<std::vector<int>>(arr.attr("shape"));
if (shape.size() < 2 || shape.size() > 3)
throw std::runtime_error(
"cv::Mat from_python: expected 2D (H×W) or 3D (H×W×C) uint8 array");
int H = shape[0], W = shape[1];
int C = (shape.size() == 3) ? shape[2] : 1;
int type = C > 1 ? CV_8UC(C) : CV_8UC1;
// Cast to ndarray to get the raw data pointer
auto binfo = nb::cast<nb::ndarray<nb::numpy, uint8_t>>(arr);
cv::Mat wrap(H, W, type, binfo.data());
return wrap.clone(); // own the pixel data
}
};
} // namespace kpn
// ── Pipeline functions ────────────────────────────────────────────────────────
static cv::Mat make_gradient(int W, int H) {
cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128));
for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W);
for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H);
cv::Mat channels[3] = {b, yg, xr};
cv::Mat grad;
cv::merge(channels, 3, grad);
return grad;
}
static std::tuple<cv::Mat, cv::Mat> capture() {
constexpr int W = 640, H = 480;
static cv::VideoCapture cap;
static bool opened = false;
if (!opened) {
opened = true;
cap.open(0, cv::CAP_V4L2);
if (cap.isOpened()) {
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
} else {
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
}
}
cv::Mat frame;
if (cap.isOpened()) {
auto t0 = std::chrono::steady_clock::now();
cap >> frame;
auto elapsed = std::chrono::steady_clock::now() - t0;
if (elapsed < std::chrono::milliseconds(20))
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
} else {
static int tick = 0;
static cv::Mat grad = make_gradient(W, H);
++tick;
frame = grad.clone();
int r = 150 + (tick % 80) * 4;
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
}
return {frame.clone(), frame.clone()};
}
static cv::Mat to_gray(cv::Mat bgr) {
cv::Mat gray;
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
return gray;
}
static cv::Mat edges_fn(cv::Mat gray) {
cv::Mat blurred, mask;
cv::GaussianBlur(gray, blurred, {5, 5}, 0);
cv::Canny(blurred, mask, 50, 150);
return mask;
}
static cv::Mat quantise(cv::Mat bgr) {
constexpr int levels = 4;
constexpr double step = 256.0 / levels;
static const cv::Mat lut = []() {
cv::Mat l(1, 256, CV_8UC1);
for (int i = 0; i < 256; ++i)
l.at<uchar>(i) = cv::saturate_cast<uchar>(
std::floor(i / step) * step + step / 2.0);
return l;
}();
cv::Mat out;
cv::LUT(bgr, lut, out);
return out;
}
// Returns composite frame AND edge mask so the display node can show both
// without needing a fan-out on the edges channel.
static std::tuple<cv::Mat, cv::Mat> composite(cv::Mat edge_mask, cv::Mat colour) {
cv::Mat result = colour.clone();
result.setTo(cv::Scalar(0, 0, 0), edge_mask);
return {result, edge_mask};
}
// ── Registry ──────────────────────────────────────────────────────────────────
// Variant deduced as std::variant<cv::Mat> — every node uses only cv::Mat.
using CvNodes = NodeRegistry<
Entry<capture, "capture">,
Entry<to_gray, "to_gray">,
Entry<edges_fn, "edges">,
Entry<quantise, "quantise">,
Entry<composite, "composite">
>;
// ── Module ────────────────────────────────────────────────────────────────────
NB_MODULE(kpn_opencv, m) {
m.doc() = "KPN++ OpenCV bindings for the cell-shading pipeline";
// Registers: Network, INode, CaptureNode, ToGrayNode, EdgesNode,
// QuantiseNode, CompositeNode, and make_<name>() factories.
// Network.add_node(name, callable, inputs=["mat"], outputs=["mat"])
// accepts Python callables that receive/return numpy uint8 arrays.
bind_network<CvNodes>(m);
// Note: bind_debug is omitted here — cv::Mat functions cannot be called
// directly from Python without the variant/network machinery. Use
// net.write() + net.read() to inject/inspect individual nodes instead.
}
-206
View File
@@ -1,206 +0,0 @@
#include <kpn/kpn.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
#include <iostream>
#include <tuple>
#include <thread>
#include <chrono>
// Teach KPN how many bytes a cv::Mat actually carries (header + pixel data).
template<>
struct kpn::ChannelDataSize<cv::Mat> {
static std::size_t bytes(const cv::Mat& m) { return m.total() * m.elemSize(); }
};
// ── Cell-shading pipeline ─────────────────────────────────────────────────────
//
// [capture] --"colour"--> [quantise] ──────────────────────────┐
// ├──> [composite] ──"result"──┐
// [capture] --"grey"---> [to_gray] --> [edges] ──"edges"───────┘ │
// └──────────────────────────────"edges"──────────┴──> [display]
//
// DisplayNode derives from MainThreadNode<> — two inputs (composite + raw edges),
// two windows opened in the constructor. step() is called on the main thread.
// ── Gradient base for synthetic pattern ──────────────────────────────────────
static cv::Mat make_gradient(int W, int H) {
cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128));
for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W);
for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H);
cv::Mat channels[3] = {b, yg, xr};
cv::Mat grad;
cv::merge(channels, 3, grad);
return grad;
}
// ── Pipeline functions ────────────────────────────────────────────────────────
// [snippet: capture_fn]
static std::tuple<cv::Mat, cv::Mat> capture() {
constexpr int W = 640, H = 480;
static cv::VideoCapture cap;
static bool opened = false;
if (!opened) {
opened = true;
cap.open(0, cv::CAP_V4L2);
if (cap.isOpened()) {
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
} else {
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
}
}
cv::Mat frame;
if (cap.isOpened()) {
auto t0 = std::chrono::steady_clock::now();
cap >> frame;
auto elapsed = std::chrono::steady_clock::now() - t0;
if (elapsed < std::chrono::milliseconds(20))
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
} else {
static int tick = 0;
static cv::Mat grad = make_gradient(W, H);
++tick;
frame = grad.clone();
int r = 150 + (tick % 80) * 4;
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
}
return {frame.clone(), frame.clone()};
}
// [/snippet: capture_fn]
static cv::Mat to_gray(cv::Mat bgr) {
cv::Mat gray;
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
return gray;
}
static cv::Mat edges_fn(cv::Mat gray) {
cv::Mat blurred, mask;
cv::GaussianBlur(gray, blurred, {5, 5}, 0);
cv::Canny(blurred, mask, 50, 150);
return mask;
}
static cv::Mat quantise(cv::Mat bgr) {
constexpr int levels = 4;
constexpr double step = 256.0 / levels;
static const cv::Mat lut = []() {
cv::Mat l(1, 256, CV_8UC1);
for (int i = 0; i < 256; ++i)
l.at<uchar>(i) = cv::saturate_cast<uchar>(
std::floor(i / step) * step + step / 2.0);
return l;
}();
cv::Mat out;
cv::LUT(bgr, lut, out);
return out;
}
// Returns both the composite frame and the original edge mask so downstream
// nodes (display) can receive both without fan-out on the edges channel.
static std::tuple<cv::Mat, cv::Mat> composite(cv::Mat edge_mask, cv::Mat colour) {
cv::Mat result = colour.clone();
result.setTo(cv::Scalar(0, 0, 0), edge_mask);
return {result, edge_mask};
}
// ── DisplayNode ───────────────────────────────────────────────────────────────
//
// Two inputs: the cell-shaded composite frame and the raw edge mask.
// MainThreadNode<> provides channel ownership, INode boilerplate, and stats.
// The constructor opens both windows on the main thread (Wayland requirement).
// operator() is called by step() whenever both channels have a frame ready.
// [snippet: display_node]
class DisplayNode : public kpn::MainThreadNode<DisplayNode,
kpn::in<"composite", "edges">,
cv::Mat, cv::Mat> {
public:
DisplayNode() : MainThreadNode(8) {
cv::namedWindow("Cell Shade", cv::WINDOW_NORMAL);
cv::namedWindow("Edge Mask", cv::WINDOW_NORMAL);
cv::resizeWindow("Cell Shade", 1280, 720);
cv::resizeWindow("Edge Mask", 640, 360);
}
~DisplayNode() { cv::destroyAllWindows(); }
bool operator()(cv::Mat composite, cv::Mat edges) {
cv::imshow("Cell Shade", composite);
cv::Mat edges_bgr;
cv::cvtColor(edges, edges_bgr, cv::COLOR_GRAY2BGR);
cv::imshow("Edge Mask", edges_bgr);
int key = cv::waitKey(1);
if (key == 'q' || key == 27) return false;
return window_open("Cell Shade") && window_open("Edge Mask");
}
private:
static bool window_open(const char* name) {
try { return cv::getWindowProperty(name, cv::WND_PROP_VISIBLE) >= 1; }
catch (const cv::Exception&) { return false; }
}
};
// [/snippet: display_node]
// ─────────────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// [snippet: opencv_network]
auto src = make_node<capture> (out<"colour","grey">{}, 8);
auto gray_node = make_node<to_gray> (in<"bgr">{}, out<"gray">{}, 8);
auto edge_node = make_node<edges_fn> (in<"gray">{}, out<"edges">{}, 8);
auto quant = make_node<quantise> (in<"bgr">{}, out<"quantised">{}, 8);
auto comp = make_node<composite>(in<"edges","colour">{}, out<"result","edges">{}, 8);
// DisplayNode: two windows opened in constructor, step() drives main thread.
DisplayNode disp;
Network net;
net.add("src", src)
.add("gray", gray_node)
.add("edges", edge_node)
.add("quant", quant)
.add("comp", comp)
.add("display", disp)
.connect("src", src.template output<"colour">(), "quant", quant.template input<"bgr">())
.connect("quant", quant.template output<"quantised">(), "comp", comp.template input<"colour">())
.connect("src", src.template output<"grey">(), "gray", gray_node.template input<"bgr">())
.connect("gray", gray_node.template output<"gray">(), "edges", edge_node.template input<"gray">())
.connect("edges", edge_node.template output<"edges">(), "comp", comp.template input<"edges">())
.connect("comp", comp.template output<"result">(), "display", disp.template input<"composite">())
.connect("comp", comp.template output<"edges">(), "display", disp.template input<"edges">())
.build();
// [/snippet: opencv_network]
net.set_watchdog_interval(std::chrono::milliseconds(5000));
#ifdef KPN_WEB_DEBUG
net.set_web_debug_port(9090);
#endif
std::cout << "Cell-shading pipeline running. Press 'q' to stop.\n";
std::cout << "Web debug UI: http://localhost:9090\n";
// [snippet: main_thread_step]
net.start();
// Main thread drives display — imshow/waitKey stay on the GUI thread.
// step() returns false when operator() returns false (q pressed / window closed).
while (disp.step())
cv::waitKey(8); // yield event loop when no frame ready
net.stop();
// [/snippet: main_thread_step]
return 0;
}
@@ -1,34 +0,0 @@
// Example 10 — Static Hello Pipeline
//
// The same linear pipeline as example 01, built with make_network() instead
// of the runtime Network builder. The topology is fully known at compile time:
// cycle detection is a static_assert, no build() step is needed, and start/stop
// require no virtual dispatch through a string-keyed node map.
//
// [produce] --int--> [double_it] --int--> [print_it]
#include <kpn/kpn.hpp>
#include <chrono>
#include <iostream>
#include <thread>
static int produce() { return 42; }
static int double_it(int x) { return x * 2; }
static void print_it(int x) { std::cout << "result: " << x << '\n'; }
int main() {
using namespace kpn;
auto src = make_node<produce, "src" >(5);
auto dbl = make_node<double_it, "dbl" >(5);
auto sink = make_node<print_it, "sink">(5);
auto net = make_network(
edge(src.output<0>(), dbl.input<0>()),
edge(dbl.output<0>(), sink.input<0>())
);
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
net.stop();
}
-55
View File
@@ -1,55 +0,0 @@
// Example 11 — Static Fan-Out (automatic FanoutNode insertion)
//
// Two consumers read from the same output port of [generate]. With the runtime
// Network builder this would require an explicit make_fanout<>. With make_network()
// the duplicate source port is detected at compile time and a FanoutNode<string,2>
// is inserted automatically — the user just writes two edges from the same port.
//
// +--> [print_key]
// [generate] --string--> [fan]
// +--> [print_upper]
//
// The FanoutNode<string,2> is owned by the StaticNetwork and invisible to the user.
#include <kpn/kpn.hpp>
#include <algorithm>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
static int gen_index = 0;
static std::string generate() {
static const char* words[] = {"hello", "kpn", "fanout", "static", "network"};
std::this_thread::sleep_for(std::chrono::milliseconds(80));
return words[gen_index++ % 5];
}
static void print_lower(std::string s) {
std::cout << "lower: " << s << '\n';
}
static void print_upper(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), ::toupper);
std::cout << "upper: " << s << '\n';
}
int main() {
using namespace kpn;
auto gen = make_node<generate, "gen" >(5);
auto lower = make_node<print_lower, "lower">(5);
auto upper = make_node<print_upper, "upper">(5);
// Two edges from gen.output<0>() — make_network() detects the fan-out
// and inserts FanoutNode<std::string, 2> automatically.
auto net = make_network(
edge(gen.output<0>(), lower.input<0>()),
edge(gen.output<0>(), upper.input<0>())
);
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(500));
net.stop();
}
-217
View File
@@ -1,217 +0,0 @@
// Example 12 — Static Cell-Shading Pipeline with Auto Fan-Out
//
// The same cell-shading effect as example 09, rebuilt with make_network().
//
// Key differences from example 09:
//
// 1. Fan-out is automatic. The edge detector output feeds both the compositing
// node and the debug display window. In example 09 this required composite()
// to re-output the edge mask as a second return value (a workaround). Here
// make_network() detects the duplicate source port and inserts
// FanoutNode<cv::Mat, 2> automatically — composite() is a clean single-output
// node.
//
// 2. No add()/connect()/build() ceremony. The full topology is expressed once
// in the make_network() call. Cycle detection and duplicate-tag checking are
// compile-time static_asserts.
//
// 3. Every node has a Label NTTP so the web debug UI shows real names.
//
// Topology:
//
// [capture] --colour--> [quant] ──────────────────────────────> [comp] --> [display_composite]
// [capture] --grey----> [to_gray] --> [edges] --edges--(fan)--> [comp]
// --edges----------> [display_edges] ← auto-fanout
//
// Note: capture returns std::tuple<cv::Mat, cv::Mat> (colour, grey).
// The two outputs are separate ports routed independently.
#include <kpn/kpn.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
#include <iostream>
#include <tuple>
#include <thread>
#include <chrono>
// ── Gradient base for synthetic pattern ───────────────────────────────────────
static cv::Mat make_gradient(int W, int H) {
cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128));
for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W);
for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H);
cv::Mat channels[3] = {b, yg, xr};
cv::Mat grad;
cv::merge(channels, 3, grad);
return grad;
}
// ── Pipeline functions ────────────────────────────────────────────────────────
static std::tuple<cv::Mat, cv::Mat> capture() {
constexpr int W = 640, H = 480;
static cv::VideoCapture cap;
static bool opened = false;
if (!opened) {
opened = true;
cap.open(0, cv::CAP_V4L2);
if (cap.isOpened()) {
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
} else {
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
}
}
cv::Mat frame;
if (cap.isOpened()) {
auto t0 = std::chrono::steady_clock::now();
cap >> frame;
auto elapsed = std::chrono::steady_clock::now() - t0;
if (elapsed < std::chrono::milliseconds(20))
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
} else {
static int tick = 0;
static cv::Mat grad = make_gradient(W, H);
++tick;
frame = grad.clone();
int r = 150 + (tick % 80) * 4;
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
}
return {frame.clone(), frame.clone()};
}
static cv::Mat to_gray(cv::Mat bgr) {
cv::Mat gray;
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
return gray;
}
static cv::Mat edges_fn(cv::Mat gray) {
cv::Mat blurred, mask;
cv::GaussianBlur(gray, blurred, {5, 5}, 0);
cv::Canny(blurred, mask, 50, 150);
return mask;
}
static cv::Mat quantise(cv::Mat bgr) {
constexpr int levels = 4;
constexpr double step = 256.0 / levels;
static const cv::Mat lut = []() {
cv::Mat l(1, 256, CV_8UC1);
for (int i = 0; i < 256; ++i)
l.at<uchar>(i) = cv::saturate_cast<uchar>(
std::floor(i / step) * step + step / 2.0);
return l;
}();
cv::Mat out;
cv::LUT(bgr, lut, out);
return out;
}
// Clean single-output composite — no longer needs to pass edges through.
static cv::Mat composite(cv::Mat edge_mask, cv::Mat colour) {
cv::Mat result = colour.clone();
result.setTo(cv::Scalar(0, 0, 0), edge_mask);
return result;
}
// ── Display nodes ─────────────────────────────────────────────────────────────
//
// Two separate MainThreadNode subclasses — one for the composited result,
// one for the raw edge mask. Each runs on the main thread via step().
// The fan-out from [edges] to both consumers is inserted automatically by
// make_network().
class DisplayComposite : public kpn::MainThreadNode<DisplayComposite,
kpn::in<"composite">,
cv::Mat> {
public:
// Label and unique_tag for StaticNetwork identity
static constexpr std::string_view label() { return "display_composite"; }
static constexpr std::size_t unique_tag = 0;
DisplayComposite() : MainThreadNode(8) {
cv::namedWindow("Cell Shade", cv::WINDOW_NORMAL);
cv::resizeWindow("Cell Shade", 1280, 720);
}
~DisplayComposite() { cv::destroyWindow("Cell Shade"); }
bool operator()(cv::Mat frame) {
cv::imshow("Cell Shade", frame);
int key = cv::waitKey(1);
if (key == 'q' || key == 27) return false;
try { return cv::getWindowProperty("Cell Shade", cv::WND_PROP_VISIBLE) >= 1; }
catch (const cv::Exception&) { return false; }
}
};
class DisplayEdges : public kpn::MainThreadNode<DisplayEdges,
kpn::in<"edges">,
cv::Mat> {
public:
static constexpr std::string_view label() { return "display_edges"; }
static constexpr std::size_t unique_tag = 1;
DisplayEdges() : MainThreadNode(8) {
cv::namedWindow("Edge Mask", cv::WINDOW_NORMAL);
cv::resizeWindow("Edge Mask", 640, 360);
}
~DisplayEdges() { cv::destroyWindow("Edge Mask"); }
bool operator()(cv::Mat mask) {
cv::Mat bgr;
cv::cvtColor(mask, bgr, cv::COLOR_GRAY2BGR);
cv::imshow("Edge Mask", bgr);
cv::waitKey(1);
try { return cv::getWindowProperty("Edge Mask", cv::WND_PROP_VISIBLE) >= 1; }
catch (const cv::Exception&) { return false; }
}
};
// ─────────────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
// Nodes — all labelled for web debug UI
auto src = make_node<capture, "capture">(8);
auto gray_node = make_node<to_gray, "to_gray">(8);
auto edge_node = make_node<edges_fn, "edges" >(8);
auto quant = make_node<quantise, "quant" >(8);
auto comp = make_node<composite, "comp" >(8);
// DisplayNodes live on the main thread — registered as sinks
DisplayComposite disp_comp;
DisplayEdges disp_edges;
// make_network() detects that edge_node.output<0>() feeds two consumers
// (comp and disp_edges) and inserts FanoutNode<cv::Mat, 2> automatically.
auto net = make_network(
edge(src.output<0>(), quant.input<0>()), // colour → quant
edge(src.output<1>(), gray_node.input<0>()), // grey → to_gray
edge(gray_node.output<0>(), edge_node.input<0>()), // gray → edges
edge(edge_node.output<0>(), comp.input<0>()), // edges → comp (fan-out src)
edge(edge_node.output<0>(), disp_edges.input<0>()), // edges → display_edges (auto fanout)
edge(quant.output<0>(), comp.input<1>()), // quantised → comp
edge(comp.output<0>(), disp_comp.input<0>()) // result → display_composite
);
std::cout << "Cell-shading pipeline (static) running. Press 'q' to stop.\n";
net.start();
// Main thread drives both display nodes — step() returns false when
// operator() returns false (q pressed or window closed).
while (disp_comp.step() && disp_edges.step())
cv::waitKey(8);
net.stop();
return 0;
}
-288
View File
@@ -1,288 +0,0 @@
// Example 13 — Debug Cell-Shading Pipeline with Tiled Debug Canvas
//
// An improved cell-shading pipeline where every processing node performs
// exactly one OpenCV operation. A variadic DebugCanvas<N> node tiles N
// cv::Mat inputs into a single debug window, making each pipeline stage
// visible side-by-side at runtime.
//
// Improvements over example 12:
// - Bilateral filter before quantisation (edge-preserving smoothing):
// flattens colour regions without softening object edges
// - Dilated edge mask for bolder black outlines
// - 6-level quantisation for richer tonal detail
//
// Topology (auto-fanouts inserted by make_network):
//
// [capture]──┬──> [bilateral]──> [quant]──┬──> [comp]──> [debug:0 result]
// │ └──> [debug:1 quantised]
// ├──> [to_gray]──> [blur]──> [canny]──┬──> [dilate]──> [comp]
// │ └──> [debug:2 edges]
// └──> [debug:3 original]
//
// make_network detects the 3-way fan from [capture], the 2-way fan from
// [quant], and the 2-way fan from [canny], inserting FanoutNode instances
// automatically.
#include <kpn/kpn.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iostream>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
// ── Synthetic source for environments without a webcam ───────────────────────
static cv::Mat make_gradient(int W, int H) {
cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128));
for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W);
for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H);
cv::Mat channels[3] = {b, yg, xr};
cv::Mat grad;
cv::merge(channels, 3, grad);
return grad;
}
// ── Pipeline nodes — one cv:: call per function ───────────────────────────────
static cv::Mat capture() {
constexpr int W = 640, H = 480;
static cv::VideoCapture cap;
static bool opened = false;
if (!opened) {
opened = true;
cap.open(0, cv::CAP_V4L2);
if (cap.isOpened()) {
cap.set(cv::CAP_PROP_FRAME_WIDTH, W);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, H);
} else {
std::cerr << "[capture] no webcam — using synthetic animated pattern\n";
}
}
cv::Mat frame;
if (cap.isOpened()) {
auto t0 = std::chrono::steady_clock::now();
cap >> frame;
auto elapsed = std::chrono::steady_clock::now() - t0;
if (elapsed < std::chrono::milliseconds(20))
std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed);
if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3);
} else {
static int tick = 0;
static cv::Mat grad = make_gradient(W, H);
++tick;
frame = grad.clone();
int r = 150 + (tick % 80) * 4;
cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1);
cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1);
cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1);
std::this_thread::sleep_for(std::chrono::milliseconds(33));
}
return frame.clone();
}
// Edge-preserving smooth: flattens colour within regions while keeping sharp
// boundaries — much better than Gaussian blur as a pre-quantisation step.
static cv::Mat bilateral_filter(cv::Mat bgr) {
cv::Mat out;
cv::bilateralFilter(bgr, out, 5, 75, 75);
return out;
}
// Snap each channel to N discrete tonal levels.
static cv::Mat quantise(cv::Mat bgr) {
constexpr int levels = 6;
constexpr double step = 256.0 / levels;
static const cv::Mat lut = []() {
cv::Mat l(1, 256, CV_8UC1);
for (int i = 0; i < 256; ++i)
l.at<uchar>(i) = cv::saturate_cast<uchar>(
std::floor(i / step) * step + step / 2.0);
return l;
}();
cv::Mat out;
cv::LUT(bgr, lut, out);
return out;
}
// BGR → greyscale; the edge path works on the original (not bilateral-filtered)
// frame so that fine edge detail is preserved.
static cv::Mat to_gray(cv::Mat bgr) {
cv::Mat gray;
cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
return gray;
}
// Suppress high-frequency noise before the Canny detector.
static cv::Mat gaussian_blur(cv::Mat gray) {
cv::Mat out;
cv::GaussianBlur(gray, out, {5, 5}, 0);
return out;
}
// Detect strong edges; returns a binary mask (CV_8UC1).
static cv::Mat canny_edges(cv::Mat blurred) {
cv::Mat out;
cv::Canny(blurred, out, 50, 150);
return out;
}
// Widen the edge mask for bolder cartoon outlines.
static cv::Mat dilate_edges(cv::Mat edges) {
static const cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, {3, 3});
cv::Mat out;
cv::dilate(edges, out, kernel);
return out;
}
// Burn black outlines into the quantised colour image.
static cv::Mat composite(cv::Mat quantised, cv::Mat thick_edges) {
cv::Mat result = quantised.clone();
result.setTo(cv::Scalar(0, 0, 0), thick_edges);
return result;
}
// ── DebugCanvas<N> ────────────────────────────────────────────────────────────
//
// Variadic MainThreadNode that accepts N cv::Mat inputs (any mix of BGR and
// greyscale) and tiles them into one debug window arranged as a
// ceil(sqrt(N)) × ceil(N/cols) grid.
//
// Template trick: MatArg<I> aliases cv::Mat for all I, so the pack expansion
// MatArg<0>, MatArg<1>, ..., MatArg<N-1>
// produces exactly N cv::Mat arguments — enough to drive the MainThreadNode
// base without manually spelling out the type N times.
template<std::size_t>
using MatArg = cv::Mat;
template<std::size_t N>
class DebugCanvas;
template<std::size_t N, typename Seq = std::make_index_sequence<N>>
struct DebugCanvasBase;
template<std::size_t N, std::size_t... Is>
struct DebugCanvasBase<N, std::index_sequence<Is...>> {
using type = kpn::MainThreadNode<DebugCanvas<N>, kpn::in<>, MatArg<Is>...>;
};
template<std::size_t N>
class DebugCanvas : public DebugCanvasBase<N>::type {
using Base = typename DebugCanvasBase<N>::type;
public:
static constexpr std::string_view label() { return "debug_canvas"; }
static constexpr std::size_t unique_tag = 0;
explicit DebugCanvas(std::vector<std::string> slot_labels = {},
std::size_t fifo_cap = 4)
: Base(fifo_cap), labels_(std::move(slot_labels))
{
cv::namedWindow("Debug Canvas", cv::WINDOW_NORMAL);
cv::resizeWindow("Debug Canvas", cols() * CW, rows() * CH);
}
~DebugCanvas() { cv::destroyWindow("Debug Canvas"); }
// Called by MainThreadNode::step() with exactly N cv::Mat arguments.
template<typename... Ms>
bool operator()(Ms&&... mats) {
static_assert(sizeof...(Ms) == N, "DebugCanvas: wrong number of inputs");
std::vector<cv::Mat> imgs;
imgs.reserve(N);
(imgs.push_back(to_bgr(std::forward<Ms>(mats))), ...);
cv::imshow("Debug Canvas", tile(imgs));
int key = cv::waitKey(1);
if (key == 'q' || key == 27) return false;
try { return cv::getWindowProperty("Debug Canvas", cv::WND_PROP_VISIBLE) >= 1; }
catch (const cv::Exception&) { return false; }
}
private:
static constexpr int CW = 640, CH = 480;
static int cols() { return std::max(1, (int)std::ceil(std::sqrt((double)N))); }
static int rows() { return ((int)N + cols() - 1) / cols(); }
std::vector<std::string> labels_;
static cv::Mat to_bgr(const cv::Mat& m) {
if (m.channels() == 1) {
cv::Mat bgr;
cv::cvtColor(m, bgr, cv::COLOR_GRAY2BGR);
return bgr;
}
return m;
}
cv::Mat tile(const std::vector<cv::Mat>& imgs) const {
const int c = cols(), r = rows();
cv::Mat canvas(r * CH, c * CW, CV_8UC3, cv::Scalar(30, 30, 30));
for (int i = 0; i < (int)imgs.size(); ++i) {
if (imgs[i].empty()) continue;
cv::Mat cell = canvas(cv::Rect((i % c) * CW, (i / c) * CH, CW, CH));
cv::Mat resized;
cv::resize(imgs[i], resized, {CW, CH});
resized.copyTo(cell);
if (i < (int)labels_.size() && !labels_[i].empty())
cv::putText(cell, labels_[i], {8, 36},
cv::FONT_HERSHEY_SIMPLEX, 1.0, {0, 255, 255}, 2,
cv::LINE_AA);
}
return canvas;
}
};
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
using namespace kpn;
auto src = make_node<capture, "capture" >(4);
auto bilateral= make_node<bilateral_filter, "bilateral" >(4);
auto quant = make_node<quantise, "quant" >(4);
auto gray = make_node<to_gray, "to_gray" >(4);
auto blur = make_node<gaussian_blur, "blur" >(4);
auto canny = make_node<canny_edges, "canny" >(4);
auto dilate = make_node<dilate_edges, "dilate" >(4);
auto comp = make_node<composite, "comp" >(4);
DebugCanvas<4> debug({"result", "quantised", "edges", "original"});
// make_network auto-inserts FanoutNode instances wherever a source port
// feeds more than one consumer:
// capture → 3-way fanout (bilateral, to_gray, debug[3])
// quant → 2-way fanout (comp, debug[1])
// canny → 2-way fanout (dilate, debug[2])
auto net = make_network(
edge(src.output<0>(), bilateral.input<0>()), // frame → bilateral
edge(src.output<0>(), gray.input<0>()), // frame → to_gray
edge(src.output<0>(), debug.input<3>()), // frame → debug[3] original
edge(bilateral.output<0>(), quant.input<0>()), // smooth → quant
edge(quant.output<0>(), comp.input<0>()), // quant → comp
edge(quant.output<0>(), debug.input<1>()), // quant → debug[1] quantised
edge(gray.output<0>(), blur.input<0>()), // gray → blur
edge(blur.output<0>(), canny.input<0>()), // blurred→ canny
edge(canny.output<0>(), dilate.input<0>()), // edges → dilate
edge(canny.output<0>(), debug.input<2>()), // edges → debug[2] edges
edge(dilate.output<0>(), comp.input<1>()), // thick → comp
edge(comp.output<0>(), debug.input<0>()) // result → debug[0] result
);
std::cout << "Debug cell-shading pipeline running — press 'q' to stop.\n";
std::cout << "Canvas: [0] result [1] quantised [2] edges [3] original\n";
net.start();
while (debug.step())
cv::waitKey(8);
net.stop();
return 0;
}
-159
View File
@@ -1,159 +0,0 @@
// Example 14 — DebugHub with Shared Resource Token
//
// Two independent KPN networks compete for one shared inference resource
// (simulating a small GPU or single-session ONNX runtime). The DebugHub
// serves a single web UI at http://localhost:9090 with:
//
// [All Networks] — resource utilisation cards + cross-network node table
// [detect] — force-directed graph for the detection pipeline
// [classify] — force-directed graph for the classification pipeline
//
// Topology:
//
// detect pipeline:
// [source_detect] ──> [run_detect] ──> [sink_detect]
//
// classify pipeline:
// [source_classify] ──> [run_classify] ──> [sink_classify]
//
// Both [run_detect] and [run_classify] call gpu.acquire() before touching the
// simulated device. The priority-based token awards the next slot to the
// waiter that is more likely to make useful progress (higher priority score).
//
// Build: cmake -DKPN_WEB_DEBUG=ON .. && cmake --build .
// Run: ./14_debug_hub
// UI: http://localhost:9090
#ifdef KPN_WEB_DEBUG
#include <kpn/kpn.hpp>
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
using namespace kpn;
using namespace std::chrono_literals;
// ── Simulated inference device ────────────────────────────────────────────────
//
// Represents any exclusive, serialised accelerator: GPU session, ONNX runtime,
// hardware encoder, etc. Only one caller can hold it at a time.
struct GPU {
// Detection model: fast, 8 ms per frame.
int detect(int frame_id) {
std::this_thread::sleep_for(8ms);
return frame_id * 2; // synthetic "score"
}
// Classification model: heavier, 14 ms per frame.
int classify(int frame_id) {
std::this_thread::sleep_for(14ms);
return frame_id % 10; // synthetic "label"
}
};
// Global pointer so free-function nodes can reach the resource.
// In production code, capture by reference inside an ObjectNode functor instead.
static SharedResource<GPU>* g_gpu = nullptr;
// ── Detection pipeline ────────────────────────────────────────────────────────
static int source_detect() {
static std::atomic<int> id{0};
std::this_thread::sleep_for(25ms); // ~40 fps source rate
return id.fetch_add(1, std::memory_order_relaxed);
}
static int run_detect(int frame_id) {
// Higher priority: detection is latency-critical.
auto guard = g_gpu->acquire([] { return 0.7f; });
return guard->detect(frame_id);
}
static std::atomic<uint64_t> detect_out{0};
static void sink_detect(int) {
detect_out.fetch_add(1, std::memory_order_relaxed);
}
// ── Classification pipeline ───────────────────────────────────────────────────
static int source_classify() {
static std::atomic<int> id{0};
std::this_thread::sleep_for(40ms); // ~25 fps source rate
return id.fetch_add(1, std::memory_order_relaxed);
}
static int run_classify(int frame_id) {
// Lower priority: classification is best-effort.
auto guard = g_gpu->acquire([] { return 0.3f; });
return guard->classify(frame_id);
}
static std::atomic<uint64_t> classify_out{0};
static void sink_classify(int) {
classify_out.fetch_add(1, std::memory_order_relaxed);
}
// ── main ──────────────────────────────────────────────────────────────────────
int main() {
SharedResource<GPU> gpu;
g_gpu = &gpu;
// ── Detection network ─────────────────────────────────────────────────────
auto src_det = make_node<source_detect, "source_detect">(4);
auto inf_det = make_node<run_detect, "run_detect" >(4);
auto snk_det = make_node<sink_detect, "sink_detect" >(4);
auto net_detect = make_network(
edge(src_det.output<0>(), inf_det.input<0>()),
edge(inf_det.output<0>(), snk_det.input<0>())
);
// ── Classification network ────────────────────────────────────────────────
auto src_cls = make_node<source_classify, "source_classify">(4);
auto inf_cls = make_node<run_classify, "run_classify" >(4);
auto snk_cls = make_node<sink_classify, "sink_classify" >(4);
auto net_classify = make_network(
edge(src_cls.output<0>(), inf_cls.input<0>()),
edge(inf_cls.output<0>(), snk_cls.input<0>())
);
// ── Hub — one debug server for both networks + the shared resource ─────────
DebugHub hub(9090);
hub.register_network("detect", net_detect);
hub.register_network("classify", net_classify);
hub.register_resource("gpu", &gpu);
net_detect.start();
net_classify.start();
hub.start();
std::cout << "Running — open http://localhost:9090\n"
<< "Tabs: [All Networks] [detect] [classify]\n"
<< "Press Enter to stop.\n";
std::cin.get();
net_detect.stop();
net_classify.stop();
std::cout << "\nResults:\n"
<< " detect: " << detect_out.load() << " frames\n"
<< " classify: " << classify_out.load() << " frames\n";
return 0;
}
#else // no KPN_WEB_DEBUG
#include <iostream>
int main() {
std::cerr << "This example requires KPN_WEB_DEBUG.\n"
<< "Rebuild with: cmake -DKPN_WEB_DEBUG=ON ..\n";
return 1;
}
#endif
-73
View File
@@ -1,73 +0,0 @@
// Example 15 — Per-node Error Handler
//
// Demonstrates set_error_handler() for deciding whether a network can
// continue when a node throws an exception.
//
// The "validator" node rejects even numbers by throwing std::runtime_error.
// Its error handler logs the failure and returns true (skip & continue),
// so odd numbers still flow through to the sink.
//
// Compare: a second handler (commented below) returns false instead,
// which stops the node and gracefully shuts the downstream side down.
//
// Pipeline: [source] --int--> [validator] --int--> [sink]
#include <kpn/kpn.hpp>
#include <chrono>
#include <iostream>
#include <thread>
static int counter = 0;
static int source() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
return ++counter;
}
static int validate(int x) {
if (x % 2 == 0)
throw std::runtime_error("even number rejected: " + std::to_string(x));
return x;
}
static int received = 0;
static void sink(int x) {
std::cout << " processed: " << x << '\n';
++received;
}
int main() {
using namespace kpn;
auto src = make_node<source> ();
auto proc = make_node<validate>();
auto snk = make_node<sink> ();
// Return true → skip this invocation, keep the node running.
// Return false → stop the node (downstream drains then also stops).
proc.set_error_handler([](std::string_view name, std::exception_ptr ep) {
try { std::rethrow_exception(ep); }
catch (const std::exception& e) {
std::cerr << "[" << name << "] skipping item — " << e.what() << '\n';
}
return true;
});
Network net;
net.add("source", src)
.add("validator", proc)
.add("sink", snk)
.connect("source", src.output<0>(), "validator", proc.input<0>())
.connect("validator", proc.output<0>(), "sink", snk.input<0>())
.build();
std::cout << "source emits 1..N; validator rejects even numbers.\n"
<< "Error messages on stderr, accepted items on stdout.\n\n";
net.start();
std::this_thread::sleep_for(std::chrono::milliseconds(300));
net.stop();
std::cout << "\nItems accepted by sink: " << received << '\n';
}
-52
View File
@@ -1,52 +0,0 @@
cmake_minimum_required(VERSION 3.21)
function(kpn_example name)
add_executable(${name} ${name}/main.cpp)
target_link_libraries(${name} PRIVATE kpn)
endfunction()
kpn_example(01_hello_pipeline)
kpn_example(02_named_ports)
kpn_example(03_multi_output)
kpn_example(04_storage_policy)
kpn_example(05_error_handling)
kpn_example(06_watchdog)
kpn_example(15_node_error_handler)
kpn_example(10_static_hello_pipeline)
kpn_example(11_static_fanout)
if(KPN_WEB_DEBUG)
kpn_target_enable_web_debug(06_watchdog)
add_executable(14_debug_hub 14_debug_hub/main.cpp)
target_link_libraries(14_debug_hub PRIVATE kpn)
kpn_target_enable_web_debug(14_debug_hub)
endif()
# 07 and 08 are Python scripts no compiled target needed.
# 09 requires OpenCV only build if found
find_package(OpenCV QUIET COMPONENTS core imgproc highgui videoio)
if(OpenCV_FOUND)
# Hybrid Python example: kpn_opencv module (requires both OpenCV and nanobind)
if(KPN_BUILD_PYTHON)
nanobind_add_module(kpn_opencv 09_opencv_cellshade/kpn_opencv.cpp)
target_link_libraries(kpn_opencv PRIVATE kpn ${OpenCV_LIBS})
target_compile_definitions(kpn_opencv PRIVATE KPN_BUILD_PYTHON)
message(STATUS "KPN++ kpn_opencv Python module: building (OpenCV ${OpenCV_VERSION})")
endif()
add_executable(09_opencv_cellshade 09_opencv_cellshade/main.cpp)
target_link_libraries(09_opencv_cellshade PRIVATE kpn ${OpenCV_LIBS})
add_executable(12_static_cellshade 12_static_cellshade/main.cpp)
target_link_libraries(12_static_cellshade PRIVATE kpn ${OpenCV_LIBS})
add_executable(13_debug_cellshade 13_debug_cellshade/main.cpp)
target_link_libraries(13_debug_cellshade PRIVATE kpn ${OpenCV_LIBS})
if(KPN_WEB_DEBUG)
kpn_target_enable_web_debug(09_opencv_cellshade)
kpn_target_enable_web_debug(13_debug_cellshade)
endif()
message(STATUS "KPN++ example 09_opencv_cellshade: OpenCV ${OpenCV_VERSION} found — building")
else()
message(STATUS "KPN++ example 09_opencv_cellshade: OpenCV not found — skipping")
endif()
+1039
View File
File diff suppressed because it is too large Load Diff
+1013
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-301
View File
@@ -1,301 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "inode.hpp"
#include "port.hpp"
#include "traits.hpp"
#include <array>
#include <atomic>
#include <functional>
#include <memory>
#include <thread>
namespace kpn {
// ── RouterNode ────────────────────────────────────────────────────────────────
//
// Reads one item and pushes it to exactly one of N output channels, chosen by
// selector(item). If selector returns >= N the item is silently dropped.
//
// Usage:
// auto router = make_router<Image, 3>(
// [](const Image& img) -> std::size_t { return img.stream_id % 3; });
// net.connect("src", src.output<0>(), "router", router.input<0>())
// .connect("router", router.output<0>(), "nodeA", nodeA.input<0>())
// .connect("router", router.output<1>(), "nodeB", nodeB.input<0>())
// .connect("router", router.output<2>(), "nodeC", nodeC.input<0>());
template<typename T, std::size_t N, std::size_t Id = 0>
class RouterNode : public INode {
public:
using Selector = std::function<std::size_t(const T&)>;
using args_tuple = std::tuple<T>;
using return_tuple = repeat_tuple_t<T, N>;
using return_raw = return_tuple;
static constexpr std::size_t input_count = 1;
static constexpr std::size_t output_count = N;
static constexpr std::size_t unique_tag = Id;
static constexpr bool is_router_node = true;
explicit RouterNode(Selector sel, std::size_t fifo_capacity = 5)
: selector_(std::move(sel))
, fifo_capacity_(fifo_capacity)
{
input_ch_ = std::make_shared<Channel<T>>(fifo_capacity);
}
~RouterNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
input_ch_->enable();
stop_flag_.store(false, std::memory_order_relaxed);
thread_ = std::jthread([this](std::stop_token) { run_loop(); });
}
void stop() override {
stop_flag_.store(true, std::memory_order_relaxed);
input_ch_->disable();
if (thread_.joinable()) thread_.request_stop(), thread_.join();
}
bool running() const override {
return thread_.joinable() && !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0};
}
// ── Port access ───────────────────────────────────────────────────────────
template<std::size_t I = 0>
InputPort<RouterNode, I> input() {
static_assert(I == 0, "RouterNode has exactly one input");
return {*this};
}
template<std::size_t I>
OutputPort<RouterNode, I> output() {
static_assert(I < N, "RouterNode output index out of range");
return {*this};
}
// ── Internal channel accessors (called by Network::connect) ───────────────
template<std::size_t I>
Channel<T>& input_channel() {
static_assert(I == 0);
return *input_ch_;
}
template<std::size_t I>
void set_input_channel(std::shared_ptr<Channel<T>> ch) {
static_assert(I == 0);
input_ch_ = std::move(ch);
}
template<std::size_t I>
void set_output_channel(Channel<T>* ch) {
static_assert(I < N);
out_channels_[I] = ch;
}
private:
void run_loop() {
while (!stop_flag_.load(std::memory_order_relaxed)) {
try {
auto t0 = clock_t::now();
T val = input_ch_->pop();
auto t1 = clock_t::now();
auto cpu0 = NodeStats::cpu_now();
std::size_t idx = selector_(val);
if (idx < N && out_channels_[idx]) {
try { out_channels_[idx]->push(val); }
catch (const ChannelOverflowError&) {}
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
} catch (const ChannelClosedError&) {
break;
}
}
}
std::string name_;
std::size_t fifo_capacity_;
Selector selector_;
std::shared_ptr<Channel<T>> input_ch_;
std::array<Channel<T>*, N> out_channels_{};
std::atomic<bool> stop_flag_{false};
std::jthread thread_;
NodeStats stats_;
};
// ── FilterNode ────────────────────────────────────────────────────────────────
//
// Reads one item and pushes it downstream only when pred(item) is true.
// Dropped items are not counted as processed frames.
//
// Usage:
// auto filt = make_filter<Frame>([](const Frame& f) { return f.valid; });
// net.connect("src", src.output<0>(), "filt", filt.input<0>())
// .connect("filt", filt.output<0>(), "dst", dst.input<0>());
template<typename T, std::size_t Id = 0>
class FilterNode : public INode {
public:
using Predicate = std::function<bool(const T&)>;
using args_tuple = std::tuple<T>;
using return_tuple = std::tuple<T>;
using return_raw = return_tuple;
static constexpr std::size_t input_count = 1;
static constexpr std::size_t output_count = 1;
static constexpr std::size_t unique_tag = Id;
static constexpr bool is_filter_node = true;
explicit FilterNode(Predicate pred, std::size_t fifo_capacity = 5)
: pred_(std::move(pred))
, fifo_capacity_(fifo_capacity)
{
input_ch_ = std::make_shared<Channel<T>>(fifo_capacity);
}
~FilterNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
input_ch_->enable();
stop_flag_.store(false, std::memory_order_relaxed);
thread_ = std::jthread([this](std::stop_token) { run_loop(); });
}
void stop() override {
stop_flag_.store(true, std::memory_order_relaxed);
input_ch_->disable();
if (thread_.joinable()) thread_.request_stop(), thread_.join();
}
bool running() const override {
return thread_.joinable() && !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0};
}
// ── Port access ───────────────────────────────────────────────────────────
template<std::size_t I = 0>
InputPort<FilterNode, I> input() {
static_assert(I == 0, "FilterNode has exactly one input");
return {*this};
}
template<std::size_t I = 0>
OutputPort<FilterNode, I> output() {
static_assert(I == 0, "FilterNode has exactly one output");
return {*this};
}
// ── Internal channel accessors (called by Network::connect) ───────────────
template<std::size_t I>
Channel<T>& input_channel() {
static_assert(I == 0);
return *input_ch_;
}
template<std::size_t I>
void set_input_channel(std::shared_ptr<Channel<T>> ch) {
static_assert(I == 0);
input_ch_ = std::move(ch);
}
template<std::size_t I>
void set_output_channel(Channel<T>* ch) {
static_assert(I == 0);
out_ch_ = ch;
}
private:
void run_loop() {
while (!stop_flag_.load(std::memory_order_relaxed)) {
try {
auto t0 = clock_t::now();
T val = input_ch_->pop();
auto t1 = clock_t::now();
auto cpu0 = NodeStats::cpu_now();
if (pred_(val) && out_ch_) {
try { out_ch_->push(val); }
catch (const ChannelOverflowError&) {}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
}
} catch (const ChannelClosedError&) {
break;
}
}
}
std::string name_;
std::size_t fifo_capacity_;
Predicate pred_;
std::shared_ptr<Channel<T>> input_ch_;
Channel<T>* out_ch_{nullptr};
std::atomic<bool> stop_flag_{false};
std::jthread thread_;
NodeStats stats_;
};
// ── Factories ─────────────────────────────────────────────────────────────────
template<typename T, std::size_t N>
RouterNode<T, N> make_router(std::function<std::size_t(const T&)> sel,
std::size_t capacity = 5) {
return RouterNode<T, N, 0>(std::move(sel), capacity);
}
template<typename T>
FilterNode<T> make_filter(std::function<bool(const T&)> pred,
std::size_t capacity = 5) {
return FilterNode<T, 0>(std::move(pred), capacity);
}
} // namespace kpn
-294
View File
@@ -1,294 +0,0 @@
#pragma once
#include "diagnostics.hpp"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
#include <type_traits>
namespace kpn {
// ── Data size trait ───────────────────────────────────────────────────────────
// Returns the number of bytes of logical payload carried by a value.
// Defaults to sizeof(T), which is correct for PODs and fixed-size types.
// Specialize for heap-owning types (e.g. cv::Mat) to get accurate bandwidth:
//
// template<> struct kpn::ChannelDataSize<cv::Mat> {
// static std::size_t bytes(const cv::Mat& m) { return m.total() * m.elemSize(); }
// };
template<typename T>
struct ChannelDataSize {
static std::size_t bytes(const T&) { return sizeof(T); }
};
// ── Storage policy ────────────────────────────────────────────────────────────
template<typename T>
struct channel_storage_policy {
static constexpr bool by_value =
std::is_trivially_copyable_v<T> && sizeof(T) <= 8;
};
template<typename T>
using channel_storage_t = std::conditional_t<
channel_storage_policy<T>::by_value,
T,
std::shared_ptr<const T>
>;
// ── Exceptions ────────────────────────────────────────────────────────────────
class ChannelOverflowError : public std::runtime_error {
public:
explicit ChannelOverflowError(std::size_t capacity)
: std::runtime_error("channel overflow: capacity " + std::to_string(capacity) +
" exceeded") {}
ChannelOverflowError(std::size_t capacity, std::string context)
: std::runtime_error(std::move(context) + ": capacity " + std::to_string(capacity) +
" exceeded") {}
};
class ChannelClosedError : public std::runtime_error {
public:
ChannelClosedError() : std::runtime_error("channel closed") {}
};
// ── CPU pause hint ────────────────────────────────────────────────────────────
// Signals the CPU that this is a spin-wait loop, improving HT sibling throughput
// and preventing branch-predictor thrash on x86. Falls back to a compiler barrier.
[[maybe_unused]] static void spin_hint() noexcept {
#if defined(__x86_64__) || defined(__i386__)
__asm__ volatile("pause" ::: "memory");
#elif defined(__aarch64__) || defined(__arm__)
__asm__ volatile("yield" ::: "memory");
#else
std::atomic_signal_fence(std::memory_order_seq_cst);
#endif
}
// ── Channel ───────────────────────────────────────────────────────────────────
// SPSC ring buffer with atomic wait/notify and configurable spin-before-sleep.
//
// `spin_count` (constructor arg, default 200): number of pause-hint iterations
// before falling back to atomic::wait (futex). At ~20 ns/pause on x86 this is
// ~4 µs. Set to 0 to disable spinning (useful for power-constrained or
// predominantly-idle pipelines).
//
// Memory ordering contract (SPSC):
// push(): tail_.store(release) pairs with pop()'s tail_.load(acquire)
// head_.load(acquire) pairs with pop()'s head_.store(release)
// pop(): head_.store(release) pairs with push()'s head_.load(acquire)
// tail_.load(acquire) pairs with push()'s tail_.store(release)
template<typename T>
class Channel {
public:
using storage_type = channel_storage_t<T>;
explicit Channel(std::size_t capacity = 5, std::size_t spin_count = 200)
: capacity_(capacity), spin_count_(spin_count)
{
std::size_t rs = 1;
while (rs <= capacity) rs <<= 1; // smallest power-of-2 > capacity
ring_mask_ = rs - 1;
buf_ = std::make_unique<storage_type[]>(rs);
}
Channel(const Channel&) = delete;
Channel& operator=(const Channel&) = delete;
// Push a value.
// - If channel is disabled (accepting_ == false): silently drop.
// - If channel is full (fill >= capacity_): throw ChannelOverflowError.
void push(T value) {
if (!accepting_.load(std::memory_order_relaxed)) {
stats_.record_drop();
return;
}
const std::size_t data_bytes = ChannelDataSize<T>::bytes(value);
const std::size_t t = tail_.load(std::memory_order_relaxed);
const std::size_t h = head_.load(std::memory_order_acquire);
if (!accepting_.load(std::memory_order_acquire)) {
stats_.record_drop();
return;
}
if (t - h >= capacity_) {
stats_.record_overflow();
throw ChannelOverflowError(capacity_);
}
const bool was_empty = (t == h);
buf_[t & ring_mask_] = make_storage(std::move(value));
tail_.store(t + 1, std::memory_order_release);
stats_.record_push(t - h + 1, data_bytes);
wake_.fetch_add(1, std::memory_order_release);
wake_.notify_one();
if (was_empty && push_callback_)
push_callback_();
}
// Blocking pop. Returns when an item is available.
// Throws ChannelClosedError if the channel is disabled (regardless of fill).
T pop() {
for (;;) {
// Snapshot wake_ BEFORE reading tail_ to prevent lost wakeups.
const uint32_t w = wake_.load(std::memory_order_relaxed);
const std::size_t h = head_.load(std::memory_order_relaxed);
std::size_t t = tail_.load(std::memory_order_acquire);
// If empty, spin before sleeping: avoids the futex when the next item
// arrives within the spin window (~4 µs at default spin_count=200 on x86).
if (h == t) {
if (!accepting_.load(std::memory_order_acquire))
throw ChannelClosedError{};
for (std::size_t s = 0; s < spin_count_; ++s) {
spin_hint();
t = tail_.load(std::memory_order_acquire);
if (t != h) break;
if (!accepting_.load(std::memory_order_relaxed))
throw ChannelClosedError{};
}
if (h == t) {
// Still empty after spin — sleep until push() or disable() fires.
// Re-check tail after loading w to guard against a lost wakeup.
if (tail_.load(std::memory_order_acquire) != h) continue;
wake_.wait(w, std::memory_order_relaxed);
continue;
}
}
// Item available (found immediately or during spin).
if (!accepting_.load(std::memory_order_acquire))
throw ChannelClosedError{};
T value = extract(std::move(buf_[h & ring_mask_]));
head_.store(h + 1, std::memory_order_release);
stats_.record_pop();
return value;
}
}
// Non-blocking pop with timeout. For watchdog/display use only.
bool try_pop(T& out, std::chrono::milliseconds timeout) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
if (try_pop_now(out)) return true;
if (!accepting_.load(std::memory_order_relaxed)) return false;
if (std::chrono::steady_clock::now() >= deadline) return false;
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
}
// Immediate non-blocking pop. Returns false if the ring is empty.
bool try_pop_now(T& out) {
const std::size_t h = head_.load(std::memory_order_relaxed);
if (h == tail_.load(std::memory_order_acquire)) return false;
out = extract(std::move(buf_[h & ring_mask_]));
head_.store(h + 1, std::memory_order_release);
stats_.record_pop();
return true;
}
// Enable the channel (called by consumer node on start()).
void enable() {
accepting_.store(true, std::memory_order_relaxed);
}
// Disable the channel: stop accepting new pushes, unblock any waiting pop().
// Items already in the ring are abandoned and freed when the Channel is destroyed.
void disable() {
accepting_.store(false, std::memory_order_release);
wake_.fetch_add(1, std::memory_order_release);
wake_.notify_all();
}
// Register a callback fired when the queue transitions empty→non-empty.
void set_push_callback(std::function<void()> cb) {
push_callback_ = std::move(cb);
}
// Size derived lazily from ring indices — no separate counter on the hot path.
std::size_t size() const {
return tail_.load(std::memory_order_relaxed)
- head_.load(std::memory_order_relaxed);
}
std::size_t approx_size() const { return size(); }
std::size_t capacity() const { return capacity_; }
bool is_accepting() const { return accepting_.load(std::memory_order_relaxed); }
const ChannelStats& stats() const { return stats_; }
ChannelSnapshot snapshot(const std::string& name) const {
const std::size_t t = tail_.load(std::memory_order_relaxed);
const std::size_t h = head_.load(std::memory_order_relaxed);
return {
name,
capacity_,
t - h,
stats_.peak_fill.load(std::memory_order_relaxed),
stats_.pushes.load(std::memory_order_relaxed),
stats_.bytes_pushed.load(std::memory_order_relaxed),
stats_.drops.load(std::memory_order_relaxed),
stats_.overflows.load(std::memory_order_relaxed),
stats_.pops.load(std::memory_order_relaxed),
sizeof(T),
};
}
private:
static storage_type make_storage(T&& v) {
if constexpr (channel_storage_policy<T>::by_value)
return std::move(v);
else
return std::make_shared<const T>(std::move(v));
}
static T extract(storage_type&& s) {
if constexpr (channel_storage_policy<T>::by_value)
return std::move(s);
else
return *s;
}
const std::size_t capacity_;
const std::size_t spin_count_;
std::size_t ring_mask_;
std::unique_ptr<storage_type[]> buf_;
std::function<void()> push_callback_;
ChannelStats stats_;
// Separate cache lines: head_ is written only by the consumer;
// tail_ and wake_ are written only by the producer.
alignas(64) std::atomic<std::size_t> head_{0};
alignas(64) std::atomic<std::size_t> tail_{0};
std::atomic<uint32_t> wake_{0};
std::atomic<bool> accepting_{true};
};
// ── Channel probe — type-erased snapshot accessor ─────────────────────────────
// Used by both Network and StaticNetwork for diagnostics.
struct IChannelProbe {
virtual ~IChannelProbe() = default;
virtual ChannelSnapshot snapshot() const = 0;
};
template<typename T>
struct ChannelProbe : IChannelProbe {
const Channel<T>& ch;
std::string name;
ChannelProbe(const Channel<T>& c, std::string n) : ch(c), name(std::move(n)) {}
ChannelSnapshot snapshot() const override { return ch.snapshot(name); }
};
} // namespace kpn
-456
View File
@@ -1,456 +0,0 @@
#pragma once
// Only active when KPN_WEB_DEBUG is defined.
#ifdef KPN_WEB_DEBUG
#include "diagnostics.hpp"
#include "web_debug.hpp"
#include <functional>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace kpn {
// ── Hub HTML ──────────────────────────────────────────────────────────────────
// Multi-tab UI: one tab per registered network (force-directed graph) +
// an "All Networks" tab showing shared resource cards and a cross-network
// node table.
static const char* HUB_HTML = R"html(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KPN++ Debug Hub</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#1a1a2e;color:#eee;font-family:monospace}
#hdr{display:flex;align-items:center;padding:0 16px;background:#16213e;
border-bottom:1px solid #0f3460;height:44px;gap:8px;overflow-x:auto}
#hdr h1{margin:0;font-size:16px;color:#e94560;white-space:nowrap;margin-right:12px}
#tab-bar{display:flex;gap:2px;flex:1}
.tab{padding:4px 14px;border:none;background:#0f3460;color:#aaa;
cursor:pointer;font-family:monospace;font-size:11px;border-radius:2px;white-space:nowrap}
.tab.active{background:#e94560;color:#fff}
.tab:hover:not(.active){background:#1e3a6e;color:#eee}
#status{font-size:10px;color:#555;white-space:nowrap}
.panel{display:none}
.panel.active{display:block}
/* ── All Networks tab ─────────────────────────────────────── */
#panel-all{height:calc(100vh - 44px);overflow-y:auto;padding:16px;
display:none;gap:16px;grid-template-columns:300px 1fr}
#panel-all.active{display:grid;align-content:start}
#panel-all h2{font-size:11px;color:#e94560;margin:0 0 8px;
letter-spacing:1px;text-transform:uppercase}
#res-col{grid-column:1}
.res-card{background:#16213e;border:1px solid #0f3460;border-radius:4px;
padding:10px 12px;margin-bottom:8px}
.res-head{display:flex;justify-content:space-between;align-items:center}
.res-name{font-size:12px}
.badge{font-size:9px;padding:2px 6px;border-radius:2px}
.held{background:#e94560}.free{background:#4CAF50;color:#111}
.res-meta{font-size:9px;color:#666;margin-top:5px;display:flex;gap:12px;flex-wrap:wrap}
.bar-wrap{height:3px;background:#0f3460;border-radius:2px;margin-top:7px}
.bar{height:3px;border-radius:2px;transition:width 0.4s}
#nodes-col{grid-column:2;overflow-y:auto;max-height:calc(100vh - 76px)}
table{width:100%;border-collapse:collapse;font-size:10px}
th{padding:4px 8px;color:#555;border-bottom:1px solid #0f3460;text-align:left;
position:sticky;top:0;background:#1a1a2e;z-index:1}
td{padding:2px 8px;border-bottom:1px solid #16213e}
tr:hover td{background:#16213e}
.ntag{font-size:9px;background:#0f3460;padding:1px 4px;border-radius:2px;color:#4CAF50}
/* ── Per-network graph panels ─────────────────────────────── */
.graph-panel{width:100vw;height:calc(100vh - 44px)}
svg.net{width:100%;height:100%}
.node circle{stroke:#fff;stroke-width:1.5px}
.node text{font-size:11px;fill:#eee;pointer-events:none;text-anchor:middle}
.node .st{font-size:9px;fill:#aaa}
.link{fill:none;stroke-width:2px}
.lbl{font-size:9px;fill:#ccc}
#tip{position:absolute;background:#0f3460;border:1px solid #e94560;border-radius:4px;
padding:8px 12px;font-size:11px;pointer-events:none;display:none;
white-space:pre;line-height:1.6}
</style>
</head>
<body>
<div id="hdr">
<h1>KPN++ Debug Hub</h1>
<div id="tab-bar"></div>
<span id="status">connecting</span>
</div>
<div id="panels">
<div id="panel-all" class="panel"></div>
</div>
<div id="tip"></div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const R = 28;
const tip = d3.select('#tip');
const nc = ema => ema>100?'#e94560':ema>50?'#e07040':ema>10?'#f0c040':'#4CAF50';
const ec = pct => pct>=80?'#e94560':pct>=50?'#f0c040':'#4CAF50';
const ea = pct => pct>=80?'url(#a2)':pct>=50?'url(#a1)':'url(#a0)';
// ── Tab management ────────────────────────────────────────────────────────────
let activeTab = null;
function ensureTab(id, label) {
if (document.getElementById('tab-' + id)) return;
const b = document.createElement('button');
b.className = 'tab'; b.id = 'tab-' + id; b.textContent = label;
b.onclick = () => showTab(id);
document.getElementById('tab-bar').appendChild(b);
}
function showTab(id) {
activeTab = id;
document.querySelectorAll('.tab').forEach(b =>
b.classList.toggle('active', b.id === 'tab-' + id));
document.querySelectorAll('.panel').forEach(p =>
p.classList.toggle('active', p.id === 'panel-' + id));
}
// ── All Networks tab ──────────────────────────────────────────────────────────
function renderAll(data) {
const panel = document.getElementById('panel-all');
// Resources column
let rhtml = '<div id="res-col"><h2>Shared Resources</h2>';
if (!data.resources || !data.resources.length)
rhtml += '<div style="color:#444;font-size:11px">None registered</div>';
for (const r of (data.resources || [])) {
const wpct = Math.min(100, r.avg_wait_ms).toFixed(1);
const bc = r.current_waiters > 0 ? '#e94560' : '#4CAF50';
rhtml += `<div class="res-card">
<div class="res-head">
<span class="res-name">${r.name}</span>
<span class="badge ${r.held ? 'held' : 'free'}">${r.held ? 'HELD' : 'free'}</span>
</div>
<div class="res-meta">
<span>avg wait ${r.avg_wait_ms.toFixed(1)} ms</span>
<span>waiters ${r.current_waiters} / peak ${r.peak_waiters}</span>
<span>${r.acquisitions} acq</span>
</div>
<div class="bar-wrap">
<div class="bar" style="width:${wpct}%;background:${bc}"></div>
</div>
</div>`;
}
rhtml += '</div>';
// Nodes column — all networks in one table
let rows = '';
for (const net of data.networks) {
for (const n of net.nodes) {
rows += `<tr>
<td><span class="ntag">${net.name}</span></td>
<td>${n.id}</td>
<td>${n.fps.toFixed(1)}</td>
<td>${n.ema_exec_ms.toFixed(2)}</td>
<td>${n.max_exec_ms.toFixed(2)}</td>
<td>${n.blocked_ms.toFixed(2)}</td>
<td>${n.cpu_util_pct.toFixed(1)}</td>
</tr>`;
}
}
const thtml = `<div id="nodes-col"><h2>All Nodes</h2>
<table>
<tr><th>Network</th><th>Node</th><th>fps</th>
<th>exec ema (ms)</th><th>exec max (ms)</th>
<th>blocked (ms)</th><th>cpu %</th></tr>
${rows}
</table></div>`;
panel.innerHTML = rhtml + thtml;
}
// ── Per-network graph ─────────────────────────────────────────────────────────
const nets = {};
function initNet(netData) {
const name = netData.name;
const div = document.createElement('div');
div.id = 'panel-' + name;
div.className = 'panel graph-panel';
document.getElementById('panels').appendChild(div);
const svg = d3.select(div).append('svg').attr('class', 'net');
const W = () => div.clientWidth || window.innerWidth;
const H = () => div.clientHeight || (window.innerHeight - 44);
const defs = svg.append('defs');
['#4CAF50','#f0c040','#e94560'].forEach((col, i) =>
defs.append('marker').attr('id','a'+i)
.attr('viewBox','0 -5 10 10').attr('refX',10).attr('refY',0)
.attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
.append('path').attr('d','M0,-5L10,0L0,5').attr('fill',col));
const g = svg.append('g');
svg.call(d3.zoom().on('zoom', e => g.attr('transform', e.transform)));
const nodes = netData.nodes.map(n => ({...n, x: W()/2, y: H()/2}));
const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
const links = netData.edges
.map(e => ({...e, source: byId[e.source], target: byId[e.target]}))
.filter(e => e.source && e.target);
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).distance(150).strength(0.5))
.force('charge', d3.forceManyBody().strength(-350))
.force('center', d3.forceCenter(W()/2, H()/2))
.force('collide', d3.forceCollide(R + 18))
.on('tick', tick);
const lsel = g.append('g').selectAll('line').data(links).join('line')
.attr('class','link')
.attr('stroke', d => ec(d.fill_pct))
.attr('marker-end', d => ea(d.fill_pct));
const llbl = g.append('g').selectAll('text').data(links).join('text')
.attr('class','lbl').text(d => `${d.fill_pct.toFixed(0)}%`);
const ng = g.append('g').selectAll('g').data(nodes).join('g').attr('class','node')
.call(d3.drag()
.on('start',(e,d)=>{ if(!e.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
.on('drag', (e,d)=>{ d.fx=e.x; d.fy=e.y; })
.on('end', (e,d)=>{ if(!e.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }));
ng.append('circle').attr('r', R).attr('fill', d => nc(d.ema_exec_ms));
ng.append('text').attr('dy', 4).text(d => d.id);
ng.append('text').attr('class','st').attr('dy', 18)
.text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
ng.on('mousemove', (e,d) =>
tip.style('display','block')
.style('left',(e.pageX+12)+'px').style('top',(e.pageY+12)+'px')
.text(`${d.id}\nframes: ${d.frames} fps: ${d.fps.toFixed(2)}\n` +
`exec ema: ${d.ema_exec_ms.toFixed(2)}ms max: ${d.max_exec_ms.toFixed(2)}ms\n` +
`blocked: ${d.blocked_ms.toFixed(2)}ms cpu: ${d.cpu_util_pct.toFixed(1)}%`))
.on('mouseleave', () => tip.style('display','none'));
g.selectAll('.link')
.on('mousemove', (e,d) =>
tip.style('display','block')
.style('left',(e.pageX+12)+'px').style('top',(e.pageY+12)+'px')
.text(`${d.name}\nfill: ${d.fill_pct.toFixed(1)}% peak: ${d.peak_pct.toFixed(1)}%\n` +
`cap: ${d.capacity} pushes: ${d.pushes} drops: ${d.drops}\n` +
`bandwidth: ${(d.bw_mbs||0).toFixed(2)} MB/s`))
.on('mouseleave', () => tip.style('display','none'));
function tick() {
const w = W(), h = H();
nodes.forEach(d => {
d.x = Math.max(R, Math.min(w - R, d.x));
d.y = Math.max(R, Math.min(h - R, d.y));
});
lsel
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
.attr('x2', d => { const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y,
dist=Math.sqrt(dx*dx+dy*dy)||1;
return d.target.x-(dx/dist)*(R+8); })
.attr('y2', d => { const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y,
dist=Math.sqrt(dx*dx+dy*dy)||1;
return d.target.y-(dy/dist)*(R+8); });
llbl.attr('x', d => (d.source.x+d.target.x)/2)
.attr('y', d => (d.source.y+d.target.y)/2 - 6);
ng.attr('transform', d => `translate(${d.x},${d.y})`);
}
nets[name] = {nodes, links, sim, ng, lsel, llbl};
}
function updateNet(netData) {
const st = nets[netData.name];
if (!st) return;
const byId = Object.fromEntries(netData.nodes.map(n => [n.id, n]));
st.nodes.forEach(n => {
const f = byId[n.id];
if (f) Object.assign(n, {frames:f.frames, ema_exec_ms:f.ema_exec_ms,
max_exec_ms:f.max_exec_ms, blocked_ms:f.blocked_ms, fps:f.fps,
total_cpu_ms:f.total_cpu_ms, cpu_util_pct:f.cpu_util_pct});
});
netData.edges.forEach((e,i) => {
if (st.links[i]) Object.assign(st.links[i], {fill_pct:e.fill_pct,
peak_pct:e.peak_pct, pushes:e.pushes, drops:e.drops,
overflows:e.overflows, current:e.current, bw_mbs:e.bw_mbs});
});
st.ng.select('circle').attr('fill', d => nc(d.ema_exec_ms));
st.ng.select('.st').text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
st.lsel.attr('stroke', d => ec(d.fill_pct)).attr('marker-end', d => ea(d.fill_pct));
st.llbl.text(d => `${d.fill_pct.toFixed(0)}%`);
}
// ── Poll loop ─────────────────────────────────────────────────────────────────
let init = false;
async function poll() {
try {
const r = await fetch('/api/snapshot');
if (!r.ok) throw new Error(r.status);
const data = await r.json();
if (!init) {
ensureTab('all', 'All Networks');
data.networks.forEach(net => ensureTab(net.name, net.name));
showTab('all');
data.networks.forEach(initNet);
init = true;
}
renderAll(data);
data.networks.forEach(updateNet);
document.getElementById('status').textContent =
`${new Date().toLocaleTimeString()} · ${data.networks.length} nets · ${(data.resources||[]).length} resources`;
} catch(e) {
document.getElementById('status').textContent = 'error: ' + e;
}
}
poll();
setInterval(poll, 500);
window.addEventListener('resize', () =>
Object.values(nets).forEach(st =>
st.sim.force('center', d3.forceCenter(
(document.getElementById('panel-' + Object.keys(nets).find(k => nets[k] === st))?.clientWidth || window.innerWidth) / 2,
(document.getElementById('panel-' + Object.keys(nets).find(k => nets[k] === st))?.clientHeight || window.innerHeight - 44) / 2
)).alpha(0.1).restart()));
</script>
</body>
</html>
)html";
// ── DebugHub ──────────────────────────────────────────────────────────────────
class DebugHub {
public:
explicit DebugHub(uint16_t port = 9090) : port_(port) {}
~DebugHub() { stop(); }
DebugHub(const DebugHub&) = delete;
DebugHub& operator=(const DebugHub&) = delete;
// Register a network. Disables that network's own web server so the hub
// becomes the single debug endpoint. Call before network.start().
template<typename Net>
void register_network(const std::string& name, Net& net) {
net.disable_web_server();
networks_.push_back({name, [&net, name] {
auto s = net.network_snapshot();
s.name = name;
return s;
}});
}
// Register a shared resource — appears in the "All Networks" resource panel.
void register_resource(const std::string& name, IResourceProbe* probe) {
resources_.emplace_back(name, probe);
}
void start() {
server_ = std::make_unique<web_debug::WebDebugServer>(
port_,
[this] { return build_json(); },
HUB_HTML);
server_->start();
std::cerr << "[kpn] hub debug UI: http://localhost:" << port_ << "\n";
}
void stop() { if (server_) server_->stop(); }
private:
// Serialise nodes array for one network snapshot
static void write_nodes(std::ostream& o, const NetworkSnapshot& s) {
o << "[";
for (std::size_t i = 0; i < s.nodes.size(); ++i) {
const auto& n = s.nodes[i];
if (i) o << ',';
o << "{\"id\":\"" << web_debug::escape_json(n.name) << "\""
<< ",\"frames\":" << n.frames_processed
<< ",\"ema_exec_ms\":" << n.ema_exec_ms
<< ",\"max_exec_ms\":" << n.max_exec_ms
<< ",\"blocked_ms\":" << n.total_blocked_ms
<< ",\"fps\":" << n.throughput_fps
<< ",\"total_cpu_ms\":" << n.total_cpu_ms
<< ",\"cpu_util_pct\":" << n.cpu_util_pct
<< "}";
}
o << "]";
}
// Serialise edges array for one network snapshot
static void write_edges(std::ostream& o, const NetworkSnapshot& s) {
o << "[";
for (std::size_t i = 0; i < s.channels.size(); ++i) {
const auto& c = s.channels[i];
if (i) o << ',';
auto [src, dst] = web_debug::parse_edge_name(c.name);
o << "{\"name\":\"" << web_debug::escape_json(c.name) << "\""
<< ",\"source\":\"" << web_debug::escape_json(src) << "\""
<< ",\"target\":\"" << web_debug::escape_json(dst) << "\""
<< ",\"capacity\":" << c.capacity
<< ",\"current\":" << c.current_fill
<< ",\"fill_pct\":" << c.fill_pct()
<< ",\"peak_pct\":" << c.peak_pct()
<< ",\"pushes\":" << c.pushes
<< ",\"drops\":" << c.drops
<< ",\"overflows\":" << c.overflows
<< ",\"item_bytes\":" << c.item_bytes
<< ",\"bw_mbs\":" << c.bandwidth_mbs(s.elapsed_s)
<< "}";
}
o << "]";
}
std::string build_json() const {
std::ostringstream o;
o << std::fixed;
o.precision(2);
o << "{\"networks\":[";
for (std::size_t i = 0; i < networks_.size(); ++i) {
if (i) o << ',';
auto s = networks_[i].fn();
o << "{\"name\":\"" << web_debug::escape_json(networks_[i].name) << "\""
<< ",\"nodes\":"; write_nodes(o, s);
o << ",\"edges\":"; write_edges(o, s);
o << "}";
}
o << "],\"resources\":[";
for (std::size_t i = 0; i < resources_.size(); ++i) {
if (i) o << ',';
const auto r = resources_[i].second->snapshot(resources_[i].first);
o << "{\"name\":\"" << web_debug::escape_json(r.name) << "\""
<< ",\"acquisitions\":" << r.acquisitions
<< ",\"avg_wait_ms\":" << r.avg_wait_ms
<< ",\"peak_waiters\":" << r.peak_waiters
<< ",\"current_waiters\":" << r.current_waiters
<< ",\"held\":" << (r.held ? "true" : "false")
<< "}";
}
o << "]}";
return o.str();
}
struct Entry {
std::string name;
std::function<NetworkSnapshot()> fn;
};
uint16_t port_;
std::vector<Entry> networks_;
std::vector<std::pair<std::string,IResourceProbe*>> resources_;
std::unique_ptr<web_debug::WebDebugServer> server_;
};
} // namespace kpn
#endif // KPN_WEB_DEBUG
-193
View File
@@ -1,193 +0,0 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <string>
#include <time.h> // clock_gettime, CLOCK_THREAD_CPUTIME_ID
namespace kpn {
using clock_t = std::chrono::steady_clock;
using duration_t = std::chrono::duration<double, std::milli>; // milliseconds
// ── Per-channel statistics ────────────────────────────────────────────────────
struct ChannelStats {
std::atomic<uint64_t> pushes{0};
std::atomic<uint64_t> bytes_pushed{0};
std::atomic<uint64_t> drops{0};
std::atomic<uint64_t> overflows{0};
std::atomic<uint64_t> pops{0};
std::atomic<std::size_t> peak_fill{0};
ChannelStats() = default;
ChannelStats(const ChannelStats&) = delete;
ChannelStats& operator=(const ChannelStats&) = delete;
void record_push(std::size_t current_fill, std::size_t data_bytes) {
pushes.fetch_add(1, std::memory_order_relaxed);
bytes_pushed.fetch_add(data_bytes, std::memory_order_relaxed);
std::size_t prev = peak_fill.load(std::memory_order_relaxed);
while (current_fill > prev &&
!peak_fill.compare_exchange_weak(prev, current_fill,
std::memory_order_relaxed, std::memory_order_relaxed))
;
}
void record_drop() { drops.fetch_add(1, std::memory_order_relaxed); }
void record_overflow() { overflows.fetch_add(1, std::memory_order_relaxed); }
void record_pop() { pops.fetch_add(1, std::memory_order_relaxed); }
};
// ── Per-node statistics ───────────────────────────────────────────────────────
struct NodeStats {
std::atomic<uint64_t> frames_processed{0};
// Wall-clock execution time EMA — warmup mean for first WARMUP_FRAMES,
// then EMA alpha=0.1. Stored as integer microseconds for atomic updates.
static constexpr int WARMUP_FRAMES = 5;
std::atomic<int64_t> ema_exec_us{0};
std::atomic<int64_t> max_exec_us{0};
std::atomic<int64_t> total_blocked_us{0};
// Thread CPU time — actual CPU consumed by this node's thread,
// measured via CLOCK_THREAD_CPUTIME_ID. Excludes time sleeping or
// blocked on mutexes/channels. Sampled once per frame.
std::atomic<int64_t> total_cpu_us{0}; // cumulative CPU µs consumed
// Pool scheduling stats — only meaningful for PoolNode / InterruptNode.
// exec_start_us: wall-clock µs when fire_once began; 0 when idle.
// Used by the watchdog to detect hung nodes (elapsed > max_exec_time).
std::atomic<int64_t> queue_wait_us{0}; // cumulative µs spent in pool queue
std::atomic<int64_t> exec_start_us{0}; // non-zero while fire_once is running
NodeStats() = default;
NodeStats(const NodeStats&) = delete;
NodeStats& operator=(const NodeStats&) = delete;
// Call at the start of run_loop to capture thread CPU baseline.
// Returns the raw timespec for use in record_exec.
static struct timespec cpu_now() {
struct timespec ts{};
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);
return ts;
}
static int64_t timespec_us(const struct timespec& ts) {
return static_cast<int64_t>(ts.tv_sec) * 1'000'000
+ static_cast<int64_t>(ts.tv_nsec) / 1'000;
}
void record_queue_wait(duration_t wait) {
int64_t us = static_cast<int64_t>(wait.count() * 1000.0);
if (us > 0) queue_wait_us.fetch_add(us, std::memory_order_relaxed);
}
void record_exec(duration_t exec_time, duration_t blocked_time,
const struct timespec& cpu_before, const struct timespec& cpu_after) {
frames_processed.fetch_add(1, std::memory_order_relaxed);
int64_t us = static_cast<int64_t>(exec_time.count() * 1000.0);
uint64_t n = frames_processed.load(std::memory_order_relaxed);
int64_t prev = ema_exec_us.load(std::memory_order_relaxed);
int64_t next = (n <= static_cast<uint64_t>(WARMUP_FRAMES))
? prev + (us - prev) / static_cast<int64_t>(n)
: prev + (us - prev) / 10;
ema_exec_us.store(next, std::memory_order_relaxed);
int64_t cur_max = max_exec_us.load(std::memory_order_relaxed);
if (us > cur_max)
max_exec_us.store(us, std::memory_order_relaxed);
int64_t blocked_us = static_cast<int64_t>(blocked_time.count() * 1000.0);
total_blocked_us.fetch_add(blocked_us, std::memory_order_relaxed);
int64_t cpu_delta = timespec_us(cpu_after) - timespec_us(cpu_before);
if (cpu_delta > 0)
total_cpu_us.fetch_add(cpu_delta, std::memory_order_relaxed);
}
};
// ── Snapshot for reporting (copyable, taken by watchdog) ─────────────────────
struct ChannelSnapshot {
std::string name;
std::size_t capacity;
std::size_t current_fill;
std::size_t peak_fill;
uint64_t pushes;
uint64_t bytes_pushed; // actual bytes accumulated via channel_data_size<T>
uint64_t drops;
uint64_t overflows;
uint64_t pops;
std::size_t item_bytes; // sizeof(T) — nominal struct size, not necessarily data size
double fill_pct() const {
return capacity ? 100.0 * current_fill / capacity : 0.0;
}
double peak_pct() const {
return capacity ? 100.0 * peak_fill / capacity : 0.0;
}
// Bandwidth in MB/s: actual bytes transferred / elapsed seconds
double bandwidth_mbs(double elapsed_s) const {
if (elapsed_s <= 0.0) return 0.0;
return static_cast<double>(bytes_pushed) / elapsed_s / 1e6;
}
};
struct NodeSnapshot {
std::string name;
uint64_t frames_processed;
double ema_exec_ms;
double max_exec_ms;
double total_blocked_ms; // ThreadPerNode: time blocked in channel pop
double throughput_fps;
double total_cpu_ms; // cumulative CPU time consumed by this node's thread
double cpu_util_pct; // exec_ms / (exec_ms + blocked_ms) * 100
double queue_wait_ms{0}; // PoolNode: cumulative time spent in pool queue
};
// ── Pool statistics + snapshot ────────────────────────────────────────────────
struct PoolSnapshot {
std::string name;
std::size_t thread_count;
std::size_t queue_depth; // tasks waiting in the priority queue
std::size_t active_count; // tasks currently executing
uint64_t tasks_submitted;
uint64_t tasks_completed;
};
struct IPoolProbe {
virtual ~IPoolProbe() = default;
virtual PoolSnapshot snapshot(const std::string& name) const = 0;
};
// ── Cross-network snapshot (used by DebugHub) ─────────────────────────────────
struct NetworkSnapshot {
std::string name;
std::vector<NodeSnapshot> nodes;
std::vector<ChannelSnapshot> channels;
double elapsed_s;
};
// ── Resource statistics + snapshot ───────────────────────────────────────────
struct ResourceSnapshot {
std::string name;
uint64_t acquisitions;
double avg_wait_ms;
uint64_t peak_waiters;
uint64_t current_waiters;
bool held;
};
struct IResourceProbe {
virtual ~IResourceProbe() = default;
virtual ResourceSnapshot snapshot(const std::string& name) const = 0;
};
} // namespace kpn
-159
View File
@@ -1,159 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "inode.hpp"
#include "port.hpp"
#include "traits.hpp"
#include <array>
#include <atomic>
#include <iostream>
#include <memory>
#include <thread>
#include <tuple>
#include <utility>
namespace kpn {
// ── FanoutNode ────────────────────────────────────────────────────────────────
//
// Reads one item from its single input channel and pushes a copy to each of
// N output channels. All N downstream nodes receive every item.
//
// Usage:
// auto fan = make_fanout<Image, 2>(/*capacity=*/8);
// net.connect("src", src.output<0>(), "fan", fan.input<0>())
// .connect("fan", fan.output<0>(), "nodeA", nodeA.input<0>())
// .connect("fan", fan.output<1>(), "nodeB", nodeB.input<0>())
template<typename T, std::size_t N, std::size_t Id = 0>
class FanoutNode : public INode {
public:
using args_tuple = std::tuple<T>;
using return_tuple = repeat_tuple_t<T, N>;
using return_raw = return_tuple;
static constexpr std::size_t input_count = 1;
static constexpr std::size_t output_count = N;
static constexpr std::size_t unique_tag = Id;
static constexpr bool is_fanout_node = true;
explicit FanoutNode(std::size_t fifo_capacity = 5)
: fifo_capacity_(fifo_capacity)
{
input_ch_ = std::make_shared<Channel<T>>(fifo_capacity);
}
~FanoutNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
input_ch_->enable();
stop_flag_.store(false, std::memory_order_relaxed);
thread_ = std::jthread([this](std::stop_token) { run_loop(); });
}
void stop() override {
stop_flag_.store(true, std::memory_order_relaxed);
input_ch_->disable();
if (thread_.joinable()) thread_.request_stop(), thread_.join();
}
bool running() const override {
return thread_.joinable() && !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0};
}
// ── Port access ───────────────────────────────────────────────────────────
template<std::size_t I = 0>
InputPort<FanoutNode, I> input() {
static_assert(I == 0, "FanoutNode has exactly one input");
return {*this};
}
template<std::size_t I>
OutputPort<FanoutNode, I> output() {
static_assert(I < N, "FanoutNode output index out of range");
return {*this};
}
// ── Internal channel accessors (called by Network::connect) ───────────────
template<std::size_t I>
Channel<T>& input_channel() {
static_assert(I == 0);
return *input_ch_;
}
template<std::size_t I>
void set_input_channel(std::shared_ptr<Channel<T>> ch) {
static_assert(I == 0);
input_ch_ = std::move(ch);
}
template<std::size_t I>
void set_output_channel(Channel<T>* ch) {
static_assert(I < N);
out_channels_[I] = ch;
}
private:
void run_loop() {
while (!stop_flag_.load(std::memory_order_relaxed)) {
try {
auto t0 = clock_t::now();
T val = input_ch_->pop();
auto t1 = clock_t::now();
auto cpu0 = NodeStats::cpu_now();
for (std::size_t i = 0; i < N; ++i) {
if (out_channels_[i]) {
try { out_channels_[i]->push(val); }
catch (const ChannelOverflowError&) {} // drop for this output independently
}
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
} catch (const ChannelClosedError&) {
break;
}
}
}
std::string name_;
std::size_t fifo_capacity_;
std::shared_ptr<Channel<T>> input_ch_;
std::array<Channel<T>*, N> out_channels_{};
std::atomic<bool> stop_flag_{false};
std::jthread thread_;
NodeStats stats_;
};
// ── Factory ───────────────────────────────────────────────────────────────────
template<typename T, std::size_t N>
FanoutNode<T, N> make_fanout(std::size_t fifo_capacity = 5) {
return FanoutNode<T, N, 0>(fifo_capacity);
}
} // namespace kpn
-46
View File
@@ -1,46 +0,0 @@
#pragma once
#include <algorithm>
#include <string_view>
#include <cstddef>
namespace kpn {
template<std::size_t N>
struct fixed_string {
char data[N]{};
constexpr fixed_string(const char (&s)[N]) { std::copy_n(s, N, data); }
constexpr bool operator==(const fixed_string&) const = default;
template<std::size_t M>
constexpr bool operator==(const fixed_string<M>&) const { return false; }
constexpr std::string_view view() const { return {data, N - 1}; }
};
template<std::size_t N>
fixed_string(const char (&)[N]) -> fixed_string<N>;
// ── Port name pack lookup ─────────────────────────────────────────────────────
inline constexpr std::size_t npos = std::size_t(-1);
template<fixed_string Name, fixed_string... Names>
constexpr std::size_t index_of() {
std::size_t i = 0;
bool found = false;
auto check = [&](auto n) {
if (!found) {
if (Name == n) found = true;
else ++i;
}
};
(check(Names), ...);
return found ? i : npos;
}
// ── in<> / out<> name tag structs ─────────────────────────────────────────────
template<fixed_string... Names> struct in {};
template<fixed_string... Names> struct out {};
} // namespace kpn
-32
View File
@@ -1,32 +0,0 @@
#pragma once
#include "diagnostics.hpp"
#include <functional>
#include <string>
#include <string_view>
namespace kpn {
// Called when a node's function throws. Return true to skip the failed
// invocation and keep running, false to stop the node.
using NodeErrorHandler = std::function<bool(std::string_view node_name, std::exception_ptr)>;
// ── INode — type-erased interface for Network / watchdog ─────────────────────
struct INode {
virtual ~INode() = default;
virtual void start() = 0;
virtual void stop() = 0;
virtual bool running() const = 0;
virtual const NodeStats& stats() const = 0;
virtual NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const = 0;
virtual void set_name(std::string name) = 0;
// halt(): alias for stop() — immediate, discards in-flight work.
virtual void halt() { stop(); }
// shutdown(): graceful drain before stopping. Base implementation falls
// back to stop(). Network and StaticNetwork override with topo-ordered drain.
virtual void shutdown() { stop(); }
};
} // namespace kpn
-259
View File
@@ -1,259 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "fixed_string.hpp"
#include "inode.hpp"
#include "port.hpp"
#include "scheduler.hpp"
#include "traits.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <tuple>
#include <type_traits>
namespace kpn {
// ── InterruptNode ─────────────────────────────────────────────────────────────
//
// A source node (zero inputs) driven by an external event — camera frame ready,
// timer tick, socket data, etc. — rather than self-submission.
//
// Usage:
// auto node = make_interrupt_node<produce_frame>(pool, out<"frame">{});
// camera_sdk.on_frame_ready(node.get_trigger()); // register with external source
// network.add("camera", node).connect(...).build().start();
//
// The trigger callable is safe to call from any thread, including signal handlers,
// provided the underlying scheduler's submit() is signal-safe. After fire_once()
// completes the node is idle until the next trigger fires — it does NOT busy-loop.
template<auto Func,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class InterruptNode;
template<auto Func, fixed_string... OutNames, fixed_string Label, std::size_t UniqueTag>
class InterruptNode<Func, out<OutNames...>, Label, UniqueTag> : public INode {
public:
using F = decltype(Func);
using return_raw = return_t<F>;
using return_tuple = normalised_return_t<return_raw>;
static_assert(arity_v<F> == 0,
"InterruptNode function must take no arguments (it has no input channels)");
static constexpr std::string_view label() { return Label.view(); }
static constexpr std::size_t unique_tag = UniqueTag;
static constexpr std::size_t input_count = 0;
static constexpr std::size_t output_count = std::tuple_size_v<return_tuple>;
static_assert(
sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count,
"make_interrupt_node: number of output names must match return tuple size, or provide none"
);
explicit InterruptNode(std::shared_ptr<IScheduler> sched, std::size_t fifo_capacity = 5)
: scheduler_(std::move(sched)), fifo_capacity_(fifo_capacity)
{}
~InterruptNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
stop_flag_.store(false, std::memory_order_relaxed);
pending_.store(0, std::memory_order_relaxed);
// Does NOT self-submit — waits for first external trigger.
}
void stop() override {
stop_flag_.store(true, std::memory_order_seq_cst);
// In-flight fire_once() observes stop_flag_ on its next check.
}
bool running() const override { return !stop_flag_.load(std::memory_order_relaxed); }
void set_name(std::string name) override { name_ = std::move(name); }
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double qwait_ms = stats_.queue_wait_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms; // no blocked time for interrupt nodes
return {
name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
0.0, // blocked_ms — not applicable
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 : 0.0,
qwait_ms,
};
}
// ── Port access — by index ────────────────────────────────────────────────
template<std::size_t I>
OutputPort<InterruptNode, I> output() {
static_assert(I < output_count, "output index out of range");
return {*this};
}
template<fixed_string Name>
auto output() {
constexpr std::size_t idx = index_of<Name, OutNames...>();
static_assert(idx != npos, "unknown output port name");
return output<idx>();
}
template<std::size_t I>
void set_output_channel(Channel<std::tuple_element_t<I, return_tuple>>* ch) {
std::get<I>(output_channels_) = ch;
}
// ── Trigger ───────────────────────────────────────────────────────────────
// Returns a callable that fires this node when called.
// Pass it to a camera SDK, timer, or any external event source.
// Thread-safe; may be called from any thread.
std::function<void()> get_trigger() {
return [this] { trigger(); };
}
private:
// Each trigger() increments pending_. When going 0→1 a task is submitted.
// Each fire_once() handles one pending event and decrements; if more remain
// (old value > 1) it resubmits itself. This guarantees every trigger produces
// exactly one execution even if triggers arrive faster than fire_once completes.
void trigger() {
if (stop_flag_.load(std::memory_order_relaxed)) return;
if (pending_.fetch_add(1, std::memory_order_acq_rel) == 0)
scheduler_->submit([this] { fire_once(); });
}
void fire_once() {
if (stop_flag_.load(std::memory_order_relaxed)) {
pending_.store(0, std::memory_order_release);
return;
}
auto t0 = clock_t::now();
int64_t now_us = std::chrono::duration_cast<std::chrono::microseconds>(
t0.time_since_epoch()).count();
stats_.exec_start_us.store(now_us, std::memory_order_relaxed);
bool fatal = false;
try {
auto t1 = clock_t::now();
stats_.record_queue_wait(duration_t(t1 - t0));
auto cpu0 = NodeStats::cpu_now();
if constexpr (std::is_void_v<return_raw>) {
Func();
} else {
auto result = Func();
push_outputs(normalise(std::move(result)),
std::make_index_sequence<output_count>{});
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
} catch (const ChannelOverflowError& e) {
std::cerr << "[kpn] interrupt node overflow: " << e.what() << "\n";
} catch (...) {
if (!error_handler_ || !error_handler_(name_, std::current_exception()))
fatal = true;
}
stats_.exec_start_us.store(0, std::memory_order_relaxed);
if (fatal) {
pending_.store(0, std::memory_order_release);
stop_flag_.store(true, std::memory_order_relaxed);
return;
}
// Decrement and resubmit only if more triggers are queued.
// fetch_sub returns old value; old > 1 means new > 0.
if (pending_.fetch_sub(1, std::memory_order_acq_rel) > 1)
scheduler_->submit([this] { fire_once(); });
}
template<typename R = return_raw>
static return_tuple normalise(R&& r) {
if constexpr (is_tuple_v<R>) return std::move(r);
else return std::make_tuple(std::move(r));
}
template<std::size_t... Is>
void push_outputs(return_tuple&& result, std::index_sequence<Is...>) {
(push_one<Is>(std::get<Is>(std::move(result))), ...);
}
template<std::size_t I>
void push_one(std::tuple_element_t<I, return_tuple>&& val) {
auto* ch = std::get<I>(output_channels_);
if (!ch) return;
try { ch->push(std::move(val)); }
catch (const ChannelOverflowError&) {
throw ChannelOverflowError(ch->capacity(),
"interrupt node '" + name_ + "' " + output_port_label<I>());
}
}
template<std::size_t I>
static std::string output_port_label() {
if constexpr (sizeof...(OutNames) > 0) {
constexpr std::array<std::string_view, sizeof...(OutNames)> names{OutNames.view()...};
return std::string("output['") + std::string(names[I]) + "']";
} else {
return "output[" + std::to_string(I) + "]";
}
}
template<typename Tup, std::size_t... Is>
static auto make_output_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<Channel<std::tuple_element_t<Is, Tup>>*...>;
using output_channels_t = decltype(make_output_channel_tuple<return_tuple>(
std::make_index_sequence<output_count>{}));
std::shared_ptr<IScheduler> scheduler_;
std::string name_;
std::size_t fifo_capacity_;
output_channels_t output_channels_{};
std::atomic<bool> stop_flag_{true};
std::atomic<int> pending_{0}; // triggers awaiting execution
NodeStats stats_;
NodeErrorHandler error_handler_;
std::chrono::milliseconds max_exec_time_{0};
};
// ── make_interrupt_node factory ───────────────────────────────────────────────
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0>
auto make_interrupt_node(std::shared_ptr<IScheduler> sched, std::size_t fifo_capacity = 5) {
return InterruptNode<Func, out<>, Label, UniqueTag>(std::move(sched), fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... OutNames>
auto make_interrupt_node(std::shared_ptr<IScheduler> sched, out<OutNames...>,
std::size_t fifo_capacity = 5) {
return InterruptNode<Func, out<OutNames...>, Label, UniqueTag>(
std::move(sched), fifo_capacity);
}
} // namespace kpn
-19
View File
@@ -1,19 +0,0 @@
#pragma once
// Convenience umbrella header — include this to get the full C++ API.
#include "fixed_string.hpp"
#include "traits.hpp"
#include "channel.hpp"
#include "port.hpp"
#include "inode.hpp"
#include "scheduler.hpp"
#include "pool_node.hpp"
#include "interrupt_node.hpp"
#include "node.hpp"
#include "fanout.hpp"
#include "branch.hpp"
#include "shared_resource.hpp"
#include "static_network.hpp"
#include "debug_hub.hpp"
#include "main_thread_node.hpp"
#include "network.hpp"
-186
View File
@@ -1,186 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "fixed_string.hpp"
#include "inode.hpp"
#include "port.hpp"
#include <atomic>
#include <chrono>
#include <cstddef>
#include <memory>
#include <optional>
#include <tuple>
namespace kpn {
// ── MainThreadNode ────────────────────────────────────────────────────────────
//
// Base class for nodes that must run on the main thread (e.g. OpenCV display
// on Wayland/Qt). Registered as a normal INode in the Network so it appears
// in diagnostics and the web UI, but spawns no thread.
//
// Usage:
// class MyDisplay : public kpn::MainThreadNode<MyDisplay, in<"a","b">, TypeA, TypeB> {
// public:
// MyDisplay(...) { /* constructor runs on main thread */ }
// bool operator()(TypeA a, TypeB b) { ...; return true; /* false = stop */ }
// };
//
// MyDisplay disp(...);
// net.add("display", disp).connect(...).build();
// net.start();
// while (disp.step()) ; // drives the event loop on the main thread
// net.stop();
//
// step() behaviour:
// - try_pop on every input channel with zero timeout
// - if all inputs have data: calls operator(), records stats, returns its result
// - if any input is missing: returns true immediately (caller should yield/waitKey)
template<typename Derived, typename InputTag, typename... Args>
class MainThreadNode;
template<typename Derived, fixed_string... InNames, typename... Args>
class MainThreadNode<Derived, in<InNames...>, Args...> : public INode {
public:
static constexpr std::size_t input_count = sizeof...(Args);
static_assert(
sizeof...(InNames) == 0 || sizeof...(InNames) == input_count,
"MainThreadNode: name count must match input type count, or provide none"
);
using args_tuple = std::tuple<Args...>; // required by Network::connect type check
explicit MainThreadNode(std::size_t fifo_capacity = 8) {
init_channels(std::make_index_sequence<input_count>{}, fifo_capacity);
}
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
enable_channels(std::make_index_sequence<input_count>{});
running_.store(true, std::memory_order_relaxed);
}
void stop() override {
running_.store(false, std::memory_order_relaxed);
disable_channels(std::make_index_sequence<input_count>{});
}
bool running() const override {
return running_.load(std::memory_order_relaxed);
}
void set_name(std::string) override {}
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {
name, frames,
exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0,
};
}
// ── Port access (for Network::connect) ───────────────────────────────────
template<std::size_t I>
Channel<std::tuple_element_t<I, std::tuple<Args...>>>& input_channel() {
return *std::get<I>(channels_);
}
template<std::size_t I>
InputPort<Derived, I> input() {
static_assert(I < input_count, "input index out of range");
return {static_cast<Derived&>(*this)};
}
template<fixed_string Name>
auto input() {
constexpr std::size_t idx = index_of<Name, InNames...>();
static_assert(idx != npos, "unknown input port name");
return input<idx>();
}
// ── Main-thread driver ────────────────────────────────────────────────────
// Call this in a loop on the main thread instead of net.start()'s thread.
// Returns false when operator() returns false or all channels are closed.
bool step() {
if (!running_.load(std::memory_order_relaxed)) return false;
auto t0 = clock_t::now();
auto inputs = try_pop_all(std::make_index_sequence<input_count>{});
auto t1 = clock_t::now();
if (!inputs.has_value()) return true; // not all inputs ready — yield
auto cpu0 = NodeStats::cpu_now();
bool cont = std::apply(
[this](Args&&... a) {
return static_cast<Derived*>(this)->operator()(std::forward<Args>(a)...);
},
std::move(*inputs));
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
return cont;
}
private:
template<std::size_t... Is>
void init_channels(std::index_sequence<Is...>, std::size_t cap) {
((std::get<Is>(channels_) =
std::make_unique<Channel<std::tuple_element_t<Is, args_tuple>>>(cap)), ...);
}
template<std::size_t... Is>
void enable_channels(std::index_sequence<Is...>) {
(std::get<Is>(channels_)->enable(), ...);
}
template<std::size_t... Is>
void disable_channels(std::index_sequence<Is...>) {
(std::get<Is>(channels_)->disable(), ...);
}
// Try to pop one item from every channel with zero timeout.
// Returns nullopt if any channel has no data ready.
template<std::size_t... Is>
std::optional<args_tuple> try_pop_all(std::index_sequence<Is...>) {
args_tuple result;
bool all_ready = true;
// Use a fold that short-circuits on first missing item
((all_ready = all_ready &&
std::get<Is>(channels_)->try_pop(
std::get<Is>(result), std::chrono::milliseconds(0))), ...);
if (!all_ready) return std::nullopt;
return result;
}
// Build the channel tuple type
template<typename Tup, std::size_t... Is>
static auto make_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<std::unique_ptr<Channel<std::tuple_element_t<Is, Tup>>>...>;
using channels_t = decltype(make_channel_tuple<args_tuple>(
std::make_index_sequence<input_count>{}));
channels_t channels_;
std::atomic<bool> running_{false};
NodeStats stats_;
};
} // namespace kpn
-442
View File
@@ -1,442 +0,0 @@
#pragma once
#include "diagnostics.hpp"
#include "inode.hpp"
#include "port.hpp"
#ifdef KPN_WEB_DEBUG
#include "web_debug.hpp"
#include <memory>
#endif
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
namespace kpn {
// ── Exceptions ────────────────────────────────────────────────────────────────
class NetworkCycleError : public std::runtime_error {
public:
NetworkCycleError() : std::runtime_error("network graph contains a directed cycle") {}
};
class NetworkBuildError : public std::runtime_error {
public:
explicit NetworkBuildError(std::string msg)
: std::runtime_error(std::move(msg)) {}
};
// ── Network ───────────────────────────────────────────────────────────────────
class Network : public INode {
public:
using ErrorHandler =
std::function<void(std::string_view node_name, std::exception_ptr)>;
using DiagnosticsHandler =
std::function<void(const std::vector<NodeSnapshot>&,
const std::vector<ChannelSnapshot>&)>;
// ── Builder API ───────────────────────────────────────────────────────────
template<typename NodeT>
Network& add(std::string name, NodeT& node) {
if (nodes_.count(name))
throw NetworkBuildError("duplicate node name: " + name);
node.set_name(name);
nodes_.emplace(name, &node);
adj_[name];
return *this;
}
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
Network& connect(const std::string& src_name,
OutputPort<SrcNode, SrcIdx>,
const std::string& dst_name,
InputPort<DstNode, DstIdx>) {
using out_t = std::tuple_element_t<SrcIdx, typename SrcNode::return_tuple>;
using dst_in_t = std::tuple_element_t<DstIdx, typename DstNode::args_tuple>;
static_assert(std::is_same_v<out_t, dst_in_t>,
"connect: output type does not match input type");
auto* src = dynamic_cast<SrcNode*>(nodes_.at(src_name));
auto* dst = dynamic_cast<DstNode*>(nodes_.at(dst_name));
if (!src) throw NetworkBuildError("node '" + src_name + "' type mismatch");
if (!dst) throw NetworkBuildError("node '" + dst_name + "' type mismatch");
auto port_key = std::make_pair(src_name, SrcIdx);
if (connected_outputs_.count(port_key))
throw NetworkBuildError(
"connect: output port '" + src_name + "':" + std::to_string(SrcIdx)
+ " is already connected — use make_fanout<T, N> for fan-out");
connected_outputs_.insert(port_key);
auto& in_ch = dst->template input_channel<DstIdx>();
src->template set_output_channel<SrcIdx>(&in_ch);
// Register channel probe for diagnostics
std::string ch_name = src_name + ":" + std::to_string(SrcIdx)
+ "" + dst_name + ":" + std::to_string(DstIdx);
channel_probes_.push_back(
std::make_unique<ChannelProbe<out_t>>(in_ch, ch_name));
adj_[src_name].push_back(dst_name);
return *this;
}
template<typename NodeT, std::size_t Idx>
Network& expose_input(std::string boundary_name, InputPort<NodeT, Idx>) {
exposed_inputs_[boundary_name] = boundary_name;
return *this;
}
template<typename NodeT, std::size_t Idx>
Network& expose_output(std::string boundary_name, OutputPort<NodeT, Idx>) {
exposed_outputs_[boundary_name] = boundary_name;
return *this;
}
Network& build() {
topo_.clear();
std::map<std::string, int> color;
for (auto& [name, _] : nodes_)
if (color[name] == 0)
dfs(name, color);
return *this;
}
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
start_time_ = clock_t::now();
for (auto& name : topo_)
nodes_.at(name)->start();
start_watchdog();
#ifdef KPN_WEB_DEBUG
web_server_ = std::make_unique<web_debug::WebDebugServer>(
web_debug_port_,
[this]() {
auto s = collect_snapshots();
return web_debug::to_json(s.nodes, s.channels, {}, s.elapsed_s, s.pools);
});
web_server_->start();
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
#endif
}
void stop() override { halt(); }
// halt(): immediate stop — broadcasts disable to all channels and joins threads.
void halt() override {
#ifdef KPN_WEB_DEBUG
if (web_server_) web_server_->stop();
#endif
stop_watchdog();
for (auto it = topo_.rbegin(); it != topo_.rend(); ++it)
nodes_.at(*it)->stop();
}
// shutdown(): graceful drain in topological order.
// Stops source nodes first, polls until their output channels drain to zero,
// then stops the next layer, and so on.
void shutdown() override {
#ifdef KPN_WEB_DEBUG
if (web_server_) web_server_->stop();
#endif
stop_watchdog();
// Identify which nodes have no incoming edges (sources).
std::map<std::string, std::size_t> in_degree;
for (auto& [name, _] : nodes_) in_degree[name] = 0;
for (auto& [src, dsts] : adj_)
for (auto& dst : dsts) in_degree[dst]++;
// Walk topo order: stop each source layer, wait for its output channels
// to drain, then proceed to the next layer.
std::set<std::string> stopped;
for (auto& name : topo_) {
if (in_degree[name] == 0 || all_predecessors_stopped(name, stopped)) {
nodes_.at(name)->stop();
stopped.insert(name);
// Wait for output channels of this node to drain.
drain_output_channels(name);
}
}
// Stop any remaining nodes (sinks / nodes not yet stopped).
for (auto it = topo_.rbegin(); it != topo_.rend(); ++it)
if (!stopped.count(*it)) nodes_.at(*it)->stop();
}
bool running() const override { return watchdog_.joinable(); }
void set_name(std::string) override {}
const NodeStats& stats() const override {
static NodeStats dummy;
return dummy;
}
NodeSnapshot node_snapshot(const std::string& name, double) const override {
return {name, 0, 0, 0, 0, 0, 0, 0};
}
// ── Configuration ─────────────────────────────────────────────────────────
void set_watchdog_interval(std::chrono::milliseconds interval) {
watchdog_interval_ = interval;
}
void set_error_handler(ErrorHandler h) { error_handler_ = std::move(h); }
void set_diagnostics_handler(DiagnosticsHandler h) { diag_handler_ = std::move(h); }
void register_pool(const std::string& name, IPoolProbe* probe) {
pool_probes_.emplace_back(name, probe);
}
#ifdef KPN_WEB_DEBUG
void set_web_debug_port(uint16_t port) { web_debug_port_ = port; }
#endif
// Print a diagnostics report to a stream (default: stderr).
// Can be called at any time; thread-safe (reads atomics with relaxed ordering).
void print_diagnostics(std::ostream& os = std::cerr) const {
auto s = collect_snapshots();
os << format_report(s.nodes, s.channels, s.pools, s.elapsed_s);
}
private:
// ── Diagnostics collection ────────────────────────────────────────────────
struct Snapshots {
std::vector<NodeSnapshot> nodes;
std::vector<ChannelSnapshot> channels;
std::vector<PoolSnapshot> pools;
double elapsed_s;
};
Snapshots collect_snapshots() const {
double elapsed_s = std::chrono::duration<double>(
clock_t::now() - start_time_).count();
std::vector<NodeSnapshot> nodes;
for (auto& name : topo_)
nodes.push_back(nodes_.at(name)->node_snapshot(name, elapsed_s));
std::vector<ChannelSnapshot> channels;
for (auto& probe : channel_probes_)
channels.push_back(probe->snapshot());
std::vector<PoolSnapshot> pools;
for (auto& [name, probe] : pool_probes_)
pools.push_back(probe->snapshot(name));
return {std::move(nodes), std::move(channels), std::move(pools), elapsed_s};
}
static std::string format_report(const std::vector<NodeSnapshot>& nodes,
const std::vector<ChannelSnapshot>& channels,
const std::vector<PoolSnapshot>& pools = {},
double elapsed_s = 0.0) {
std::ostringstream os;
os << std::fixed << std::setprecision(1);
os << "\n┌─ KPN++ Diagnostics ────────────────────────────────────────────────────────\n";
// Node table
os << "│ Nodes:\n";
os << "" << std::left
<< std::setw(16) << "name"
<< std::setw(10) << "frames"
<< std::setw(12) << "exec ms"
<< std::setw(12) << "max ms"
<< std::setw(14) << "blocked ms"
<< std::setw(10) << "fps"
<< std::setw(12) << "cpu ms"
<< std::setw(10) << "util%"
<< "\n" << std::string(92, '-') << "\n";
for (auto& n : nodes) {
os << "" << std::left
<< std::setw(16) << n.name
<< std::setw(10) << n.frames_processed
<< std::setw(12) << n.ema_exec_ms
<< std::setw(12) << n.max_exec_ms
<< std::setw(14) << n.total_blocked_ms
<< std::setw(10) << n.throughput_fps
<< std::setw(12) << n.total_cpu_ms
<< std::setw(10) << n.cpu_util_pct
<< "\n";
}
// Channel table
os << "\n│ Channels:\n";
os << "" << std::left
<< std::setw(40) << "edge"
<< std::setw(8) << "fill%"
<< std::setw(8) << "peak%"
<< std::setw(8) << "pushes"
<< std::setw(8) << "drops"
<< std::setw(8) << "oflow"
<< std::setw(12) << "MB/s"
<< std::setw(10) << "item B"
<< "\n" << std::string(102, '-') << "\n";
for (auto& c : channels) {
std::string flag = c.fill_pct() >= 80.0 ? " <<<" :
c.peak_pct() >= 80.0 ? " (peak)" : "";
os << "" << std::left
<< std::setw(40) << c.name
<< std::setw(8) << c.fill_pct()
<< std::setw(8) << c.peak_pct()
<< std::setw(8) << c.pushes
<< std::setw(8) << c.drops
<< std::setw(8) << c.overflows
<< std::setw(12) << c.bandwidth_mbs(elapsed_s)
<< std::setw(10) << c.item_bytes
<< flag << "\n";
}
// Pool table
if (!pools.empty()) {
os << "\n│ Thread Pools:\n";
os << "" << std::left
<< std::setw(16) << "name"
<< std::setw(10) << "threads"
<< std::setw(12) << "queued"
<< std::setw(12) << "active"
<< std::setw(14) << "in/s"
<< std::setw(14) << "out/s"
<< "\n" << std::string(78, '-') << "\n";
for (auto& p : pools) {
double in_rate = elapsed_s > 0.0 ? p.tasks_submitted / elapsed_s : 0.0;
double out_rate = elapsed_s > 0.0 ? p.tasks_completed / elapsed_s : 0.0;
os << "" << std::left
<< std::setw(16) << p.name
<< std::setw(10) << p.thread_count
<< std::setw(12) << p.queue_depth
<< std::setw(12) << p.active_count
<< std::setw(14) << in_rate
<< std::setw(14) << out_rate
<< "\n";
}
}
// Bottleneck hint: node with highest ema_exec_ms
if (!nodes.empty()) {
auto it = std::max_element(nodes.begin(), nodes.end(),
[](const NodeSnapshot& a, const NodeSnapshot& b) {
return a.ema_exec_ms < b.ema_exec_ms;
});
os << "\n│ Bottleneck hint: '" << it->name
<< "' (avg exec " << it->ema_exec_ms << " ms)\n";
}
os << "└────────────────────────────────────────────────────────────────\n";
return os.str();
}
// ── Shutdown helpers ──────────────────────────────────────────────────────
bool all_predecessors_stopped(const std::string& name,
const std::set<std::string>& stopped) const {
for (auto& [src, dsts] : adj_)
for (auto& dst : dsts)
if (dst == name && !stopped.count(src)) return false;
return true;
}
void drain_output_channels(const std::string& /*name*/) const {
// Poll all channel probes until none report non-zero fill.
// A short sleep prevents busy-spin; 1 ms is fine for drain purposes.
bool any_full = true;
while (any_full) {
any_full = false;
for (auto& probe : channel_probes_) {
auto snap = probe->snapshot();
if (snap.current_fill > 0) { any_full = true; break; }
}
if (any_full)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
// ── Cycle detection / topological sort ───────────────────────────────────
void dfs(const std::string& name, std::map<std::string, int>& color) {
color[name] = 1;
for (auto& nbr : adj_[name]) {
if (color[nbr] == 1) throw NetworkCycleError{};
if (color[nbr] == 0) dfs(nbr, color);
}
color[name] = 2;
topo_.insert(topo_.begin(), name);
}
// ── Watchdog ──────────────────────────────────────────────────────────────
void start_watchdog() {
watchdog_ = std::jthread([this](std::stop_token tok) {
while (!tok.stop_requested()) {
std::this_thread::sleep_for(watchdog_interval_);
if (tok.stop_requested()) break;
auto s = collect_snapshots();
check_hung_nodes();
if (diag_handler_) {
diag_handler_(s.nodes, s.channels);
} else {
std::cerr << format_report(s.nodes, s.channels, s.pools, s.elapsed_s);
}
}
});
}
void check_hung_nodes() const {
auto now_us = std::chrono::duration_cast<std::chrono::microseconds>(
clock_t::now().time_since_epoch()).count();
for (auto& [name, node] : nodes_) {
int64_t start = node->stats().exec_start_us.load(std::memory_order_relaxed);
if (start == 0) continue;
int64_t elapsed_ms = (now_us - start) / 1000;
// Warn if a node has been executing for > 5 s with no max_exec_time set,
// or if it exceeds its configured max. Threshold: 5000 ms default.
if (elapsed_ms > 5000) {
std::cerr << "[kpn] WARNING: node '" << name
<< "' has been executing for " << elapsed_ms << " ms\n";
}
}
}
void stop_watchdog() {
if (watchdog_.joinable())
watchdog_.request_stop(), watchdog_.join();
}
// ── State ─────────────────────────────────────────────────────────────────
std::map<std::string, INode*> nodes_;
std::map<std::string, std::vector<std::string>> adj_;
std::vector<std::string> topo_;
std::map<std::string, std::string> exposed_inputs_;
std::map<std::string, std::string> exposed_outputs_;
std::set<std::pair<std::string, std::size_t>> connected_outputs_;
std::vector<std::unique_ptr<IChannelProbe>> channel_probes_;
std::vector<std::pair<std::string, IPoolProbe*>> pool_probes_;
ErrorHandler error_handler_;
DiagnosticsHandler diag_handler_;
std::chrono::milliseconds watchdog_interval_{3000};
std::jthread watchdog_;
clock_t::time_point start_time_;
#ifdef KPN_WEB_DEBUG
uint16_t web_debug_port_{9090};
std::unique_ptr<web_debug::WebDebugServer> web_server_;
#endif
};
} // namespace kpn
-127
View File
@@ -1,127 +0,0 @@
#pragma once
#include "inode.hpp"
#include "pool_node.hpp" // PoolNode, PoolObjectNode
// node.hpp — Node<> and ObjectNode<> as thin wrappers over PoolNode<>.
//
// Each Node owns a private single-thread ThreadPool so the API is unchanged:
// node.start() / node.stop() are self-contained with no external scheduler.
// Internally, all execution goes through PoolNode::fire_once() — the same code
// path as explicitly pool-scheduled nodes.
//
// To share a thread pool across multiple nodes, use make_pool_node() directly.
namespace kpn {
namespace detail {
// Private base initialized before PoolNode so its pool can be passed to the
// PoolNode constructor (C++ initialises bases left-to-right).
struct NodePrivatePool {
std::shared_ptr<ThreadPool> pool{std::make_shared<ThreadPool>(1)};
};
} // namespace detail
// ── Node ─────────────────────────────────────────────────────────────────────
template<auto Func,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class Node;
template<auto Func, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class Node<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag>
: private detail::NodePrivatePool
, public PoolNode<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag> {
using Base = PoolNode<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag>;
public:
explicit Node(std::size_t fifo_capacity = 5)
: detail::NodePrivatePool{}
, Base(pool, fifo_capacity)
{}
~Node() override { stop(); }
void start() override { pool->start(); Base::start(); }
void stop() override { Base::stop(); pool->stop(); }
};
// ── ObjectNode ────────────────────────────────────────────────────────────────
template<typename Obj,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class ObjectNode;
template<typename Obj, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class ObjectNode<Obj, in<InNames...>, out<OutNames...>, Label, UniqueTag>
: private detail::NodePrivatePool
, public PoolObjectNode<Obj, in<InNames...>, out<OutNames...>, Label, UniqueTag> {
using Base = PoolObjectNode<Obj, in<InNames...>, out<OutNames...>, Label, UniqueTag>;
public:
explicit ObjectNode(Obj& obj, std::size_t fifo_capacity = 5)
: detail::NodePrivatePool{}
, Base(obj, pool, fifo_capacity)
{}
~ObjectNode() override { stop(); }
void start() override { pool->start(); Base::start(); }
void stop() override { Base::stop(); pool->stop(); }
};
// ── make_node overloads for callable objects ──────────────────────────────────
template<typename Obj>
auto make_node(Obj& obj, std::size_t fifo_capacity = 5) {
return ObjectNode<Obj, in<>, out<>>(obj, fifo_capacity);
}
template<typename Obj, fixed_string... InNames>
auto make_node(Obj& obj, in<InNames...>, std::size_t fifo_capacity = 5) {
return ObjectNode<Obj, in<InNames...>, out<>>(obj, fifo_capacity);
}
template<typename Obj, fixed_string... OutNames>
auto make_node(Obj& obj, out<OutNames...>, std::size_t fifo_capacity = 5) {
return ObjectNode<Obj, in<>, out<OutNames...>>(obj, fifo_capacity);
}
template<typename Obj, fixed_string... InNames, fixed_string... OutNames>
auto make_node(Obj& obj, in<InNames...>, out<OutNames...>, std::size_t fifo_capacity = 5) {
return ObjectNode<Obj, in<InNames...>, out<OutNames...>>(obj, fifo_capacity);
}
// ── make_node factory (NTTP) ──────────────────────────────────────────────────
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0>
auto make_node(std::size_t fifo_capacity = 5) {
return Node<Func, in<>, out<>, Label, UniqueTag>(fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames>
auto make_node(in<InNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<InNames...>, out<>, Label, UniqueTag>(fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... OutNames>
auto make_node(out<OutNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<>, out<OutNames...>, Label, UniqueTag>(fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames, fixed_string... OutNames>
auto make_node(in<InNames...>, out<OutNames...>, std::size_t fifo_capacity = 5) {
return Node<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag>(fifo_capacity);
}
} // namespace kpn
-696
View File
@@ -1,696 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "fixed_string.hpp"
#include "inode.hpp"
#include "port.hpp"
#include "scheduler.hpp"
#include "traits.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <thread>
#include <tuple>
#include <type_traits>
namespace kpn {
// ── PoolNode ──────────────────────────────────────────────────────────────────
//
// Reactive alternative to Node<>. Instead of owning a blocked thread, the node
// is submitted to a shared IScheduler whenever all its input channels become
// non-empty. A single fire_once() call pops all inputs, executes the function,
// and pushes outputs. At most one fire_once() runs at a time (queued_ flag).
//
// Source nodes (input_count == 0) submit themselves immediately on start() and
// resubmit after each fire_once().
//
// Multiple PoolNodes can share one ThreadPool for resource-bounded execution,
// or each can have a dedicated single-thread pool for serialisation.
template<auto Func,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class PoolNode;
template<auto Func, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class PoolNode<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag> : public INode {
public:
using F = decltype(Func);
using args_tuple = args_t<F>;
using return_raw = return_t<F>;
using return_tuple = normalised_return_t<return_raw>;
static constexpr std::string_view label() { return Label.view(); }
static constexpr std::size_t unique_tag = UniqueTag;
static constexpr std::size_t input_count = arity_v<F>;
static constexpr std::size_t output_count = std::tuple_size_v<return_tuple>;
static_assert(
sizeof...(InNames) == 0 || sizeof...(InNames) == input_count,
"make_pool_node: number of input names must match function arity, or provide none"
);
static_assert(
sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count,
"make_pool_node: number of output names must match return tuple size, or provide none"
);
explicit PoolNode(std::shared_ptr<IScheduler> sched, std::size_t fifo_capacity = 5)
: scheduler_(std::move(sched)), fifo_capacity_(fifo_capacity)
{
init_input_channels(std::make_index_sequence<input_count>{});
}
~PoolNode() override { stop(); }
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
enable_inputs(std::make_index_sequence<input_count>{});
stop_flag_.store(false, std::memory_order_relaxed);
queued_.store(false, std::memory_order_relaxed);
register_callbacks(std::make_index_sequence<input_count>{});
if constexpr (input_count == 0)
try_submit(0.5f);
}
void stop() override {
stop_flag_.store(true, std::memory_order_seq_cst);
disable_inputs(std::make_index_sequence<input_count>{});
// fire_once() observes stop_flag_ and will not resubmit.
// We do not wait for an in-flight fire_once() to complete here;
// callers that need that guarantee should call scheduler_->drain() first.
}
bool running() const override {
return !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override { name_ = std::move(name); }
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double qwait_ms = stats_.queue_wait_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {
name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0,
qwait_ms,
};
}
// ── Port access — by index ────────────────────────────────────────────────
template<std::size_t I>
InputPort<PoolNode, I> input() {
static_assert(I < input_count, "input index out of range");
return {*this};
}
template<std::size_t I>
OutputPort<PoolNode, I> output() {
static_assert(I < output_count, "output index out of range");
return {*this};
}
// ── Port access — by name ─────────────────────────────────────────────────
template<fixed_string Name>
auto input() {
constexpr std::size_t idx = index_of<Name, InNames...>();
static_assert(idx != npos, "unknown input port name");
return input<idx>();
}
template<fixed_string Name>
auto output() {
constexpr std::size_t idx = index_of<Name, OutNames...>();
static_assert(idx != npos, "unknown output port name");
return output<idx>();
}
// ── Internal channel accessors ────────────────────────────────────────────
template<std::size_t I>
Channel<std::tuple_element_t<I, args_tuple>>& input_channel() {
return *std::get<I>(input_channels_);
}
template<std::size_t I>
void set_input_channel(
std::shared_ptr<Channel<std::tuple_element_t<I, args_tuple>>> ch) {
std::get<I>(input_channels_) = std::move(ch);
}
template<std::size_t I>
void set_output_channel(
Channel<std::tuple_element_t<I, return_tuple>>* ch) {
std::get<I>(output_channels_) = ch;
}
private:
// ── Channel storage ───────────────────────────────────────────────────────
template<std::size_t... Is>
void init_input_channels(std::index_sequence<Is...>) {
((std::get<Is>(input_channels_) =
std::make_shared<Channel<std::tuple_element_t<Is, args_tuple>>>(fifo_capacity_)),
...);
}
template<std::size_t... Is>
void enable_inputs(std::index_sequence<Is...>) {
(std::get<Is>(input_channels_)->enable(), ...);
}
template<std::size_t... Is>
void disable_inputs(std::index_sequence<Is...>) {
(std::get<Is>(input_channels_)->disable(), ...);
}
template<std::size_t... Is>
void register_callbacks(std::index_sequence<Is...>) {
(std::get<Is>(input_channels_)->set_push_callback(
[this] { on_input_ready(); }), ...);
}
template<typename Tup, std::size_t... Is>
static auto make_input_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<std::shared_ptr<Channel<std::tuple_element_t<Is, Tup>>>...>;
using input_channels_t = decltype(make_input_channel_tuple<args_tuple>(
std::make_index_sequence<input_count>{}));
template<typename Tup, std::size_t... Is>
static auto make_output_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<Channel<std::tuple_element_t<Is, Tup>>*...>;
using output_channels_t = decltype(make_output_channel_tuple<return_tuple>(
std::make_index_sequence<output_count>{}));
// ── Scheduling ────────────────────────────────────────────────────────────
// Called by channel push_callbacks (on the producer's thread).
void on_input_ready() {
if (stop_flag_.load(std::memory_order_relaxed)) return;
std::size_t ready = count_ready(std::make_index_sequence<input_count>{});
if (ready == input_count)
try_submit(compute_priority());
}
template<std::size_t... Is>
std::size_t count_ready(std::index_sequence<Is...>) {
return ((std::get<Is>(input_channels_)->approx_size() > 0 ? 1u : 0u) + ...);
}
float compute_priority() {
if constexpr (input_count == 0) return 0.5f;
float sum = 0.0f;
sum_fill(sum, std::make_index_sequence<input_count>{});
return sum / static_cast<float>(input_count);
}
template<std::size_t... Is>
void sum_fill(float& sum, std::index_sequence<Is...>) {
((sum += std::get<Is>(input_channels_)->capacity() > 0
? float(std::get<Is>(input_channels_)->approx_size())
/ float(std::get<Is>(input_channels_)->capacity())
: 0.5f), ...);
}
void try_submit(float priority) {
bool expected = false;
if (queued_.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
scheduler_->submit([this] { fire_once(); }, priority);
}
// ── Execution ─────────────────────────────────────────────────────────────
void fire_once() {
if (stop_flag_.load(std::memory_order_relaxed)) {
queued_.store(false, std::memory_order_release);
return;
}
// Record queue wait time (submission → now) and mark as executing
auto t0 = clock_t::now();
int64_t now_us = std::chrono::duration_cast<std::chrono::microseconds>(
t0.time_since_epoch()).count();
stats_.exec_start_us.store(now_us, std::memory_order_relaxed);
try {
auto args = pop_inputs(std::make_index_sequence<input_count>{});
auto t1 = clock_t::now();
stats_.record_queue_wait(duration_t(t1 - t0));
auto cpu0 = NodeStats::cpu_now();
if constexpr (std::is_void_v<return_raw>) {
std::apply(Func, args);
} else {
auto result = std::apply(Func, args);
push_outputs(normalise(std::move(result)),
std::make_index_sequence<output_count>{});
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
// blocked_time = 0 for pool nodes (we don't block waiting for inputs)
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
} catch (const ChannelClosedError&) {
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
stop_flag_.store(true, std::memory_order_relaxed);
return;
} catch (const ChannelOverflowError& e) {
std::cerr << "[kpn] pool overflow: " << e.what() << "\n";
} catch (...) {
if (error_handler_ && error_handler_(name_, std::current_exception())) {
// continue — fall through to resubmit check
} else {
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
stop_flag_.store(true, std::memory_order_relaxed);
return;
}
}
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
if (stop_flag_.load(std::memory_order_relaxed)) return;
// Source nodes always resubmit; others resubmit only if inputs are ready.
if constexpr (input_count == 0) {
try_submit(0.5f);
} else {
on_input_ready();
}
}
// Pop all inputs — safe because we're the sole consumer and fire_once
// is guarded by queued_ (only one fire_once runs at a time).
template<std::size_t... Is>
args_tuple pop_inputs(std::index_sequence<Is...>) {
return {pop_one<Is>()...};
}
template<std::size_t I>
std::tuple_element_t<I, args_tuple> pop_one() {
auto& ch = *std::get<I>(input_channels_);
std::tuple_element_t<I, args_tuple> val;
if (!ch.try_pop_now(val))
throw ChannelClosedError{};
return val;
}
template<typename R = return_raw>
static return_tuple normalise(R&& r) {
if constexpr (is_tuple_v<R>) return std::move(r);
else return std::make_tuple(std::move(r));
}
template<std::size_t... Is>
void push_outputs(return_tuple&& result, std::index_sequence<Is...>) {
(push_one_out<Is>(std::get<Is>(std::move(result))), ...);
}
template<std::size_t I>
void push_one_out(std::tuple_element_t<I, return_tuple>&& val) {
auto* ch = std::get<I>(output_channels_);
if (!ch) return;
try {
ch->push(std::move(val));
} catch (const ChannelOverflowError&) {
throw ChannelOverflowError(ch->capacity(),
"pool node '" + name_ + "' " + output_port_label<I>());
}
}
template<std::size_t I>
static std::string output_port_label() {
if constexpr (sizeof...(OutNames) > 0) {
constexpr std::array<std::string_view, sizeof...(OutNames)> names{OutNames.view()...};
return std::string("output['") + std::string(names[I]) + "']";
} else {
return "output[" + std::to_string(I) + "]";
}
}
// ── State ─────────────────────────────────────────────────────────────────
std::shared_ptr<IScheduler> scheduler_;
std::string name_;
std::size_t fifo_capacity_;
input_channels_t input_channels_;
output_channels_t output_channels_{};
std::atomic<bool> stop_flag_{true};
std::atomic<bool> queued_{false};
NodeStats stats_;
NodeErrorHandler error_handler_;
std::chrono::milliseconds max_exec_time_{0};
};
// ── PoolObjectNode ────────────────────────────────────────────────────────────
//
// Same as PoolNode but wraps a stateful callable object (functor / class with
// operator()). The object must outlive the PoolObjectNode.
template<typename Obj,
typename InputTag = in<>,
typename OutputTag = out<>,
fixed_string Label = "",
std::size_t UniqueTag = 0>
class PoolObjectNode;
template<typename Obj, fixed_string... InNames, fixed_string... OutNames,
fixed_string Label, std::size_t UniqueTag>
class PoolObjectNode<Obj, in<InNames...>, out<OutNames...>, Label, UniqueTag> : public INode {
public:
using F = decltype(&Obj::operator());
using args_tuple = args_t<F>;
using return_raw = return_t<F>;
using return_tuple = normalised_return_t<return_raw>;
static constexpr std::string_view label() { return Label.view(); }
static constexpr std::size_t unique_tag = UniqueTag;
static constexpr std::size_t input_count = arity_v<F>;
static constexpr std::size_t output_count = std::tuple_size_v<return_tuple>;
static_assert(
sizeof...(InNames) == 0 || sizeof...(InNames) == input_count,
"make_pool_node: number of input names must match operator() arity, or provide none"
);
static_assert(
sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count,
"make_pool_node: number of output names must match return tuple size, or provide none"
);
explicit PoolObjectNode(Obj& obj, std::shared_ptr<IScheduler> sched,
std::size_t fifo_capacity = 5)
: obj_(obj), scheduler_(std::move(sched)), fifo_capacity_(fifo_capacity)
{
init_input_channels(std::make_index_sequence<input_count>{});
}
~PoolObjectNode() override { stop(); }
void start() override {
enable_inputs(std::make_index_sequence<input_count>{});
stop_flag_.store(false, std::memory_order_relaxed);
queued_.store(false, std::memory_order_relaxed);
register_callbacks(std::make_index_sequence<input_count>{});
if constexpr (input_count == 0)
try_submit(0.5f);
}
void stop() override {
stop_flag_.store(true, std::memory_order_seq_cst);
disable_inputs(std::make_index_sequence<input_count>{});
}
bool running() const override { return !stop_flag_.load(std::memory_order_relaxed); }
void set_name(std::string name) override { name_ = std::move(name); }
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blocked_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double qwait_ms = stats_.queue_wait_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blocked_ms;
return {
name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blocked_ms,
elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0,
qwait_ms,
};
}
template<std::size_t I> InputPort<PoolObjectNode, I> input() { return {*this}; }
template<std::size_t I> OutputPort<PoolObjectNode, I> output() { return {*this}; }
template<fixed_string Name>
auto input() {
constexpr std::size_t idx = index_of<Name, InNames...>();
static_assert(idx != npos, "unknown input port name");
return input<idx>();
}
template<fixed_string Name>
auto output() {
constexpr std::size_t idx = index_of<Name, OutNames...>();
static_assert(idx != npos, "unknown output port name");
return output<idx>();
}
template<std::size_t I>
Channel<std::tuple_element_t<I, args_tuple>>& input_channel() {
return *std::get<I>(input_channels_);
}
template<std::size_t I>
void set_input_channel(std::shared_ptr<Channel<std::tuple_element_t<I, args_tuple>>> ch) {
std::get<I>(input_channels_) = std::move(ch);
}
template<std::size_t I>
void set_output_channel(Channel<std::tuple_element_t<I, return_tuple>>* ch) {
std::get<I>(output_channels_) = ch;
}
private:
template<std::size_t... Is>
void init_input_channels(std::index_sequence<Is...>) {
((std::get<Is>(input_channels_) =
std::make_shared<Channel<std::tuple_element_t<Is, args_tuple>>>(fifo_capacity_)),
...);
}
template<std::size_t... Is> void enable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->enable(), ...); }
template<std::size_t... Is> void disable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->disable(), ...); }
template<std::size_t... Is>
void register_callbacks(std::index_sequence<Is...>) {
(std::get<Is>(input_channels_)->set_push_callback([this] { on_input_ready(); }), ...);
}
template<typename Tup, std::size_t... Is>
static auto make_input_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<std::shared_ptr<Channel<std::tuple_element_t<Is, Tup>>>...>;
using input_channels_t = decltype(make_input_channel_tuple<args_tuple>(
std::make_index_sequence<input_count>{}));
template<typename Tup, std::size_t... Is>
static auto make_output_channel_tuple(std::index_sequence<Is...>)
-> std::tuple<Channel<std::tuple_element_t<Is, Tup>>*...>;
using output_channels_t = decltype(make_output_channel_tuple<return_tuple>(
std::make_index_sequence<output_count>{}));
void on_input_ready() {
if (stop_flag_.load(std::memory_order_relaxed)) return;
std::size_t ready = count_ready(std::make_index_sequence<input_count>{});
if (ready == input_count) try_submit(compute_priority());
}
template<std::size_t... Is>
std::size_t count_ready(std::index_sequence<Is...>) {
return ((std::get<Is>(input_channels_)->approx_size() > 0 ? 1u : 0u) + ...);
}
float compute_priority() {
if constexpr (input_count == 0) return 0.5f;
float sum = 0.0f;
sum_fill(sum, std::make_index_sequence<input_count>{});
return sum / static_cast<float>(input_count);
}
template<std::size_t... Is>
void sum_fill(float& sum, std::index_sequence<Is...>) {
((sum += std::get<Is>(input_channels_)->capacity() > 0
? float(std::get<Is>(input_channels_)->approx_size())
/ float(std::get<Is>(input_channels_)->capacity())
: 0.5f), ...);
}
void try_submit(float priority) {
bool expected = false;
if (queued_.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
scheduler_->submit([this] { fire_once(); }, priority);
}
void fire_once() {
if (stop_flag_.load(std::memory_order_relaxed)) {
queued_.store(false, std::memory_order_release);
return;
}
auto t0 = clock_t::now();
int64_t now_us = std::chrono::duration_cast<std::chrono::microseconds>(
t0.time_since_epoch()).count();
stats_.exec_start_us.store(now_us, std::memory_order_relaxed);
try {
auto args = pop_inputs(std::make_index_sequence<input_count>{});
auto t1 = clock_t::now();
stats_.record_queue_wait(duration_t(t1 - t0));
auto cpu0 = NodeStats::cpu_now();
if constexpr (std::is_void_v<return_raw>) {
std::apply([this](auto&&... a) { obj_(std::forward<decltype(a)>(a)...); }, args);
} else {
auto result = std::apply([this](auto&&... a) { return obj_(std::forward<decltype(a)>(a)...); }, args);
push_outputs(normalise(std::move(result)), std::make_index_sequence<output_count>{});
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
} catch (const ChannelClosedError&) {
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
stop_flag_.store(true, std::memory_order_relaxed);
return;
} catch (const ChannelOverflowError& e) {
std::cerr << "[kpn] pool overflow: " << e.what() << "\n";
} catch (...) {
if (error_handler_ && error_handler_(name_, std::current_exception())) {
} else {
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
stop_flag_.store(true, std::memory_order_relaxed);
return;
}
}
stats_.exec_start_us.store(0, std::memory_order_relaxed);
queued_.store(false, std::memory_order_release);
if (stop_flag_.load(std::memory_order_relaxed)) return;
if constexpr (input_count == 0) try_submit(0.5f);
else on_input_ready();
}
template<std::size_t... Is>
args_tuple pop_inputs(std::index_sequence<Is...>) { return {pop_one<Is>()...}; }
template<std::size_t I>
std::tuple_element_t<I, args_tuple> pop_one() {
auto& ch = *std::get<I>(input_channels_);
std::tuple_element_t<I, args_tuple> val;
if (!ch.try_pop_now(val)) throw ChannelClosedError{};
return val;
}
template<typename R = return_raw>
static return_tuple normalise(R&& r) {
if constexpr (is_tuple_v<R>) return std::move(r);
else return std::make_tuple(std::move(r));
}
template<std::size_t... Is>
void push_outputs(return_tuple&& result, std::index_sequence<Is...>) {
(push_one_out<Is>(std::get<Is>(std::move(result))), ...);
}
template<std::size_t I>
void push_one_out(std::tuple_element_t<I, return_tuple>&& val) {
auto* ch = std::get<I>(output_channels_);
if (!ch) return;
try {
ch->push(std::move(val));
} catch (const ChannelOverflowError&) {
throw ChannelOverflowError(ch->capacity(),
"pool node '" + name_ + "'");
}
}
Obj& obj_;
std::shared_ptr<IScheduler> scheduler_;
std::string name_;
std::size_t fifo_capacity_;
input_channels_t input_channels_;
output_channels_t output_channels_{};
std::atomic<bool> stop_flag_{true};
std::atomic<bool> queued_{false};
NodeStats stats_;
NodeErrorHandler error_handler_;
std::chrono::milliseconds max_exec_time_{0};
};
// ── make_pool_node factory (NTTP) ─────────────────────────────────────────────
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0>
auto make_pool_node(std::shared_ptr<IScheduler> sched, std::size_t fifo_capacity = 5) {
return PoolNode<Func, in<>, out<>, Label, UniqueTag>(std::move(sched), fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames>
auto make_pool_node(std::shared_ptr<IScheduler> sched, in<InNames...>,
std::size_t fifo_capacity = 5) {
return PoolNode<Func, in<InNames...>, out<>, Label, UniqueTag>(std::move(sched), fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... OutNames>
auto make_pool_node(std::shared_ptr<IScheduler> sched, out<OutNames...>,
std::size_t fifo_capacity = 5) {
return PoolNode<Func, in<>, out<OutNames...>, Label, UniqueTag>(std::move(sched), fifo_capacity);
}
template<auto Func, fixed_string Label = "", std::size_t UniqueTag = 0,
fixed_string... InNames, fixed_string... OutNames>
auto make_pool_node(std::shared_ptr<IScheduler> sched, in<InNames...>, out<OutNames...>,
std::size_t fifo_capacity = 5) {
return PoolNode<Func, in<InNames...>, out<OutNames...>, Label, UniqueTag>(
std::move(sched), fifo_capacity);
}
// ── make_pool_node factory (callable object) ──────────────────────────────────
template<typename Obj>
auto make_pool_node(Obj& obj, std::shared_ptr<IScheduler> sched,
std::size_t fifo_capacity = 5) {
return PoolObjectNode<Obj, in<>, out<>>(obj, std::move(sched), fifo_capacity);
}
template<typename Obj, fixed_string... InNames>
auto make_pool_node(Obj& obj, std::shared_ptr<IScheduler> sched, in<InNames...>,
std::size_t fifo_capacity = 5) {
return PoolObjectNode<Obj, in<InNames...>, out<>>(obj, std::move(sched), fifo_capacity);
}
template<typename Obj, fixed_string... OutNames>
auto make_pool_node(Obj& obj, std::shared_ptr<IScheduler> sched, out<OutNames...>,
std::size_t fifo_capacity = 5) {
return PoolObjectNode<Obj, in<>, out<OutNames...>>(obj, std::move(sched), fifo_capacity);
}
template<typename Obj, fixed_string... InNames, fixed_string... OutNames>
auto make_pool_node(Obj& obj, std::shared_ptr<IScheduler> sched,
in<InNames...>, out<OutNames...>,
std::size_t fifo_capacity = 5) {
return PoolObjectNode<Obj, in<InNames...>, out<OutNames...>>(
obj, std::move(sched), fifo_capacity);
}
} // namespace kpn
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include <cstddef>
namespace kpn {
// Forward-declared so port handles can reference a node without including node.hpp
template<typename NodeT, std::size_t Idx>
struct InputPort {
NodeT& node;
};
template<typename NodeT, std::size_t Idx>
struct OutputPort {
NodeT& node;
};
} // namespace kpn
-311
View File
@@ -1,311 +0,0 @@
#pragma once
// Auto-binding helpers for KPN++ Python bindings.
//
// Usage in your binding .cpp:
//
// #define KPN_BUILD_PYTHON
// #include <kpn/python/auto_bind.hpp>
//
// int produce() { return 42; }
// int double_it(int x) { return x * 2; }
// void print_it(int x) { std::cout << x << '\n'; }
//
// using MyNodes = kpn::python::NodeRegistry<
// kpn::python::Entry<produce, "produce">,
// kpn::python::Entry<double_it, "double_it">,
// kpn::python::Entry<print_it, "print_it">
// >;
//
// NB_MODULE(my_kpn, m) {
// kpn::python::bind_network<MyNodes>(m); // KPN_BIND_PYTHON behaviour
// kpn::python::bind_debug<MyNodes>(m); // KPN_PYTHON_DEBUG behaviour
// }
//
// bind_network registers:
// - Network class (PyNetwork<auto-deduced-variant>) with auto-registered converters
// - make_<name>(capacity=5) factory for each entry
// - <Name>Node class for each entry
//
// bind_debug additionally registers each raw C++ function as a free Python
// callable (e.g. double_it(5) → 10) so node logic can be tested without a network.
//
// To support a custom type T, specialise kpn::PythonConverter<T> before calling
// bind_network:
//
// namespace kpn {
// template<> struct PythonConverter<MyVec3> {
// static constexpr const char* type_name = "vec3"; // optional friendly name
// static nb::object to_python(const MyVec3& v) { ... }
// static MyVec3 from_python(nb::object o) { ... }
// };
// } // namespace kpn
#include "../variant_node.hpp"
#include "../traits.hpp"
#include "bindings.hpp"
#ifdef KPN_BUILD_PYTHON
#include <nanobind/nanobind.h>
#include <nanobind/stl/shared_ptr.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
#include <cctype>
#include <string>
#include <tuple>
#include <type_traits>
// ── PythonConverter specialisations for built-in nanobind-castable types ─────
// These live in kpn:: to match the primary template in variant_node.hpp.
namespace kpn {
template<> struct PythonConverter<int> {
static constexpr const char* type_name = "int";
static nanobind::object to_python(const int& v) { return nanobind::cast(v); }
static int from_python(nanobind::object o) { return nanobind::cast<int>(std::move(o)); }
};
template<> struct PythonConverter<float> {
static constexpr const char* type_name = "float";
static nanobind::object to_python(const float& v) { return nanobind::cast(v); }
static float from_python(nanobind::object o) { return nanobind::cast<float>(std::move(o)); }
};
template<> struct PythonConverter<double> {
static constexpr const char* type_name = "double";
static nanobind::object to_python(const double& v) { return nanobind::cast(v); }
static double from_python(nanobind::object o) { return nanobind::cast<double>(std::move(o)); }
};
template<> struct PythonConverter<bool> {
static constexpr const char* type_name = "bool";
static nanobind::object to_python(const bool& v) { return nanobind::cast(v); }
static bool from_python(nanobind::object o) { return nanobind::cast<bool>(std::move(o)); }
};
template<> struct PythonConverter<std::string> {
static constexpr const char* type_name = "str";
static nanobind::object to_python(const std::string& v) { return nanobind::cast(v); }
static std::string from_python(nanobind::object o) {
return nanobind::cast<std::string>(std::move(o));
}
};
} // namespace kpn
namespace kpn::python {
namespace nb = nanobind;
// ── Entry<Func, Name> ─────────────────────────────────────────────────────────
// Compile-time descriptor for one bindable node function.
template<auto Func, fixed_string Name>
struct Entry {
static constexpr auto func = Func;
static constexpr auto name = Name;
};
// ── NodeRegistry<Es...> ───────────────────────────────────────────────────────
template<typename... Es>
struct NodeRegistry {
using entries_tuple = std::tuple<Es...>;
static constexpr std::size_t size = sizeof...(Es);
};
// ── Internal TMP ──────────────────────────────────────────────────────────────
namespace detail {
// All non-void port types for a single function (args + normalised returns).
template<auto Func>
struct entry_port_types {
using args = args_t<decltype(Func)>;
using ret = normalised_return_t<return_t<decltype(Func)>>;
using type = decltype(std::tuple_cat(std::declval<args>(), std::declval<ret>()));
};
// Flat tuple of all types across all entries (may contain duplicates).
template<typename... Es>
struct all_types_flat {
using type = decltype(std::tuple_cat(
std::declval<typename entry_port_types<Es::func>::type>()...));
};
template<typename Registry>
struct registry_flat_types;
template<typename... Es>
struct registry_flat_types<NodeRegistry<Es...>> {
using type = typename all_types_flat<Es...>::type;
};
// Unpack a tuple into unique_types_t (which takes a pack, not a tuple).
// unique_types_t<T> takes Ts... not std::tuple<Ts...>, so we need this bridge.
template<typename Tuple>
struct unpack_unique;
template<typename... Ts>
struct unpack_unique<std::tuple<Ts...>> {
using type = kpn::detail::unique_types_t<Ts...>;
};
// SFINAE: does PythonConverter<T> have a 'type_name' member?
template<typename Conv, typename = void>
struct has_type_name : std::false_type {};
template<typename Conv>
struct has_type_name<Conv, std::void_t<decltype(Conv::type_name)>> : std::true_type {};
inline std::string make_class_name(std::string_view snake) {
std::string result(snake);
if (!result.empty()) result[0] = static_cast<char>(std::toupper(result[0]));
result += "Node";
return result;
}
} // namespace detail
// ── registry_variant_t<Registry> ─────────────────────────────────────────────
// Deduces std::variant<UniqueTypes...> from all port types across the registry.
template<typename Registry>
using registry_variant_t = typename kpn::detail::tuple_to_variant<
typename detail::unpack_unique<
typename detail::registry_flat_types<Registry>::type>::type
>::type;
// ── Converter registration ────────────────────────────────────────────────────
template<typename T, typename Variant>
void register_one_type(PyNetwork<Variant>& net) {
const char* friendly = nullptr;
if constexpr (detail::has_type_name<PythonConverter<T>>::value)
friendly = PythonConverter<T>::type_name;
net.template register_full_type<T>(
[](const T& v) -> nb::object { return PythonConverter<T>::to_python(v); },
[](nb::object o) -> T { return PythonConverter<T>::from_python(std::move(o)); },
friendly);
}
template<typename Variant, typename... Ts>
void register_types_impl(PyNetwork<Variant>& net, std::tuple<Ts...>*) {
(register_one_type<Ts>(net), ...);
}
template<typename Registry, typename Variant>
void register_all_converters(PyNetwork<Variant>& net) {
using Flat = typename detail::registry_flat_types<Registry>::type;
using Unique = typename detail::unpack_unique<Flat>::type;
register_types_impl(net, static_cast<Unique*>(nullptr));
}
// ── Per-entry class + factory registration ───────────────────────────────────
namespace detail {
template<typename E, typename Variant>
void register_one_entry(nb::module_& m) {
using Wrapper = VariantNodeWrapper<E::func, Variant>;
auto class_name = make_class_name(E::name.view());
auto make_name = "make_" + std::string(E::name.view());
nb::class_<Wrapper, IVariantNode<Variant>>(m, class_name.c_str())
.def("__init__", [](Wrapper* self, std::size_t cap) {
new (self) Wrapper(cap);
}, nb::arg("capacity") = 5);
m.def(make_name.c_str(),
[](std::size_t cap) -> std::shared_ptr<IVariantNode<Variant>> {
return std::make_shared<Wrapper>(cap);
},
nb::arg("capacity") = 5);
}
template<typename Variant, typename... Es>
void register_entries_impl(nb::module_& m, std::tuple<Es...>*) {
(register_one_entry<Es, Variant>(m), ...);
}
} // namespace detail
// ── bind_network<Registry> ────────────────────────────────────────────────────
// Registers:
// - INode — base class (opaque Python handle)
// - Network — PyNetwork with auto-registered converters
// - <Name>Node — VariantNodeWrapper for each entry
// - make_<name>() — factory returning shared_ptr<INode>
template<typename Registry>
void bind_network(nb::module_& m) {
using Variant = registry_variant_t<Registry>;
using Net = PyNetwork<Variant>;
using Entries = typename Registry::entries_tuple;
nb::class_<IVariantNode<Variant>>(m, "INode");
nb::class_<Net>(m, "Network")
.def("__init__", [](Net* self) {
new (self) Net();
register_all_converters<Registry>(*self);
})
// add(name, c++_node)
.def("add", [](Net& self, std::string name,
std::shared_ptr<IVariantNode<Variant>> node) {
self.add(std::move(name), std::move(node));
}, nb::arg("name"), nb::arg("node"))
// add_node(name, callable, inputs=[...], outputs=[...], capacity=5)
.def("add_node", &Net::add_node_python,
nb::arg("name"),
nb::arg("callable"),
nb::arg("inputs") = std::vector<std::string>{},
nb::arg("outputs") = std::vector<std::string>{},
nb::arg("capacity") = std::size_t(5))
.def("connect", &Net::connect,
nb::arg("src"), nb::arg("out_idx"),
nb::arg("dst"), nb::arg("in_idx"))
.def("build", &Net::build)
.def("start", &Net::start)
.def("stop", &Net::stop)
.def("read", &Net::read,
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
.def("write", &Net::write,
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"))
;
detail::register_entries_impl<Variant>(m, static_cast<Entries*>(nullptr));
}
// ── bind_debug<Registry> ─────────────────────────────────────────────────────
// Exposes each node's raw C++ function as a free Python callable so node logic
// can be unit-tested without constructing a network.
//
// Example: assert kpn.double_it(5) == 10
namespace detail {
template<typename E>
void bind_one_debug(nb::module_& m) {
auto name_str = std::string(E::name.view());
m.def(name_str.c_str(), E::func);
}
template<typename... Es>
void bind_debug_impl(nb::module_& m, std::tuple<Es...>*) {
(bind_one_debug<Es>(m), ...);
}
} // namespace detail
template<typename Registry>
void bind_debug(nb::module_& m) {
using Entries = typename Registry::entries_tuple;
detail::bind_debug_impl(m, static_cast<Entries*>(nullptr));
}
} // namespace kpn::python
#endif // KPN_BUILD_PYTHON
-466
View File
@@ -1,466 +0,0 @@
#pragma once
// Nanobind binding helpers for KPN++ Python interface.
// Included only by python/kpn_python.cpp — do not include from core headers.
#include "../variant_node.hpp"
#include "../network.hpp"
#ifdef KPN_BUILD_PYTHON
#include <nanobind/nanobind.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>
#include <functional>
#include <map>
#include <stdexcept>
#include <string>
#include <typeindex>
#include <vector>
namespace kpn::python {
namespace nb = nanobind;
// ── PyNetwork<Variant> ────────────────────────────────────────────────────────
// Runtime graph builder for Python. Holds IVariantNode instances and connects
// them via IVariantChannel adapters. The variant only lives at the boundary;
// each node's internal Channel<T> stores raw T values.
template<typename Variant>
class PyNode; // forward declaration
template<typename Variant>
class PyNetwork {
public:
using VNode = IVariantNode<Variant>;
using VChannel = IVariantChannel<Variant>;
// ── Builder API ───────────────────────────────────────────────────────────
void add(std::string name, std::shared_ptr<VNode> node) {
if (nodes_.count(name))
throw std::runtime_error("duplicate node name: " + name);
node->set_name(name);
nodes_.emplace(name, std::move(node));
adj_[name];
}
// connect(src_name, out_idx, dst_name, in_idx)
void connect(const std::string& src_name, std::size_t out_idx,
const std::string& dst_name, std::size_t in_idx)
{
auto& src = node_at(src_name);
auto& dst = node_at(dst_name);
if (out_idx >= src.output_count())
throw std::out_of_range(src_name + ": output index " +
std::to_string(out_idx) + " out of range");
if (in_idx >= dst.input_count())
throw std::out_of_range(dst_name + ": input index " +
std::to_string(in_idx) + " out of range");
if (src.output_type(out_idx) != dst.input_type(in_idx))
throw std::runtime_error(
"type mismatch: " + src_name + ".output[" + std::to_string(out_idx) +
"] (" + src.output_type(out_idx).name() + ") → " +
dst_name + ".input[" + std::to_string(in_idx) +
"] (" + dst.input_type(in_idx).name() + ")");
auto ch = dst.input_channel(in_idx);
src.set_output_channel(out_idx, std::move(ch));
adj_[src_name].push_back(dst_name);
}
void build() {
topo_.clear();
std::map<std::string, int> color;
for (auto& [name, _] : nodes_)
if (color[name] == 0) dfs(name, color);
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
void start() {
for (auto& name : topo_)
nodes_.at(name)->start();
}
void stop() {
for (auto it = topo_.rbegin(); it != topo_.rend(); ++it)
nodes_.at(*it)->stop();
}
// ── Python tap/inject ─────────────────────────────────────────────────────
nb::object read(const std::string& node_name, std::size_t out_idx) {
auto key = tap_key(node_name, out_idx);
if (!taps_.count(key)) {
auto& src = node_at(node_name);
if (out_idx >= src.output_count())
throw std::out_of_range(node_name + ": output index out of range");
auto tap = make_tap_channel(src.output_type(out_idx));
src.set_output_channel(out_idx, tap);
taps_[key] = std::move(tap);
}
Variant v;
{
nb::gil_scoped_release release;
v = taps_.at(key)->pop();
}
return variant_to_python(std::move(v));
}
void write(const std::string& node_name, std::size_t in_idx, nb::object value) {
auto& dst = node_at(node_name);
if (in_idx >= dst.input_count())
throw std::out_of_range(node_name + ": input index out of range");
auto ch = dst.input_channel(in_idx);
Variant v = python_to_variant(ch->type_index(), std::move(value));
{
nb::gil_scoped_release release;
ch->push(std::move(v));
}
}
// ── Python-callable node creation ─────────────────────────────────────────
// Creates a PyNode wrapping a Python callable and adds it to the graph.
// Type names must have been registered via register_full_type<T>().
void add_node_python(std::string name, nb::object callable,
std::vector<std::string> in_names,
std::vector<std::string> out_names,
std::size_t capacity = 5)
{
std::vector<std::type_index> in_types, out_types;
for (auto& s : in_names) in_types.push_back(resolve_type_name(s));
for (auto& s : out_names) out_types.push_back(resolve_type_name(s));
add(std::move(name),
std::make_shared<PyNode<Variant>>(
std::move(callable),
std::move(in_types),
std::move(out_types),
to_python_,
from_python_,
ch_factories_,
capacity));
}
// ── Type converter registration ───────────────────────────────────────────
template<typename T>
void register_type(
std::function<nb::object(const T&)> to_py,
std::function<T(nb::object)> from_py)
{
auto idx = std::type_index(typeid(T));
to_python_[idx] = [to_py](const Variant& v) { return to_py(std::get<T>(v)); };
from_python_[idx] = [from_py](nb::object o) -> Variant {
return Variant{ from_py(std::move(o)) };
};
}
// register_channel_factory<T>: registers factory for creating input channels.
template<typename T>
void register_channel_factory() {
ch_factories_[std::type_index(typeid(T))] =
[](std::size_t cap) -> std::shared_ptr<VChannel> {
return std::make_shared<VariantChannel<T, Variant>>(
std::make_shared<Channel<T>>(cap));
};
}
// Backward-compatible alias.
template<typename T>
void register_tap_factory(std::size_t = 5) {
register_channel_factory<T>();
}
// register_full_type<T>: registers converters + channel factory + type name.
// This is what auto_bind.hpp calls; manual bindings can call register_type +
// register_tap_factory separately for backward compatibility.
template<typename T>
void register_full_type(
std::function<nb::object(const T&)> to_py,
std::function<T(nb::object)> from_py,
const char* friendly_name = nullptr)
{
register_type<T>(std::move(to_py), std::move(from_py));
register_channel_factory<T>();
auto idx = std::type_index(typeid(T));
type_names_.insert_or_assign(typeid(T).name(), idx);
if (friendly_name) type_names_.insert_or_assign(friendly_name, idx);
}
// ── Type name lookup ──────────────────────────────────────────────────────
void register_type_name(const std::string& name, std::type_index idx) {
type_names_.insert_or_assign(name, idx);
}
std::type_index resolve_type_name(const std::string& name) const {
auto it = type_names_.find(name);
if (it == type_names_.end())
throw std::runtime_error(
"type '" + name + "' not registered — call register_full_type<T>() first");
return it->second;
}
private:
VNode& node_at(const std::string& name) {
auto it = nodes_.find(name);
if (it == nodes_.end())
throw std::runtime_error("unknown node: " + name);
return *it->second;
}
void dfs(const std::string& name, std::map<std::string, int>& color) {
color[name] = 1;
for (auto& nbr : adj_[name]) {
if (color[nbr] == 1)
throw std::runtime_error("cycle detected in graph");
if (color[nbr] == 0) dfs(nbr, color);
}
color[name] = 2;
topo_.insert(topo_.begin(), name);
}
std::string tap_key(const std::string& node, std::size_t idx) {
return node + ":" + std::to_string(idx);
}
std::shared_ptr<VChannel> make_tap_channel(std::type_index type,
std::size_t cap = 5) {
auto it = ch_factories_.find(type);
if (it == ch_factories_.end())
throw std::runtime_error(
"no channel factory for type: " + std::string(type.name()) +
" — call register_full_type<T>() or register_tap_factory<T>()");
return it->second(cap);
}
nb::object variant_to_python(Variant v) {
auto idx = std::visit([](auto& x) {
return std::type_index(typeid(x));
}, v);
auto it = to_python_.find(idx);
if (it == to_python_.end())
throw std::runtime_error("no to_python converter for type");
return it->second(v);
}
Variant python_to_variant(std::type_index idx, nb::object obj) {
auto it = from_python_.find(idx);
if (it == from_python_.end())
throw std::runtime_error("no from_python converter for type");
return it->second(std::move(obj));
}
std::map<std::string, std::shared_ptr<VNode>> nodes_;
std::map<std::string, std::vector<std::string>> adj_;
std::vector<std::string> topo_;
std::map<std::string, std::shared_ptr<VChannel>> taps_;
std::map<std::type_index, std::function<nb::object(const Variant&)>> to_python_;
std::map<std::type_index, std::function<Variant(nb::object)>> from_python_;
// Channel factory: type → function(capacity) → VChannel.
// Used both for tap channels (read()) and PyNode input channel creation.
std::map<std::type_index,
std::function<std::shared_ptr<VChannel>(std::size_t)>> ch_factories_;
// Friendly name → type_index (e.g. "int" → typeid(int)).
std::map<std::string, std::type_index> type_names_;
};
// ── PyNode<Variant> ───────────────────────────────────────────────────────────
// A pure-Python processing node. Holds a nanobind callable.
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs.
template<typename Variant>
class PyNode : public IVariantNode<Variant> {
public:
using VChannel = IVariantChannel<Variant>;
using ChannelFactory =
std::function<std::shared_ptr<VChannel>(std::size_t capacity)>;
PyNode(nb::object callable,
std::vector<std::type_index> in_types,
std::vector<std::type_index> out_types,
std::map<std::type_index, std::function<nb::object(const Variant&)>> to_py,
std::map<std::type_index, std::function<Variant(nb::object)>> from_py,
std::map<std::type_index, ChannelFactory> ch_factories,
std::size_t capacity = 5)
: callable_(std::move(callable))
, in_types_(std::move(in_types))
, out_types_(std::move(out_types))
, to_python_(std::move(to_py))
, from_python_(std::move(from_py))
, ch_factories_(std::move(ch_factories))
, in_channels_(in_types_.size())
, out_channels_(out_types_.size())
{
for (std::size_t i = 0; i < in_types_.size(); ++i) {
auto it = ch_factories_.find(in_types_[i]);
if (it == ch_factories_.end())
throw std::runtime_error("PyNode: no channel factory for input type");
in_channels_[i] = it->second(capacity);
}
}
// ── INode ─────────────────────────────────────────────────────────────────
void start() override {
for (auto& ch : in_channels_) ch->enable();
stop_flag_.store(false, std::memory_order_relaxed);
thread_ = std::jthread([this](std::stop_token) { run_loop(); });
}
void stop() override {
stop_flag_.store(true, std::memory_order_relaxed);
for (auto& ch : in_channels_) ch->disable();
if (thread_.joinable()) {
thread_.request_stop();
nb::gil_scoped_release release;
thread_.join();
}
}
bool running() const override {
return thread_.joinable() && !stop_flag_.load(std::memory_order_relaxed);
}
void set_name(std::string name) override {
IVariantNode<Variant>::set_name(std::move(name));
}
const NodeStats& stats() const override { return stats_; }
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
uint64_t frames = stats_.frames_processed.load(std::memory_order_relaxed);
double exec_ms = stats_.ema_exec_us.load(std::memory_order_relaxed) / 1000.0;
double blk_ms = stats_.total_blocked_us.load(std::memory_order_relaxed) / 1000.0;
double total_ms = exec_ms + blk_ms;
return { name, frames, exec_ms,
stats_.max_exec_us.load(std::memory_order_relaxed) / 1000.0,
blk_ms, elapsed_s > 0 ? frames / elapsed_s : 0.0,
stats_.total_cpu_us.load(std::memory_order_relaxed) / 1000.0,
total_ms > 0 ? 100.0 * exec_ms / total_ms : 0.0 };
}
// ── IVariantNode ──────────────────────────────────────────────────────────
std::size_t input_count() const override { return in_types_.size(); }
std::size_t output_count() const override { return out_types_.size(); }
std::type_index input_type(std::size_t i) const override { return in_types_[i]; }
std::type_index output_type(std::size_t i) const override { return out_types_[i]; }
std::shared_ptr<VChannel> input_channel(std::size_t i) override {
return in_channels_[i];
}
void set_output_channel(std::size_t i,
std::shared_ptr<VChannel> ch) override {
out_channels_[i] = std::move(ch);
}
private:
void run_loop() {
while (!stop_flag_.load(std::memory_order_relaxed)) {
try {
auto t0 = clock_t::now();
std::vector<Variant> inputs(in_channels_.size());
for (std::size_t i = 0; i < in_channels_.size(); ++i)
inputs[i] = in_channels_[i]->pop();
auto t1 = clock_t::now();
auto cpu0 = NodeStats::cpu_now();
std::vector<Variant> outputs;
{
nb::gil_scoped_acquire acquire;
nb::list py_args;
for (auto& v : inputs)
py_args.append(variant_to_python(v));
nb::object result = callable_(*py_args);
if (out_channels_.size() == 1) {
outputs.push_back(python_to_variant(out_types_[0], result));
} else {
nb::tuple tup = nb::cast<nb::tuple>(result);
for (std::size_t i = 0; i < out_channels_.size(); ++i)
outputs.push_back(python_to_variant(out_types_[i], tup[i]));
}
}
auto cpu1 = NodeStats::cpu_now();
auto t2 = clock_t::now();
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
for (std::size_t i = 0; i < out_channels_.size(); ++i) {
if (out_channels_[i])
out_channels_[i]->push(std::move(outputs[i]));
}
} catch (const ChannelClosedError&) {
break;
} catch (const ChannelOverflowError&) {
// drop and continue
}
}
}
nb::object variant_to_python(const Variant& v) {
auto idx = std::visit([](const auto& x) {
return std::type_index(typeid(x));
}, v);
return to_python_.at(idx)(v);
}
Variant python_to_variant(std::type_index idx, nb::object obj) {
return from_python_.at(idx)(std::move(obj));
}
nb::object callable_;
std::vector<std::type_index> in_types_;
std::vector<std::type_index> out_types_;
std::map<std::type_index, std::function<nb::object(const Variant&)>> to_python_;
std::map<std::type_index, std::function<Variant(nb::object)>> from_python_;
std::map<std::type_index, ChannelFactory> ch_factories_;
std::vector<std::shared_ptr<VChannel>> in_channels_;
std::vector<std::shared_ptr<VChannel>> out_channels_;
std::atomic<bool> stop_flag_{false};
std::jthread thread_;
NodeStats stats_;
};
// ── register_py_network (legacy helper) ───────────────────────────────────────
// Registers PyNetwork<Variant> with the given nanobind module.
// Prefer bind_network<Registry> from auto_bind.hpp for new code.
template<typename Variant>
void register_py_network(nb::module_& m, const char* class_name = "Network") {
using Net = PyNetwork<Variant>;
nb::class_<Net>(m, class_name)
.def(nb::init<>())
.def("connect", &Net::connect,
nb::arg("src"), nb::arg("out_idx"),
nb::arg("dst"), nb::arg("in_idx"))
.def("build", &Net::build)
.def("start", &Net::start)
.def("stop", &Net::stop)
.def("read", &Net::read,
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
.def("write", &Net::write,
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
}
} // namespace kpn::python
#endif // KPN_BUILD_PYTHON
-198
View File
@@ -1,198 +0,0 @@
#pragma once
#include "diagnostics.hpp"
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <thread>
#include <vector>
namespace kpn {
// ── IScheduler ────────────────────────────────────────────────────────────────
struct IScheduler {
virtual ~IScheduler() = default;
// Submit a task with an optional priority in [0, 1]. Higher = run sooner.
virtual void submit(std::function<void()> task, float priority = 0.5f) = 0;
// Start worker threads. Must be called before submit().
virtual void start() = 0;
// Halt: signal workers to exit and join them. Pending tasks are discarded.
virtual void stop() = 0;
// Drain: block until all in-flight tasks complete. Workers keep running.
virtual void drain() = 0;
};
// ── ThreadPool ────────────────────────────────────────────────────────────────
//
// Work-stealing thread pool with per-thread priority queues.
//
// Each worker owns a priority_queue (max-heap by priority, FIFO within equal
// priority via sequence number). submit() distributes via round-robin. When a
// worker's queue is empty it tries to steal from the most-loaded peer using
// try_lock to avoid blocking; if no work is found it sleeps on a shared CV.
//
// total_ counts tasks submitted-but-not-completed (queued + executing).
// drain() waits until total_ == 0.
class ThreadPool : public IScheduler, public IPoolProbe {
public:
explicit ThreadPool(std::size_t thread_count) : thread_count_(thread_count) {}
~ThreadPool() {
if (!stopped_.load(std::memory_order_relaxed))
stop();
}
void start() override {
stopped_.store(false, std::memory_order_relaxed);
queues_.clear();
for (std::size_t i = 0; i < thread_count_; ++i)
queues_.push_back(std::make_unique<WorkerQueue>());
workers_.reserve(thread_count_);
for (std::size_t i = 0; i < thread_count_; ++i)
workers_.emplace_back([this, i] { worker_loop(i); });
}
void stop() override {
stopped_.store(true, std::memory_order_seq_cst);
for (auto& q : queues_) {
std::lock_guard lock(q->mx);
std::size_t discarded = q->pq.size();
while (!q->pq.empty()) q->pq.pop();
total_.fetch_sub(discarded, std::memory_order_relaxed);
}
cv_.notify_all();
for (auto& t : workers_) if (t.joinable()) t.join();
workers_.clear();
queues_.clear();
}
void drain() override {
std::unique_lock lock(drain_mx_);
drain_cv_.wait(lock, [this] {
return total_.load(std::memory_order_acquire) == 0;
});
}
void submit(std::function<void()> task, float priority = 0.5f) override {
std::size_t target = next_.fetch_add(1, std::memory_order_relaxed) % thread_count_;
{
std::lock_guard lock(queues_[target]->mx);
queues_[target]->pq.push(
{std::move(task), priority, seq_.fetch_add(1, std::memory_order_relaxed)});
}
total_.fetch_add(1, std::memory_order_relaxed);
submitted_.fetch_add(1, std::memory_order_relaxed);
cv_.notify_one();
}
std::size_t thread_count() const { return thread_count_; }
// ── IPoolProbe ────────────────────────────────────────────────────────────
PoolSnapshot snapshot(const std::string& name) const override {
std::size_t a = active_.load(std::memory_order_relaxed);
std::size_t t = total_.load(std::memory_order_relaxed);
return {
name, thread_count_,
t > a ? t - a : 0, // queued (approximate)
a, // executing
submitted_.load(std::memory_order_relaxed),
completed_.load(std::memory_order_relaxed),
};
}
private:
struct Task {
std::function<void()> fn;
float priority;
uint64_t seq;
// max-heap: higher priority runs first; older task wins tie
bool operator<(const Task& o) const {
if (priority != o.priority) return priority < o.priority;
return seq > o.seq;
}
};
// Separate cache lines to prevent false sharing between adjacent queues.
struct alignas(64) WorkerQueue {
std::priority_queue<Task> pq;
std::mutex mx;
};
std::optional<std::function<void()>> try_pop(WorkerQueue& q) {
std::lock_guard lock(q.mx);
if (q.pq.empty()) return std::nullopt;
auto fn = std::move(const_cast<Task&>(q.pq.top()).fn);
q.pq.pop();
return fn;
}
std::optional<std::function<void()>> try_steal(std::size_t thief) {
// Find the most-loaded peer without blocking — racy peek is fine.
std::size_t victim = thief, best = 0;
for (std::size_t i = 0; i < queues_.size(); ++i) {
if (i == thief) continue;
std::unique_lock lk(queues_[i]->mx, std::try_to_lock);
if (!lk) continue;
std::size_t n = queues_[i]->pq.size();
if (n > best) { best = n; victim = i; }
}
if (victim == thief) return std::nullopt;
return try_pop(*queues_[victim]);
}
void execute(std::function<void()>& fn) {
active_.fetch_add(1, std::memory_order_relaxed);
fn();
completed_.fetch_add(1, std::memory_order_relaxed);
active_.fetch_sub(1, std::memory_order_relaxed);
// Notify drain() if this was the last in-flight task.
// acq_rel ensures the decrement is visible before any drain() load.
if (total_.fetch_sub(1, std::memory_order_acq_rel) == 1)
drain_cv_.notify_all();
}
void worker_loop(std::size_t id) {
while (true) {
if (auto fn = try_pop(*queues_[id])) { execute(*fn); continue; }
if (auto fn = try_steal(id)) { execute(*fn); continue; }
std::unique_lock lock(cv_mx_);
cv_.wait(lock, [this] {
return stopped_.load(std::memory_order_seq_cst)
|| total_.load(std::memory_order_relaxed) > 0;
});
if (stopped_.load(std::memory_order_seq_cst)
&& total_.load(std::memory_order_relaxed) == 0)
return;
}
}
const std::size_t thread_count_;
std::vector<std::unique_ptr<WorkerQueue>> queues_;
std::vector<std::thread> workers_;
std::mutex cv_mx_;
std::condition_variable cv_;
std::mutex drain_mx_;
std::condition_variable drain_cv_;
std::atomic<bool> stopped_{true};
std::atomic<size_t> total_{0}; // queued + executing
std::atomic<size_t> active_{0}; // executing only (for snapshot)
std::atomic<size_t> next_{0}; // round-robin submit cursor
std::atomic<uint64_t> seq_{0}; // tie-break for equal-priority tasks
std::atomic<uint64_t> submitted_{0};
std::atomic<uint64_t> completed_{0};
};
} // namespace kpn
-190
View File
@@ -1,190 +0,0 @@
#pragma once
#include "diagnostics.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <utility>
#include <vector>
namespace kpn {
template<typename T> class Channel; // forward declaration for acquire_balanced
// ── SharedResource ────────────────────────────────────────────────────────────
//
// Wraps an exclusive resource (e.g. an ONNX session, a CUDA stream) and
// arbitrates concurrent access using a priority-based waiter queue.
//
// When multiple nodes compete, the one with the highest priority score wins
// the next slot. Priority is re-evaluated at release time so it reflects the
// current queue state, not the state when the node first started waiting.
//
// Starvation prevention: each waiter's effective score grows with elapsed wait
// time (aging_per_second), ensuring a low-priority node eventually gets served.
//
// Usage:
// SharedResource<OrtSession> res(session_args...);
//
// // inside a node functor —
// auto guard = res.acquire_balanced(in_channel, out_channel);
// guard->Run(...); // guard releases automatically on scope exit
template<typename T>
class SharedResource : public IResourceProbe {
public:
// ── RAII guard ────────────────────────────────────────────────────────────
class Guard {
SharedResource* owner_;
explicit Guard(SharedResource* o) : owner_(o) {}
friend class SharedResource;
public:
Guard(Guard&& o) noexcept : owner_(std::exchange(o.owner_, nullptr)) {}
Guard& operator=(Guard&&) = delete;
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
~Guard() { if (owner_) owner_->release(); }
T& get() { return owner_->resource_; }
T* operator->() { return &owner_->resource_; }
T& operator*() { return owner_->resource_; }
};
// ── Construction ──────────────────────────────────────────────────────────
template<typename... Args>
explicit SharedResource(Args&&... args)
: resource_(std::forward<Args>(args)...) {}
SharedResource(const SharedResource&) = delete;
SharedResource& operator=(const SharedResource&) = delete;
SharedResource(SharedResource&&) = delete;
SharedResource& operator=(SharedResource&&) = delete;
// ── Acquire ───────────────────────────────────────────────────────────────
// Acquire with a callable that returns a priority in [0, 1].
// Higher = more urgent. Called at every release to pick the best waiter.
template<typename PriorityFn>
Guard acquire(PriorityFn&& fn) {
std::unique_lock lock(mutex_);
if (!held_) {
held_ = true;
acq_.fetch_add(1, std::memory_order_relaxed);
return Guard(this);
}
Waiter w{std::function<float()>(std::forward<PriorityFn>(fn)), clock_t::now()};
waiters_.push_back(&w);
update_peak(waiters_.size());
current_waiters_.store(waiters_.size(), std::memory_order_relaxed);
auto t0 = w.wait_start;
w.cv.wait(lock, [&w] { return w.ready; });
int64_t wait_us = std::chrono::duration_cast<std::chrono::microseconds>(
clock_t::now() - t0).count();
waiters_.erase(std::find(waiters_.begin(), waiters_.end(), &w));
current_waiters_.store(waiters_.size(), std::memory_order_relaxed);
acq_.fetch_add(1, std::memory_order_relaxed);
total_wait_us_.fetch_add(static_cast<uint64_t>(wait_us > 0 ? wait_us : 0),
std::memory_order_relaxed);
return Guard(this);
}
// Acquire with no priority (all waiters treated equally, order is fair-ish).
Guard acquire() {
return acquire([] { return 0.5f; });
}
// Acquire with priority derived from channel fill fractions:
// score = input_fill × output_headroom
// A node with a full input queue and empty output queue has the highest
// urgency — it has work to do and nowhere to stall downstream.
template<typename In, typename Out>
Guard acquire_balanced(const Channel<In>& in_ch, const Channel<Out>& out_ch) {
return acquire([&in_ch, &out_ch] {
float in_fill = in_ch.capacity() ? float(in_ch.size()) / in_ch.capacity() : 0.5f;
float out_head = out_ch.capacity() ? 1.0f - float(out_ch.size()) / out_ch.capacity() : 0.5f;
return in_fill * out_head;
});
}
// ── IResourceProbe ────────────────────────────────────────────────────────
ResourceSnapshot snapshot(const std::string& name) const override {
std::lock_guard lock(mutex_);
uint64_t a = acq_.load(std::memory_order_relaxed);
uint64_t w = total_wait_us_.load(std::memory_order_relaxed);
return {
name,
a,
a > 0 ? double(w) / a / 1000.0 : 0.0,
peak_waiters_.load(std::memory_order_relaxed),
current_waiters_.load(std::memory_order_relaxed),
held_,
};
}
private:
void release() {
std::unique_lock lock(mutex_);
if (waiters_.empty()) {
held_ = false;
return;
}
// Re-evaluate every waiter's current priority and apply aging bonus.
auto now = clock_t::now();
Waiter* best = nullptr;
float best_score = -1.0f;
for (Waiter* w : waiters_) {
float age_s = std::chrono::duration<float>(now - w->wait_start).count();
float score = w->priority_fn() + age_s * kAgingPerSecond;
if (score > best_score) { best_score = score; best = w; }
}
best->ready = true;
best->cv.notify_one();
// held_ stays true — ownership transfers to the woken waiter.
}
void update_peak(std::size_t n) {
uint64_t prev = peak_waiters_.load(std::memory_order_relaxed);
while (n > prev &&
!peak_waiters_.compare_exchange_weak(prev, n,
std::memory_order_relaxed, std::memory_order_relaxed))
;
}
struct Waiter {
std::function<float()> priority_fn;
clock_t::time_point wait_start;
std::condition_variable cv;
bool ready{false};
Waiter(std::function<float()> fn, clock_t::time_point t)
: priority_fn(std::move(fn)), wait_start(t) {}
};
static constexpr float kAgingPerSecond = 0.05f;
T resource_;
bool held_{false};
mutable std::mutex mutex_;
std::vector<Waiter*> waiters_;
std::atomic<uint64_t> acq_{0};
std::atomic<uint64_t> total_wait_us_{0};
std::atomic<uint64_t> peak_waiters_{0};
std::atomic<uint64_t> current_waiters_{0};
};
// ── Factory ───────────────────────────────────────────────────────────────────
template<typename T, typename... Args>
SharedResource<T> make_shared_resource(Args&&... args) {
return SharedResource<T>(std::forward<Args>(args)...);
}
} // namespace kpn
-412
View File
@@ -1,412 +0,0 @@
#pragma once
#include "channel.hpp"
#include "diagnostics.hpp"
#include "fanout.hpp"
#include "inode.hpp"
#include "port.hpp"
#include "tmp/fanout_groups.hpp"
#include "tmp/topo_sort.hpp"
#ifdef KPN_WEB_DEBUG
#include "web_debug.hpp"
#include <memory>
#endif
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace kpn {
// ── Edge descriptor ───────────────────────────────────────────────────────────
//
// Carries compile-time type info and runtime references to the two endpoints.
// Constructed by the edge() factory; consumed by make_network().
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
struct Edge {
using src_node_t = SrcNode;
using dst_node_t = DstNode;
static constexpr std::size_t src_idx = SrcIdx;
static constexpr std::size_t dst_idx = DstIdx;
SrcNode& src;
DstNode& dst;
};
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
auto edge(OutputPort<SrcNode, SrcIdx>, InputPort<DstNode, DstIdx> in)
-> Edge<SrcNode, SrcIdx, DstNode, DstIdx>; // deduction only; defined below
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
Edge<SrcNode, SrcIdx, DstNode, DstIdx>
edge(OutputPort<SrcNode, SrcIdx> out, InputPort<DstNode, DstIdx> in) {
return {out.node, in.node};
}
// ── StaticNetwork ─────────────────────────────────────────────────────────────
// node_label<NodeT>: returns Label NTTP as string_view if present, else empty.
template<typename NodeT, typename = void>
struct node_label_helper {
static constexpr std::string_view value = "";
};
template<typename NodeT>
struct node_label_helper<NodeT, std::void_t<decltype(NodeT::label())>> {
static constexpr std::string_view value = NodeT::label();
};
template<typename NodeT>
inline constexpr std::string_view node_label_v = node_label_helper<NodeT>::value;
// node_display_name<NodeT, UniqueTag>: Label if non-empty, else "node[UniqueTag]"
// Returned as std::string at runtime (called once at StaticNetwork construction).
template<typename NodeT>
std::string node_display_name() {
constexpr std::string_view lbl = node_label_v<NodeT>;
if constexpr (!lbl.empty()) {
return std::string(lbl);
} else if constexpr (requires { NodeT::is_fanout_node; NodeT::unique_tag; }) {
return "fanout[" + std::to_string(NodeT::unique_tag) + "]";
} else if constexpr (requires { NodeT::is_router_node; NodeT::unique_tag; }) {
return "router[" + std::to_string(NodeT::unique_tag) + "]";
} else if constexpr (requires { NodeT::is_filter_node; NodeT::unique_tag; }) {
return "filter[" + std::to_string(NodeT::unique_tag) + "]";
} else if constexpr (requires { NodeT::unique_tag; }) {
return "node[" + std::to_string(NodeT::unique_tag) + "]";
} else {
return "node[?]";
}
}
template<typename FanoutStorage, typename TopoNodeList>
class StaticNetwork : public INode {
public:
// FanoutStorage = std::tuple<FanoutNode<T0,N0>, ...> (owned, heap-allocated)
// TopoNodeList = tmp::TypeList<NodeA, NodeB, ...> sources-first
StaticNetwork(std::unique_ptr<FanoutStorage> fanouts,
std::vector<INode*> user_nodes_topo,
std::vector<INode*> fanout_ptrs,
std::vector<std::string> user_node_names,
std::vector<std::string> fanout_node_names,
std::vector<std::unique_ptr<IChannelProbe>> channel_probes)
: fanouts_(std::move(fanouts))
, user_nodes_topo_(std::move(user_nodes_topo))
, fanout_nodes_ptr_(std::move(fanout_ptrs))
, user_node_names_(std::move(user_node_names))
, fanout_node_names_(std::move(fanout_node_names))
, channel_probes_(std::move(channel_probes))
{}
~StaticNetwork() override { stop(); }
void start() override {
stop_flag_ = false;
start_time_ = clock_t::now();
for (auto* n : user_nodes_topo_) n->start();
for (auto* n : fanout_nodes_ptr_) n->start();
#ifdef KPN_WEB_DEBUG
if (web_server_enabled_) {
web_server_ = std::make_unique<web_debug::WebDebugServer>(
web_debug_port_,
[this]() {
auto s = collect_snapshots();
return web_debug::to_json(s.nodes, s.channels, s.resources, s.elapsed_s, s.pools);
});
web_server_->start();
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
}
#endif
}
void stop() override { halt(); }
void halt() override {
stop_flag_ = true;
#ifdef KPN_WEB_DEBUG
if (web_server_) web_server_->stop();
#endif
for (auto it = fanout_nodes_ptr_.rbegin(); it != fanout_nodes_ptr_.rend(); ++it)
(*it)->stop();
for (auto it = user_nodes_topo_.rbegin(); it != user_nodes_topo_.rend(); ++it)
(*it)->stop();
}
// shutdown(): graceful drain in topological order (sources first).
// Stops source nodes, polls channels until empty, then stops each downstream layer.
void shutdown() override {
stop_flag_ = true;
#ifdef KPN_WEB_DEBUG
if (web_server_) web_server_->stop();
#endif
// user_nodes_topo_ is already in sources-first order.
// Stop each node and drain its output channels before moving on.
for (auto* n : user_nodes_topo_) {
n->stop();
drain_all_channels();
}
for (auto* n : fanout_nodes_ptr_) n->stop();
}
bool running() const override { return !stop_flag_; }
void set_name(std::string name) override { name_ = std::move(name); }
const NodeStats& stats() const override { static NodeStats dummy; return dummy; }
NodeSnapshot node_snapshot(const std::string& n, double) const override {
return {n, 0, 0, 0, 0, 0, 0, 0};
}
#ifdef KPN_WEB_DEBUG
void set_web_debug_port(uint16_t port) { web_debug_port_ = port; }
// Called by DebugHub::register_network() so the hub owns the debug server.
void disable_web_server() { web_server_enabled_ = false; }
#endif
// Returns a snapshot of this network's nodes and channels for the DebugHub.
NetworkSnapshot network_snapshot() const {
auto s = collect_snapshots();
return {"", std::move(s.nodes), std::move(s.channels), s.elapsed_s};
}
// Register a shared resource so it appears in diagnostics and the debug UI.
// The probe must outlive this network (typically the resource is on the same stack).
void register_resource(const std::string& name, IResourceProbe* probe) {
resource_probes_.emplace_back(name, probe);
}
// Register a thread pool so it appears in diagnostics and the debug UI.
// The probe must outlive this network (typically the pool is on the same stack/shared_ptr).
void register_pool(const std::string& name, IPoolProbe* probe) {
pool_probes_.emplace_back(name, probe);
}
// Print diagnostics using compile-time node labels
void print_diagnostics(std::ostream& os = std::cerr) const {
os << "\n┌─ KPN++ StaticNetwork diagnostics ─────────────────────────────\n";
for (std::size_t i = 0; i < user_nodes_topo_.size(); ++i) {
auto snap = user_nodes_topo_[i]->node_snapshot(user_node_names_[i], 0.0);
os << "" << snap.name
<< " frames=" << snap.frames_processed
<< " ema=" << snap.ema_exec_ms << "ms\n";
}
os << "└────────────────────────────────────────────────────────────────\n";
}
FanoutStorage& fanouts_storage() { return *fanouts_; }
private:
struct Snapshots {
std::vector<NodeSnapshot> nodes;
std::vector<ChannelSnapshot> channels;
std::vector<ResourceSnapshot> resources;
std::vector<PoolSnapshot> pools;
double elapsed_s;
};
Snapshots collect_snapshots() const {
double elapsed_s = std::chrono::duration<double>(
clock_t::now() - start_time_).count();
std::vector<NodeSnapshot> nodes;
for (std::size_t i = 0; i < user_nodes_topo_.size(); ++i)
nodes.push_back(user_nodes_topo_[i]->node_snapshot(user_node_names_[i], elapsed_s));
for (std::size_t i = 0; i < fanout_nodes_ptr_.size(); ++i)
nodes.push_back(fanout_nodes_ptr_[i]->node_snapshot(fanout_node_names_[i], elapsed_s));
std::vector<ChannelSnapshot> channels;
for (auto& probe : channel_probes_)
channels.push_back(probe->snapshot());
std::vector<ResourceSnapshot> resources;
for (auto& [name, probe] : resource_probes_)
resources.push_back(probe->snapshot(name));
std::vector<PoolSnapshot> pools;
for (auto& [name, probe] : pool_probes_)
pools.push_back(probe->snapshot(name));
return {std::move(nodes), std::move(channels), std::move(resources), std::move(pools), elapsed_s};
}
void drain_all_channels() const {
bool any_full = true;
while (any_full) {
any_full = false;
for (auto& probe : channel_probes_) {
if (probe->snapshot().current_fill > 0) { any_full = true; break; }
}
if (any_full)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
std::string name_;
bool stop_flag_{false};
std::unique_ptr<FanoutStorage> fanouts_;
std::vector<INode*> user_nodes_topo_;
std::vector<INode*> fanout_nodes_ptr_;
std::vector<std::string> user_node_names_;
std::vector<std::string> fanout_node_names_;
std::vector<std::unique_ptr<IChannelProbe>> channel_probes_;
std::vector<std::pair<std::string, IResourceProbe*>> resource_probes_;
std::vector<std::pair<std::string, IPoolProbe*>> pool_probes_;
clock_t::time_point start_time_;
#ifdef KPN_WEB_DEBUG
uint16_t web_debug_port_{9090};
bool web_server_enabled_{true};
std::unique_ptr<web_debug::WebDebugServer> web_server_;
#endif
};
// ── make_network ──────────────────────────────────────────────────────────────
template<typename... Edges>
auto make_network(Edges&&... edges) {
// 1. Expand edges — detect fan-outs, splice FanoutNodes
using FanoutSto = tmp::fanout_storage_t<std::decay_t<Edges>...>;
using ExpandedEdges = tmp::expanded_edges_t<std::decay_t<Edges>...>;
// 2. Duplicate-tag check — fires before cycle check for a cleaner error message
using UserNodes = typename tmp::all_node_types<tmp::TypeList<std::decay_t<Edges>...>>::type;
static_assert(!tmp::has_duplicate_tags_v<std::decay_t<Edges>...>,
"make_network: two nodes have the same (Func, UniqueTag) — they are "
"indistinguishable as graph vertices. Add a UniqueTag: "
"make_node<func, \"label\", 1>(capacity)");
// 3. Cycle check
using Topo = tmp::topo_sort<ExpandedEdges>;
constexpr bool has_cycle = Topo::has_cycle;
static_assert(!has_cycle,
"make_network: graph contains a directed cycle");
// 4. Construct owned fanout storage on the heap (FanoutNode has jthread — not moveable)
auto fanout_storage = std::make_unique<FanoutSto>();
// 5. Collect unique user node pointers + their display names, in edge-declaration order
std::vector<INode*> user_node_ptrs;
std::vector<std::string> user_node_names;
auto collect = [&](auto& e) {
using SrcT = std::decay_t<decltype(e.src)>;
using DstT = std::decay_t<decltype(e.dst)>;
auto* s = static_cast<INode*>(&e.src);
auto* d = static_cast<INode*>(&e.dst);
if (std::find(user_node_ptrs.begin(), user_node_ptrs.end(), s) == user_node_ptrs.end()) {
auto sname = node_display_name<SrcT>();
user_node_ptrs.push_back(s);
user_node_names.push_back(sname);
s->set_name(sname);
}
if (std::find(user_node_ptrs.begin(), user_node_ptrs.end(), d) == user_node_ptrs.end()) {
auto dname = node_display_name<DstT>();
user_node_ptrs.push_back(d);
user_node_names.push_back(dname);
d->set_name(dname);
}
};
(collect(edges), ...);
// 5. Wire all expanded SimpleEdges.
// find_node<NodeT>: searches fanout storage then user edge pack, returns NodeT*.
// Uses if constexpr in a fold so mismatched types never reach assignment.
auto find_node = [&]<typename NodeT>() -> NodeT* {
NodeT* ptr = nullptr;
std::apply([&](auto&... fn) {
([&](auto& node) {
if constexpr (std::is_same_v<std::decay_t<decltype(node)>, NodeT>)
if (!ptr) ptr = &node;
}(fn), ...);
}, *fanout_storage);
if (!ptr) {
([&](auto& e) {
if (!ptr) {
if constexpr (std::is_same_v<std::decay_t<decltype(e.src)>, NodeT>)
ptr = &e.src;
else if constexpr (std::is_same_v<std::decay_t<decltype(e.dst)>, NodeT>)
ptr = &e.dst;
}
}(edges), ...);
}
return ptr;
};
// Pre-pass: build fanout_id → source display name map so fanout nodes
// can be named after the node feeding them (e.g. "capture_fanout").
std::map<std::size_t, std::string> fanout_src_name;
[&]<typename... SEs>(tmp::TypeList<SEs...>) {
([&]<typename SE>(SE) {
using DstNode = typename SE::dst_node_t;
using SrcNode = typename SE::src_node_t;
if constexpr (requires { DstNode::is_fanout_node; })
fanout_src_name.emplace(DstNode::unique_tag, node_display_name<SrcNode>());
}(SEs{}), ...);
}(ExpandedEdges{});
// Helper: display name for any node type, resolving fanouts to "src_fanout".
auto node_name = [&]<typename NodeT>() -> std::string {
if constexpr (requires { NodeT::is_fanout_node; }) {
auto it = fanout_src_name.find(NodeT::unique_tag);
return it != fanout_src_name.end() ? it->second + "_fanout"
: node_display_name<NodeT>();
} else {
return node_display_name<NodeT>();
}
};
std::vector<std::unique_ptr<IChannelProbe>> channel_probes;
auto wire_one = [&]<typename SE>(SE) {
using SrcNode = typename SE::src_node_t;
using DstNode = typename SE::dst_node_t;
using out_t = std::tuple_element_t<SE::src_idx, typename SrcNode::return_tuple>;
constexpr std::size_t SrcIdx = SE::src_idx;
constexpr std::size_t DstIdx = SE::dst_idx;
auto* src = find_node.template operator()<SrcNode>();
auto* dst = find_node.template operator()<DstNode>();
if (src && dst) {
auto& ch = dst->template input_channel<DstIdx>();
src->template set_output_channel<SrcIdx>(&ch);
std::string ch_name = node_name.template operator()<SrcNode>() + ":" + std::to_string(SrcIdx)
+ " \xe2\x86\x92 " // UTF-8 →
+ node_name.template operator()<DstNode>() + ":" + std::to_string(DstIdx);
channel_probes.push_back(std::make_unique<ChannelProbe<out_t>>(ch, ch_name));
}
};
[&]<typename... SEs>(tmp::TypeList<SEs...>) {
(wire_one(SEs{}), ...);
}(ExpandedEdges{});
// 6. Collect fanout node pointers and names
std::vector<INode*> fanout_ptrs;
std::vector<std::string> fanout_node_names;
std::apply([&](auto&... fn) {
([&](auto& node) {
using NodeT = std::decay_t<decltype(node)>;
auto fname = node_name.template operator()<NodeT>();
static_cast<INode&>(node).set_name(fname);
fanout_ptrs.push_back(static_cast<INode*>(&node));
fanout_node_names.push_back(fname);
}(fn), ...);
}, *fanout_storage);
// 7. Construct and return the StaticNetwork
using Net = StaticNetwork<FanoutSto, typename Topo::topo>;
return Net(std::move(fanout_storage),
std::move(user_node_ptrs),
std::move(fanout_ptrs),
std::move(user_node_names),
std::move(fanout_node_names),
std::move(channel_probes));
}
} // namespace kpn
-357
View File
@@ -1,357 +0,0 @@
#pragma once
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>
// Metafunctions that scan a pack of Edge<> types, group edges by source port,
// auto-insert FanoutNode<T,N> where N>1, and produce an expanded edge list
// plus a tuple type for the owned fanout nodes.
//
// Key types produced:
// expanded_edges_t<Edges...> — type list of SimpleEdge after fanout insertion
// fanout_storage_t<Edges...> — std::tuple<FanoutNode<T0,N0>, ...> to own
namespace kpn::tmp {
// ── Type list ─────────────────────────────────────────────────────────────────
template<typename... Ts>
struct TypeList {};
template<typename List, typename T>
struct append;
template<typename... Ts, typename T>
struct append<TypeList<Ts...>, T> { using type = TypeList<Ts..., T>; };
template<typename List, typename T>
using append_t = typename append<List, T>::type;
template<typename A, typename B>
struct concat;
template<typename... As, typename... Bs>
struct concat<TypeList<As...>, TypeList<Bs...>> { using type = TypeList<As..., Bs...>; };
template<typename A, typename B>
using concat_t = typename concat<A, B>::type;
// ── Source port identity (used as compile-time map key) ───────────────────────
// Two edges share a source port when their SrcNode type AND SrcIdx are identical.
template<typename SrcNode, std::size_t SrcIdx>
struct SrcPort {};
template<typename Edge>
using src_port_of = SrcPort<typename Edge::src_node_t, Edge::src_idx>;
// ── Count occurrences of a SrcPort in an edge list ───────────────────────────
template<typename Port, typename EdgeList>
struct count_port;
template<typename Port>
struct count_port<Port, TypeList<>> : std::integral_constant<std::size_t, 0> {};
template<typename Port, typename Head, typename... Tail>
struct count_port<Port, TypeList<Head, Tail...>>
: std::integral_constant<std::size_t,
(std::is_same_v<Port, src_port_of<Head>> ? 1 : 0)
+ count_port<Port, TypeList<Tail...>>::value> {};
// ── Collect all destination (DstNode&, DstIdx) for a given SrcPort ────────────
// A destination descriptor — just type tags, no references (references go in
// the runtime SimpleEdge structs produced after wiring).
template<typename DstNode, std::size_t DstIdx>
struct DstDesc {};
template<typename Port, typename EdgeList>
struct collect_dsts;
template<typename Port>
struct collect_dsts<Port, TypeList<>> { using type = TypeList<>; };
template<typename Port, typename Head, typename... Tail>
struct collect_dsts<Port, TypeList<Head, Tail...>> {
using rest = typename collect_dsts<Port, TypeList<Tail...>>::type;
using type = std::conditional_t<
std::is_same_v<Port, src_port_of<Head>>,
append_t<rest, DstDesc<typename Head::dst_node_t, Head::dst_idx>>,
rest>;
};
// ── SimpleEdge: a resolved edge after fanout expansion ────────────────────────
//
// At runtime, StaticNetwork wires edges using SimpleEdge descriptors.
// Each SimpleEdge is just a pair of (SrcNode&, SrcIdx, DstNode&, DstIdx) stored
// as a type — the actual references come from the node tuple at wire time.
template<typename SrcNode, std::size_t SrcIdx,
typename DstNode, std::size_t DstIdx>
struct SimpleEdge {
using src_node_t = SrcNode;
using dst_node_t = DstNode;
static constexpr std::size_t src_idx = SrcIdx;
static constexpr std::size_t dst_idx = DstIdx;
};
// ── FanoutPlaceholder: a fanout node that will be owned by StaticNetwork ───────
template<typename T, std::size_t N, std::size_t FanoutId>
struct FanoutPlaceholder {
using value_type = T;
static constexpr std::size_t fan_n = N;
static constexpr std::size_t fan_id = FanoutId;
};
// ── expand_edge: for one original edge, produce the replacement SimpleEdge(s) ──
//
// If the source port has N>1 consumers: the edge from src→fanout and the
// fanout→dst edges are synthesised elsewhere (see expand_all). Here we only
// need to emit the fanout→dst edge for this particular destination.
//
// For N==1 edges we emit the edge unchanged.
//
// This is called after the fanout node type has already been determined.
} // namespace kpn::tmp
namespace kpn {
// Forward declaration — FanoutNode is defined in fanout.hpp.
template<typename T, std::size_t N, std::size_t Id> class FanoutNode;
} // namespace kpn
namespace kpn::tmp {
// ── Master expansion: iterate edges, build expanded list + storage tuple ───────
//
// Strategy:
// 1. First pass: for each unique SrcPort with N>1, record a FanoutPlaceholder.
// 2. Second pass: rewrite each edge.
// - N==1 edges become a single SimpleEdge unchanged.
// - N>1 edges: on first encounter emit src→fanout SimpleEdge + N fanout→dst
// SimpleEdges; on subsequent encounters for the same src port emit nothing
// (already handled).
//
// To implement "first encounter" tracking we carry a list of already-processed
// SrcPorts through the fold.
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, // TypeList<FanoutPlaceholder<...>>
typename EdgeList, // TypeList<SimpleEdge<...>> — accumulated output
typename RemainingEdges> // TypeList<original edges> still to process
struct expand_impl;
// Base case — no more edges
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList>
struct expand_impl<ProcessedPorts, NextFanoutId, FanoutList, EdgeList, TypeList<>> {
using fanouts = FanoutList;
using edges = EdgeList;
};
// Helper: is Port in ProcessedPorts?
template<typename Port, typename Processed>
struct already_processed : std::false_type {};
template<typename Port, typename Head, typename... Tail>
struct already_processed<Port, TypeList<Head, Tail...>>
: std::conditional_t<std::is_same_v<Port, Head>,
std::true_type,
already_processed<Port, TypeList<Tail...>>> {};
// Helper: given DstDesc list + FanoutPlaceholder id, produce SimpleEdge list
// FanoutNode<T,N>::output<I> → DstNode::input<DstIdx>
template<std::size_t FanoutId, typename T, std::size_t N,
typename DstDescList, std::size_t I = 0>
struct fanout_to_dst_edges;
template<std::size_t FanoutId, typename T, std::size_t N, std::size_t I>
struct fanout_to_dst_edges<FanoutId, T, N, TypeList<>, I> {
using type = TypeList<>;
};
template<std::size_t FanoutId, typename T, std::size_t N,
typename DstNodeT, std::size_t DstI, typename... DstTail, std::size_t I>
struct fanout_to_dst_edges<FanoutId, T, N,
TypeList<DstDesc<DstNodeT, DstI>, DstTail...>, I> {
using head_edge = SimpleEdge<kpn::FanoutNode<T, N, FanoutId>, I, DstNodeT, DstI>;
using rest = typename fanout_to_dst_edges<FanoutId, T, N,
TypeList<DstTail...>, I+1>::type;
using type = append_t<rest, head_edge>;
};
// Recursive case — process head edge
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList,
typename Head, typename... Tail>
struct expand_impl<ProcessedPorts, NextFanoutId, FanoutList, EdgeList,
TypeList<Head, Tail...>> {
using AllEdges = TypeList<Head, Tail...>;
using Port = src_port_of<Head>;
using SrcNode = typename Head::src_node_t;
using T = std::tuple_element_t<Head::src_idx,
typename SrcNode::return_tuple>;
static constexpr std::size_t N = count_port<Port, AllEdges>::value
+ count_port<Port, EdgeList>::value
// recount against full original list approximation:
// simpler: recount in remaining + already done
;
// Recount properly against the complete original edge list is not possible here
// without passing it along. Instead we pre-compute N before entering the fold.
// See expand_all below which pre-computes per-port counts.
//
// This struct is not used directly — expand_all drives the logic with pre-computed N.
};
// ── expand_all: top-level entry point ─────────────────────────────────────────
//
// Pre-computes per-source-port counts, then runs a fold that processes edges
// one by one.
// Port count map entry
template<typename Port, std::size_t Count>
struct PortCount {};
// Build port count list from full edge list
template<typename AllEdges, typename UniquePortsSeen>
struct build_port_counts;
template<typename AllEdges>
struct build_port_counts<AllEdges, TypeList<>> {
using type = TypeList<>;
};
template<typename AllEdges, typename HeadPort, typename... TailPorts>
struct build_port_counts<AllEdges, TypeList<HeadPort, TailPorts...>> {
static constexpr std::size_t cnt = count_port<HeadPort, AllEdges>::value;
using rest = typename build_port_counts<AllEdges, TypeList<TailPorts...>>::type;
using type = append_t<rest, PortCount<HeadPort, cnt>>;
};
// Collect unique source ports from edge list
template<typename EdgeList, typename SeenSoFar = TypeList<>>
struct unique_src_ports;
template<typename SeenSoFar>
struct unique_src_ports<TypeList<>, SeenSoFar> { using type = SeenSoFar; };
template<typename Head, typename... Tail, typename SeenSoFar>
struct unique_src_ports<TypeList<Head, Tail...>, SeenSoFar> {
using Port = src_port_of<Head>;
using next_seen = std::conditional_t<
already_processed<Port, SeenSoFar>::value,
SeenSoFar,
append_t<SeenSoFar, Port>>;
using type = typename unique_src_ports<TypeList<Tail...>, next_seen>::type;
};
// Look up count for a port
template<typename Port, typename CountList>
struct lookup_count : std::integral_constant<std::size_t, 1> {};
template<typename Port, std::size_t N, typename... Rest>
struct lookup_count<Port, TypeList<PortCount<Port, N>, Rest...>>
: std::integral_constant<std::size_t, N> {};
template<typename Port, typename Head, typename... Rest>
struct lookup_count<Port, TypeList<Head, Rest...>>
: lookup_count<Port, TypeList<Rest...>> {};
// Fold state for the wiring pass
template<typename ProcessedPorts, std::size_t NextFanoutId,
typename FanoutList, typename EdgeList>
struct FoldState {
using processed = ProcessedPorts;
static constexpr std::size_t next_id = NextFanoutId;
using fanouts = FanoutList;
using edges = EdgeList;
};
// Process one edge given pre-computed port counts
template<typename State, typename Edge, typename CountList, typename AllEdges>
struct process_edge {
using Port = src_port_of<Edge>;
using SrcNode = typename Edge::src_node_t;
using T = std::tuple_element_t<Edge::src_idx, typename SrcNode::return_tuple>;
static constexpr std::size_t N = lookup_count<Port, CountList>::value;
// N==1: pass through unchanged
using passthrough_edges = append_t<typename State::edges,
SimpleEdge<SrcNode, Edge::src_idx,
typename Edge::dst_node_t, Edge::dst_idx>>;
// N>1, first encounter: emit src→fanout + all fanout→dst edges
using DstDescs = typename collect_dsts<Port, AllEdges>::type;
static constexpr std::size_t fid = State::next_id;
using fanout_type = FanoutPlaceholder<T, N, fid>;
using src_to_fan = SimpleEdge<SrcNode, Edge::src_idx, FanoutNode<T, N, fid>, 0>;
using fan_to_dsts = typename fanout_to_dst_edges<fid, T, N, DstDescs>::type;
using fanout_edges = concat_t<append_t<typename State::edges, src_to_fan>, fan_to_dsts>;
using new_fanouts = append_t<typename State::fanouts, fanout_type>;
static constexpr bool seen = already_processed<Port, typename State::processed>::value;
using type = std::conditional_t<
(N == 1),
FoldState<typename State::processed, State::next_id,
typename State::fanouts, passthrough_edges>,
std::conditional_t<
!seen,
FoldState<append_t<typename State::processed, Port>,
State::next_id + 1,
new_fanouts, fanout_edges>,
// already processed — skip (fanout edges already emitted)
State>>;
};
// Fold over all edges
template<typename State, typename EdgeList, typename CountList, typename AllEdges>
struct fold_edges;
template<typename State, typename CountList, typename AllEdges>
struct fold_edges<State, TypeList<>, CountList, AllEdges> { using type = State; };
template<typename State, typename Head, typename... Tail,
typename CountList, typename AllEdges>
struct fold_edges<State, TypeList<Head, Tail...>, CountList, AllEdges> {
using next = typename process_edge<State, Head, CountList, AllEdges>::type;
using type = typename fold_edges<next, TypeList<Tail...>, CountList, AllEdges>::type;
};
// ── Public interface ───────────────────────────────────────────────────────────
template<typename... Edges>
struct expand_all {
using AllEdges = TypeList<Edges...>;
using UniquePorts = typename unique_src_ports<AllEdges>::type;
using CountList = typename build_port_counts<AllEdges, UniquePorts>::type;
using InitState = FoldState<TypeList<>, 0, TypeList<>, TypeList<>>;
using FinalState = typename fold_edges<InitState, AllEdges, CountList, AllEdges>::type;
using fanout_placeholders = typename FinalState::fanouts; // TypeList<FanoutPlaceholder<...>>
using expanded_edges = typename FinalState::edges; // TypeList<SimpleEdge<...>>
};
// Convert TypeList<FanoutPlaceholder<T,N,Id>...> to std::tuple<FanoutNode<T,N>...>
template<typename PlaceholderList>
struct to_fanout_tuple;
template<>
struct to_fanout_tuple<TypeList<>> { using type = std::tuple<>; };
template<typename T, std::size_t N, std::size_t Id, typename... Rest>
struct to_fanout_tuple<TypeList<FanoutPlaceholder<T, N, Id>, Rest...>> {
using rest = typename to_fanout_tuple<TypeList<Rest...>>::type;
template<typename Tuple> struct prepend;
template<typename... Ts> struct prepend<std::tuple<Ts...>> {
using type = std::tuple<kpn::FanoutNode<T, N, Id>, Ts...>;
};
using type = typename prepend<rest>::type;
};
template<typename... Edges>
using fanout_storage_t = typename to_fanout_tuple<
typename expand_all<Edges...>::fanout_placeholders>::type;
template<typename... Edges>
using expanded_edges_t = typename expand_all<Edges...>::expanded_edges;
} // namespace kpn::tmp
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include <cstddef>
#include <tuple>
#include <utility>
namespace kpn::tmp {
// repeat_tuple_t<T, N> — std::tuple<T, T, ..., T> with N elements.
// Used by FanoutNode and fanout_groups to express N homogeneous outputs.
template<typename T, std::size_t N, typename Seq = std::make_index_sequence<N>>
struct repeat_tuple;
template<typename T, std::size_t N, std::size_t... Is>
struct repeat_tuple<T, N, std::index_sequence<Is...>> {
template<std::size_t> using always_T = T;
using type = std::tuple<always_T<Is>...>;
};
template<typename T, std::size_t N>
using repeat_tuple_t = typename repeat_tuple<T, N>::type;
} // namespace kpn::tmp
-223
View File
@@ -1,223 +0,0 @@
#pragma once
#include "fanout_groups.hpp"
#include <cstddef>
#include <tuple>
#include <type_traits>
// Compile-time topological sort + cycle detection over a TypeList<SimpleEdge<...>>.
//
// Nodes are identified by type (not by index), since two different node types
// are always distinct vertices. The sort produces a TypeList of node types in
// topological order (sources first).
//
// cycle detection: if DFS revisits a node currently on the stack → static_assert.
namespace kpn::tmp {
// ── Collect all unique node types from an expanded edge list ──────────────────
template<typename EdgeList, typename Seen = TypeList<>>
struct all_node_types;
template<typename Seen>
struct all_node_types<TypeList<>, Seen> { using type = Seen; };
template<typename Head, typename... Tail, typename Seen>
struct all_node_types<TypeList<Head, Tail...>, Seen> {
using S1 = std::conditional_t<
already_processed<typename Head::src_node_t, Seen>::value,
Seen, append_t<Seen, typename Head::src_node_t>>;
using S2 = std::conditional_t<
already_processed<typename Head::dst_node_t, S1>::value,
S1, append_t<S1, typename Head::dst_node_t>>;
using type = typename all_node_types<TypeList<Tail...>, S2>::type;
};
// ── Collect successors (dst node types) of a given src node type ──────────────
template<typename NodeType, typename EdgeList>
struct successors;
template<typename NodeType>
struct successors<NodeType, TypeList<>> { using type = TypeList<>; };
template<typename NodeType, typename Head, typename... Tail>
struct successors<NodeType, TypeList<Head, Tail...>> {
using rest = typename successors<NodeType, TypeList<Tail...>>::type;
using type = std::conditional_t<
std::is_same_v<typename Head::src_node_t, NodeType>,
std::conditional_t<
already_processed<typename Head::dst_node_t, rest>::value,
rest,
append_t<rest, typename Head::dst_node_t>>,
rest>;
};
// ── DFS state ─────────────────────────────────────────────────────────────────
// Color: 0=white(unseen), 1=grey(on stack), 2=black(done)
// We track grey nodes as a TypeList to detect back-edges.
template<typename Node, typename GreyList>
struct is_grey : already_processed<Node, GreyList> {};
// DFS result: has_cycle flag + topo order (black nodes appended post-visit)
template<bool Cycle, typename TopoList>
struct DfsResult { static constexpr bool has_cycle = Cycle; using topo = TopoList; };
template<typename Node, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList>
struct dfs_node;
// Iterate successors
template<typename SuccList, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_successors;
template<typename EdgeList, typename GreyList, typename BlackList,
typename TopoList, bool CycleSoFar>
struct dfs_successors<TypeList<>, EdgeList, GreyList, BlackList, TopoList, CycleSoFar> {
static constexpr bool has_cycle = CycleSoFar;
using black = BlackList;
using topo = TopoList;
};
template<typename Head, typename... Tail, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_successors<TypeList<Head, Tail...>, EdgeList,
GreyList, BlackList, TopoList, CycleSoFar> {
// Visit Head
using visit = dfs_node<Head, EdgeList, GreyList, BlackList, TopoList>;
static constexpr bool cycle1 = CycleSoFar || visit::has_cycle;
// Continue with remaining successors using updated black/topo
using rest = dfs_successors<TypeList<Tail...>, EdgeList,
GreyList, typename visit::black,
typename visit::topo, cycle1>;
static constexpr bool has_cycle = rest::has_cycle;
using black = typename rest::black;
using topo = typename rest::topo;
};
template<typename Node, typename EdgeList,
typename GreyList, typename BlackList, typename TopoList>
struct dfs_node {
// Already black — skip
static constexpr bool already_done = already_processed<Node, BlackList>::value;
// On stack — cycle
static constexpr bool on_stack = is_grey<Node, GreyList>::value;
// Visit successors (only if not already done / on stack)
using succs = typename successors<Node, EdgeList>::type;
using new_grey = append_t<GreyList, Node>;
using visit_succs = dfs_successors<succs, EdgeList, new_grey, BlackList, TopoList,
on_stack>;
static constexpr bool has_cycle = already_done ? false :
on_stack ? true :
visit_succs::has_cycle;
// Append node to topo after all successors (post-order = reverse topo)
using topo_after = std::conditional_t<
already_done || on_stack,
TopoList,
append_t<typename visit_succs::topo, Node>>;
using black = std::conditional_t<
already_done || on_stack,
BlackList,
append_t<typename visit_succs::black, Node>>;
using topo = topo_after;
};
// ── Top-level DFS over all nodes ──────────────────────────────────────────────
template<typename NodeList, typename EdgeList,
typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all;
template<typename EdgeList, typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all<TypeList<>, EdgeList, BlackList, TopoList, CycleSoFar> {
static constexpr bool has_cycle = CycleSoFar;
using topo = TopoList;
};
template<typename Head, typename... Tail, typename EdgeList,
typename BlackList, typename TopoList, bool CycleSoFar>
struct dfs_all<TypeList<Head, Tail...>, EdgeList, BlackList, TopoList, CycleSoFar> {
using visit = dfs_node<Head, EdgeList, TypeList<>, BlackList, TopoList>;
static constexpr bool cycle1 = CycleSoFar || visit::has_cycle;
using rest = dfs_all<TypeList<Tail...>, EdgeList,
typename visit::black, typename visit::topo, cycle1>;
static constexpr bool has_cycle = rest::has_cycle;
using topo = typename rest::topo;
};
// ── Public interface ───────────────────────────────────────────────────────────
// topo_sort<EdgeList>:
// ::topo — TypeList of node types, sources first (start order)
// ::has_cycle — true if a cycle was detected
template<typename EdgeList>
struct topo_sort {
using Nodes = typename all_node_types<EdgeList>::type;
using result = dfs_all<Nodes, EdgeList, TypeList<>, TypeList<>, false>;
// DFS post-order gives reverse topo; reverse the list for sources-first order.
// Reversing a TypeList:
template<typename List, typename Acc = TypeList<>>
struct reverse_list;
template<typename Acc>
struct reverse_list<TypeList<>, Acc> { using type = Acc; };
template<typename H, typename... T, typename Acc>
struct reverse_list<TypeList<H, T...>, Acc>
: reverse_list<TypeList<T...>, append_t<Acc, H>> {}; // wrong direction intentionally
// Post-order appends children before parent, so the list is already
// reverse-topo (sinks first). Reverse it to get sources first.
template<typename List, typename Acc = TypeList<>>
struct rev;
template<typename Acc>
struct rev<TypeList<>, Acc> { using type = Acc; };
template<typename H, typename... T, typename Acc>
struct rev<TypeList<H, T...>, Acc> : rev<TypeList<T...>, TypeList<H, Acc>> {
// prepend H to Acc: TypeList<H, Acc...>
};
// Simpler: just reverse by prepending
template<typename L, typename A = TypeList<>>
struct rev2 { using type = A; };
template<typename H, typename... T, typename... As>
struct rev2<TypeList<H, T...>, TypeList<As...>>
: rev2<TypeList<T...>, TypeList<H, As...>> {};
static constexpr bool has_cycle = result::has_cycle;
using topo = typename rev2<typename result::topo>::type;
};
// ── Duplicate-tag detection ───────────────────────────────────────────────────
//
// Two user nodes share a (Func, UniqueTag) pair iff they have the same type.
// has_duplicate_tags_v<Edges...> is true if any two user node types are identical.
template<typename T, typename List>
struct type_in_list : std::false_type {};
template<typename T, typename H, typename... Tail>
struct type_in_list<T, TypeList<H, Tail...>>
: std::conditional_t<std::is_same_v<T, H>,
std::true_type,
type_in_list<T, TypeList<Tail...>>> {};
template<typename NodeList>
struct has_duplicate_node_types : std::false_type {};
template<typename H, typename... Tail>
struct has_duplicate_node_types<TypeList<H, Tail...>>
: std::conditional_t<type_in_list<H, TypeList<Tail...>>::value,
std::true_type,
has_duplicate_node_types<TypeList<Tail...>>> {};
template<typename... Edges>
inline constexpr bool has_duplicate_tags_v =
has_duplicate_node_types<
typename all_node_types<TypeList<Edges...>>::type>::value;
} // namespace kpn::tmp

Some files were not shown because too many files have changed in this diff Show More