Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee99ab2cc | ||
|
|
904f80af6e | ||
|
|
b278824b6b |
@@ -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
@@ -1,27 +0,0 @@
|
||||
# Build output
|
||||
build/
|
||||
|
||||
# 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
|
||||
@@ -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>
|
||||
@@ -1,71 +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()
|
||||
|
||||
# ── 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()
|
||||
@@ -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
|
||||
@@ -1,257 +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;
|
||||
|
||||
// Single input, single output
|
||||
int double_it(int x) { return x * 2; }
|
||||
|
||||
// Multi-output — must return std::tuple
|
||||
std::tuple<cv::Mat, cv::Mat> split(cv::Mat frame) { ... }
|
||||
|
||||
// Sink — void return, no output ports
|
||||
void display(cv::Mat frame) { cv::imshow("out", frame); }
|
||||
```
|
||||
|
||||
### Creating Nodes
|
||||
|
||||
```cpp
|
||||
// No port names (index-only access)
|
||||
auto node = make_node<double_it>(/*fifo_capacity=*/5);
|
||||
|
||||
// Named input ports only
|
||||
auto node = make_node<double_it>(in<"value">{}, 5);
|
||||
|
||||
// Named input and output ports
|
||||
auto node = make_node<double_it>(in<"value">{}, out<"result">{}, 5);
|
||||
|
||||
// Named output ports only (e.g. a source with no inputs)
|
||||
auto node = make_node<capture>(out<"colour","grey">{}, 5);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```cpp
|
||||
auto src = make_node<produce>(in<"x">{}, out<"value">{}, 5);
|
||||
auto proc = make_node<double_it>(in<"value">{}, out<"result">{}, 5);
|
||||
|
||||
Network net;
|
||||
net.add("src", src)
|
||||
.add("proc", proc)
|
||||
.connect("src", src.output<0>(), "proc", proc.input<0>()) // by index
|
||||
.connect("src", src.template output<"value">(), "proc", proc.template input<"value">()) // by name
|
||||
.build(); // runs cycle detection — throws NetworkCycleError on cycles
|
||||
|
||||
net.start();
|
||||
// ... do work ...
|
||||
net.stop();
|
||||
```
|
||||
|
||||
> **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:
|
||||
|
||||
```cpp
|
||||
template<> struct kpn::channel_storage_policy<MyType> {
|
||||
static constexpr bool by_value = true;
|
||||
};
|
||||
```
|
||||
|
||||
### 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, wire the final output channel to a `Channel<cv::Mat>` and pop it on the main thread:
|
||||
|
||||
```cpp
|
||||
Channel<cv::Mat> result_ch(8);
|
||||
comp.set_output_channel<0>(&result_ch);
|
||||
// ... build and start network ...
|
||||
|
||||
// Main thread display loop
|
||||
while (true) {
|
||||
cv::Mat frame;
|
||||
if (!result_ch.try_pop(frame, std::chrono::milliseconds(100))) continue;
|
||||
cv::imshow("output", frame);
|
||||
if (cv::waitKey(1) == 'q') break;
|
||||
}
|
||||
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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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/
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
+16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+18
@@ -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")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -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-Za-zA-Z0-90-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")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+1
@@ -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")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+1
@@ -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-Za-zA-Z0-90-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
@@ -0,0 +1 @@
|
||||
module.exports=require("./lunr.ja");
|
||||
+1
@@ -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")}});
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -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))}}}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
@@ -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")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+1
@@ -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
@@ -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
@@ -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-Za-zA-Z0-90-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
@@ -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
@@ -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("|")}}});
|
||||
+18
File diff suppressed because one or more lines are too long
+1
@@ -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
@@ -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")}});
|
||||
@@ -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-Za-zA-Z]":"A",
|
||||
"[0-90-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,"1":-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,"1":-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,"1":-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
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
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"}
|
||||
+1073
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,38 +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]
|
||||
|
||||
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>(5);
|
||||
auto dbl = make_node<double_it>(5);
|
||||
auto sink = make_node<print_it>(5);
|
||||
|
||||
// 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);
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,80 +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;
|
||||
|
||||
// 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 — note the function takes (int, vector<string>)
|
||||
// so we need two separate input ports wired independently
|
||||
// For a two-input sink we wire each output of cnt to a different input of report
|
||||
auto snk = make_node<report>(in<"count", "words">{}, 4);
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,75 +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];
|
||||
}
|
||||
|
||||
// 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)};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,118 +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;
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// ── 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();
|
||||
}
|
||||
@@ -1,78 +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;
|
||||
|
||||
// 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';
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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,190 +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>
|
||||
|
||||
// ── 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 ────────────────────────────────────────────────────────
|
||||
|
||||
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 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.
|
||||
|
||||
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; }
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
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();
|
||||
|
||||
net.set_watchdog_interval(std::chrono::milliseconds(5000));
|
||||
net.set_web_debug_port(9090);
|
||||
|
||||
std::cout << "Cell-shading pipeline running. Press 'q' to stop.\n";
|
||||
std::cout << "Web debug UI: http://localhost:9090\n";
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,43 +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(10_static_hello_pipeline)
|
||||
kpn_example(11_static_fanout)
|
||||
if(KPN_WEB_DEBUG)
|
||||
kpn_target_enable_web_debug(06_watchdog)
|
||||
endif()
|
||||
# 07 and 08 require the Python bindings — only add if built
|
||||
if(KPN_BUILD_PYTHON)
|
||||
# These are Python scripts, not compiled targets — installed alongside kpn_python
|
||||
endif()
|
||||
|
||||
# 09 requires OpenCV — only build if found
|
||||
find_package(OpenCV QUIET COMPONENTS core imgproc highgui videoio)
|
||||
if(OpenCV_FOUND)
|
||||
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
File diff suppressed because it is too large
Load Diff
+1013
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,191 +0,0 @@
|
||||
#pragma once
|
||||
#include "diagnostics.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace kpn {
|
||||
|
||||
// ── 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") {}
|
||||
};
|
||||
|
||||
// ── Channel ───────────────────────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
class Channel {
|
||||
public:
|
||||
using storage_type = channel_storage_t<T>;
|
||||
|
||||
explicit Channel(std::size_t capacity = 5) : capacity_(capacity) {}
|
||||
|
||||
Channel(const Channel&) = delete;
|
||||
Channel& operator=(const Channel&) = delete;
|
||||
|
||||
// Push a value.
|
||||
// - If channel is disabled (accepting_ == false): silently drop, return immediately.
|
||||
// - If channel is full: throw ChannelOverflowError.
|
||||
void push(T value) {
|
||||
if (!accepting_.load(std::memory_order_relaxed)) {
|
||||
stats_.record_drop();
|
||||
return;
|
||||
}
|
||||
std::unique_lock lock(mutex_);
|
||||
if (!accepting_.load(std::memory_order_relaxed)) {
|
||||
stats_.record_drop();
|
||||
return;
|
||||
}
|
||||
if (queue_.size() >= capacity_) {
|
||||
stats_.record_overflow();
|
||||
throw ChannelOverflowError(capacity_);
|
||||
}
|
||||
queue_.push(make_storage(std::move(value)));
|
||||
stats_.record_push(queue_.size());
|
||||
lock.unlock();
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
// Blocking pop. Unblocks when an item is available or the channel is disabled.
|
||||
// Throws ChannelClosedError if disabled and queue is empty.
|
||||
T pop() {
|
||||
std::unique_lock lock(mutex_);
|
||||
cv_.wait(lock, [this] {
|
||||
return !queue_.empty() || !accepting_.load(std::memory_order_relaxed);
|
||||
});
|
||||
if (queue_.empty())
|
||||
throw ChannelClosedError{};
|
||||
T value = extract(std::move(queue_.front()));
|
||||
queue_.pop();
|
||||
stats_.record_pop();
|
||||
return value;
|
||||
}
|
||||
|
||||
// Non-blocking pop with timeout. For watchdog/display use only — not used in run_loop.
|
||||
bool try_pop(T& out, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock lock(mutex_);
|
||||
if (!cv_.wait_for(lock, timeout, [this] {
|
||||
return !queue_.empty() || !accepting_.load(std::memory_order_relaxed);
|
||||
}))
|
||||
return false;
|
||||
if (queue_.empty())
|
||||
return false;
|
||||
out = extract(std::move(queue_.front()));
|
||||
queue_.pop();
|
||||
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: drop all queued items, unblock any waiting pop().
|
||||
// Called by consumer node on stop(). Producer push() will silently drop after this.
|
||||
void disable() {
|
||||
accepting_.store(false, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
while (!queue_.empty()) queue_.pop();
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
std::size_t size() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return queue_.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 {
|
||||
std::lock_guard lock(mutex_);
|
||||
return {
|
||||
name,
|
||||
capacity_,
|
||||
queue_.size(),
|
||||
stats_.peak_fill.load(std::memory_order_relaxed),
|
||||
stats_.pushes.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), // payload bytes — sizeof(T) regardless of storage policy
|
||||
};
|
||||
}
|
||||
|
||||
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_;
|
||||
std::queue<storage_type> queue_;
|
||||
std::atomic<bool> accepting_{true};
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
ChannelStats stats_;
|
||||
};
|
||||
|
||||
// ── 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
|
||||
@@ -1,137 +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> 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) {
|
||||
pushes.fetch_add(1, 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
|
||||
|
||||
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_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 drops;
|
||||
uint64_t overflows;
|
||||
uint64_t pops;
|
||||
std::size_t item_bytes; // sizeof(T) for the stored type — set by Channel<T>
|
||||
|
||||
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: bytes transferred / elapsed seconds
|
||||
double bandwidth_mbs(double elapsed_s) const {
|
||||
if (elapsed_s <= 0.0 || item_bytes == 0) return 0.0;
|
||||
return static_cast<double>(pushes * item_bytes) / elapsed_s / 1e6;
|
||||
}
|
||||
};
|
||||
|
||||
struct NodeSnapshot {
|
||||
std::string name;
|
||||
uint64_t frames_processed;
|
||||
double ema_exec_ms;
|
||||
double max_exec_ms;
|
||||
double total_blocked_ms;
|
||||
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
|
||||
};
|
||||
|
||||
} // namespace kpn
|
||||
@@ -1,177 +0,0 @@
|
||||
#pragma once
|
||||
#include "channel.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "node.hpp"
|
||||
#include "port.hpp"
|
||||
#include "traits.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
namespace kpn {
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Produces std::tuple<T, T, ..., T> with N repetitions — used so that
|
||||
// Network::connect can do its normal return_tuple type-check against FanoutNode.
|
||||
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 detail
|
||||
|
||||
// ── 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 = detail::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
|
||||
@@ -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
|
||||
@@ -1,12 +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 "node.hpp"
|
||||
#include "fanout.hpp"
|
||||
#include "static_network.hpp"
|
||||
#include "main_thread_node.hpp"
|
||||
#include "network.hpp"
|
||||
@@ -1,186 +0,0 @@
|
||||
#pragma once
|
||||
#include "channel.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "fixed_string.hpp"
|
||||
#include "node.hpp" // INode
|
||||
#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
|
||||
@@ -1,330 +0,0 @@
|
||||
#pragma once
|
||||
#include "diagnostics.hpp"
|
||||
#include "node.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);
|
||||
});
|
||||
web_server_->start();
|
||||
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
||||
#endif
|
||||
}
|
||||
|
||||
void stop() 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();
|
||||
}
|
||||
|
||||
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); }
|
||||
|
||||
#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.elapsed_s);
|
||||
}
|
||||
|
||||
private:
|
||||
// ── Diagnostics collection ────────────────────────────────────────────────
|
||||
|
||||
struct Snapshots {
|
||||
std::vector<NodeSnapshot> nodes;
|
||||
std::vector<ChannelSnapshot> channels;
|
||||
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());
|
||||
|
||||
return {std::move(nodes), std::move(channels), elapsed_s};
|
||||
}
|
||||
|
||||
static std::string format_report(const std::vector<NodeSnapshot>& nodes,
|
||||
const std::vector<ChannelSnapshot>& channels,
|
||||
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";
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// ── 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();
|
||||
|
||||
if (diag_handler_) {
|
||||
diag_handler_(s.nodes, s.channels);
|
||||
} else {
|
||||
std::cerr << format_report(s.nodes, s.channels, s.elapsed_s);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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_;
|
||||
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
|
||||
@@ -1,586 +0,0 @@
|
||||
#pragma once
|
||||
#include "channel.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "fixed_string.hpp"
|
||||
#include "port.hpp"
|
||||
#include "traits.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace kpn {
|
||||
|
||||
// ── 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;
|
||||
};
|
||||
|
||||
// ── Node ─────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Template parameters:
|
||||
// Func — the wrapped function (auto NTTP, deduced as a function pointer)
|
||||
// InputNames — optional kpn::in<"a","b"> tag type (at most one)
|
||||
// OutputNames — optional kpn::out<"x","y"> tag type (at most one)
|
||||
|
||||
template<auto Func,
|
||||
typename InputTag = in<>,
|
||||
typename OutputTag = out<>,
|
||||
fixed_string Label = "",
|
||||
std::size_t UniqueTag = 0>
|
||||
class Node;
|
||||
|
||||
// Specialisation that unpacks the in<>/out<> tag packs
|
||||
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> : 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>;
|
||||
|
||||
// Identity accessors — used by StaticNetwork for diagnostics and type-level uniqueness
|
||||
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_node: number of input names must match function arity, or provide none"
|
||||
);
|
||||
static_assert(
|
||||
sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count,
|
||||
"make_node: number of output names must match return tuple size, or provide none"
|
||||
);
|
||||
|
||||
explicit Node(std::size_t fifo_capacity = 5)
|
||||
: fifo_capacity_(fifo_capacity)
|
||||
{
|
||||
init_input_channels(std::make_index_sequence<input_count>{});
|
||||
}
|
||||
|
||||
~Node() override { stop(); }
|
||||
|
||||
// ── INode ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void start() override {
|
||||
enable_inputs(std::make_index_sequence<input_count>{});
|
||||
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);
|
||||
// Disable all input channels: drops queued items and unblocks waiting pop()
|
||||
disable_inputs(std::make_index_sequence<input_count>{});
|
||||
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 — by index ────────────────────────────────────────────────
|
||||
|
||||
template<std::size_t I>
|
||||
InputPort<Node, I> input() {
|
||||
static_assert(I < input_count, "input index out of range");
|
||||
return {*this};
|
||||
}
|
||||
|
||||
template<std::size_t I>
|
||||
OutputPort<Node, 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 (used by Network at connect time) ──────────
|
||||
|
||||
template<std::size_t I>
|
||||
Channel<std::tuple_element_t<I, args_tuple>>& input_channel() {
|
||||
return *std::get<I>(input_channels_);
|
||||
}
|
||||
|
||||
// Replace the owned input channel with an externally provided one.
|
||||
// Used by VariantNodeWrapper to share a Channel<T> with a VariantChannel adapter.
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
// Input channels — shared ownership so VariantChannel adapters can share them
|
||||
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<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>{}));
|
||||
|
||||
// Output channels — non-owning pointers, set at connect time
|
||||
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>{}));
|
||||
|
||||
// ── run_loop ──────────────────────────────────────────────────────────────
|
||||
|
||||
void run_loop() {
|
||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
auto t0 = clock_t::now();
|
||||
auto args = pop_inputs(std::make_index_sequence<input_count>{});
|
||||
auto t1 = clock_t::now();
|
||||
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();
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
|
||||
} catch (const ChannelClosedError&) {
|
||||
break;
|
||||
} catch (const ChannelOverflowError& e) {
|
||||
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop all inputs into a tuple of argument values
|
||||
template<std::size_t... Is>
|
||||
args_tuple pop_inputs(std::index_sequence<Is...>) {
|
||||
return {std::get<Is>(input_channels_)->pop()...};
|
||||
}
|
||||
|
||||
// Normalise return value to tuple (handles void and single-value returns)
|
||||
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));
|
||||
}
|
||||
|
||||
static return_tuple normalise_void() { return {}; }
|
||||
|
||||
// Push each output element to its connected channel (if connected)
|
||||
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(), "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::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{false};
|
||||
std::jthread thread_;
|
||||
NodeStats stats_;
|
||||
};
|
||||
|
||||
// ── ObjectNode — wraps a callable object (functor / class with operator()) ────
|
||||
//
|
||||
// Use this when the node needs state initialised in a constructor.
|
||||
// The object must outlive the ObjectNode (stored by reference).
|
||||
//
|
||||
// Usage:
|
||||
// MyFunctor obj(...);
|
||||
// auto node = make_node(obj, in<"x">{}, out<"y">{}, capacity);
|
||||
|
||||
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> : 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_node: number of input names must match operator() arity, or provide none"
|
||||
);
|
||||
static_assert(
|
||||
sizeof...(OutNames) == 0 || sizeof...(OutNames) == output_count,
|
||||
"make_node: number of output names must match return tuple size, or provide none"
|
||||
);
|
||||
|
||||
explicit ObjectNode(Obj& obj, std::size_t fifo_capacity = 5)
|
||||
: obj_(obj), fifo_capacity_(fifo_capacity)
|
||||
{
|
||||
init_input_channels(std::make_index_sequence<input_count>{});
|
||||
}
|
||||
|
||||
~ObjectNode() override { stop(); }
|
||||
|
||||
// ── INode ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void start() override {
|
||||
enable_inputs(std::make_index_sequence<input_count>{});
|
||||
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);
|
||||
disable_inputs(std::make_index_sequence<input_count>{});
|
||||
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>
|
||||
InputPort<ObjectNode, I> input() {
|
||||
static_assert(I < input_count, "input index out of range");
|
||||
return {*this};
|
||||
}
|
||||
|
||||
template<std::size_t I>
|
||||
OutputPort<ObjectNode, I> output() {
|
||||
static_assert(I < output_count, "output index out of range");
|
||||
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<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 run_loop() {
|
||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
auto t0 = clock_t::now();
|
||||
auto args = pop_inputs(std::make_index_sequence<input_count>{});
|
||||
auto t1 = clock_t::now();
|
||||
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(t1 - t0), cpu0, cpu1);
|
||||
} catch (const ChannelClosedError&) {
|
||||
break;
|
||||
} catch (const ChannelOverflowError& e) {
|
||||
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
args_tuple pop_inputs(std::index_sequence<Is...>) {
|
||||
return {std::get<Is>(input_channels_)->pop()...};
|
||||
}
|
||||
|
||||
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(), "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) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
Obj& obj_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{false};
|
||||
std::jthread thread_;
|
||||
NodeStats stats_;
|
||||
};
|
||||
|
||||
// ── 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) ──────────────────────────────────────────────────
|
||||
//
|
||||
// Usage:
|
||||
// make_node<func>(capacity)
|
||||
// make_node<func, "label">(capacity)
|
||||
// make_node<func, "label", 1>(capacity) // UniqueTag=1
|
||||
// make_node<func>(in<"a","b">{}, capacity)
|
||||
// make_node<func, "label">(in<"a","b">{}, out<"x">{}, capacity)
|
||||
|
||||
// No port names
|
||||
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);
|
||||
}
|
||||
|
||||
// in<> only
|
||||
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);
|
||||
}
|
||||
|
||||
// out<> only
|
||||
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);
|
||||
}
|
||||
|
||||
// in<> and out<>
|
||||
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
|
||||
@@ -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
|
||||
@@ -1,412 +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 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)
|
||||
// Wires src's output port out_idx to dst's input port in_idx.
|
||||
// Type check: both sides must carry the same T.
|
||||
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() + ")");
|
||||
|
||||
// The destination node owns the input channel — get it, then tell src to use it.
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
// Read one value from node's output port. Releases GIL while blocking.
|
||||
nb::object read(const std::string& node_name, std::size_t out_idx) {
|
||||
// We need a channel that sits on the output of this node.
|
||||
// read() installs a tap channel if not already present.
|
||||
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");
|
||||
// Create a tap channel matching the output type and wire it
|
||||
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));
|
||||
}
|
||||
|
||||
// Write a Python value into node's input port. Releases GIL while blocking.
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converter registration ────────────────────────────────────────────────
|
||||
// Called once per type at module init time to register to/from Python converters.
|
||||
|
||||
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)) };
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
// Create the right VariantChannel<T> based on the registered type index.
|
||||
// We need a factory registered per type — stored in tap_factories_.
|
||||
auto it = tap_factories_.find(type);
|
||||
if (it == tap_factories_.end())
|
||||
throw std::runtime_error(
|
||||
"no tap factory for type: " + std::string(type.name()) +
|
||||
" — was register_type() called for this type?");
|
||||
return it->second();
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public:
|
||||
// Called by register_type to also register a tap channel factory.
|
||||
template<typename T>
|
||||
void register_tap_factory(std::size_t capacity = 5) {
|
||||
auto idx = std::type_index(typeid(T));
|
||||
tap_factories_[idx] = [capacity]() -> std::shared_ptr<VChannel> {
|
||||
auto ch = std::make_shared<Channel<T>>(capacity);
|
||||
return std::make_shared<VariantChannel<T, Variant>>(std::move(ch));
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
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_;
|
||||
std::map<std::type_index, std::function<std::shared_ptr<VChannel>()>> tap_factories_;
|
||||
};
|
||||
|
||||
// ── PyNode<Variant> ───────────────────────────────────────────────────────────
|
||||
// A pure-Python processing node. Holds a nanobind callable.
|
||||
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs (release GIL).
|
||||
|
||||
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();
|
||||
// Release GIL while joining — run_loop may be waiting to acquire it.
|
||||
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() {
|
||||
// This thread does not hold the GIL. It acquires it only for Python calls.
|
||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
auto t0 = clock_t::now();
|
||||
|
||||
// Pop all inputs — no GIL needed, these are pure C++ channel ops
|
||||
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();
|
||||
|
||||
// Acquire GIL only for the Python call and type conversion
|
||||
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);
|
||||
|
||||
// Push outputs — no GIL needed
|
||||
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 ───────────────────────────────────────────────────────
|
||||
// Registers PyNetwork<Variant> and PyNode<Variant> with the given nanobind module.
|
||||
// Call once per module, passing the Variant type derived from your registered node types.
|
||||
|
||||
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") = 0)
|
||||
.def("write", &Net::write,
|
||||
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
|
||||
}
|
||||
|
||||
} // namespace kpn::python
|
||||
|
||||
#endif // KPN_BUILD_PYTHON
|
||||
@@ -1,335 +0,0 @@
|
||||
#pragma once
|
||||
#include "channel.hpp"
|
||||
#include "diagnostics.hpp"
|
||||
#include "fanout.hpp"
|
||||
#include "node.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 <string>
|
||||
#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::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
|
||||
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);
|
||||
});
|
||||
web_server_->start();
|
||||
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
||||
#endif
|
||||
}
|
||||
|
||||
void stop() 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();
|
||||
}
|
||||
|
||||
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; }
|
||||
#endif
|
||||
|
||||
// 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;
|
||||
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());
|
||||
|
||||
return {std::move(nodes), std::move(channels), elapsed_s};
|
||||
}
|
||||
|
||||
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_;
|
||||
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
|
||||
};
|
||||
|
||||
// ── 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()) {
|
||||
user_node_ptrs.push_back(s);
|
||||
user_node_names.push_back(node_display_name<SrcT>());
|
||||
}
|
||||
if (std::find(user_node_ptrs.begin(), user_node_ptrs.end(), d) == user_node_ptrs.end()) {
|
||||
user_node_ptrs.push_back(d);
|
||||
user_node_names.push_back(node_display_name<DstT>());
|
||||
}
|
||||
};
|
||||
(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)>;
|
||||
fanout_ptrs.push_back(static_cast<INode*>(&node));
|
||||
fanout_node_names.push_back(node_name.template operator()<NodeT>());
|
||||
}(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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,86 +0,0 @@
|
||||
#pragma once
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace kpn {
|
||||
|
||||
// ── Primary template — not defined; only specialisations match ────────────────
|
||||
template<typename F>
|
||||
struct function_traits;
|
||||
|
||||
// Free function
|
||||
template<typename R, typename... Args>
|
||||
struct function_traits<R(Args...)> {
|
||||
using return_t = R;
|
||||
using args = std::tuple<Args...>;
|
||||
static constexpr std::size_t arity = sizeof...(Args);
|
||||
};
|
||||
|
||||
// Function pointer
|
||||
template<typename R, typename... Args>
|
||||
struct function_traits<R(*)(Args...)> : function_traits<R(Args...)> {};
|
||||
|
||||
// Member function pointer (const)
|
||||
template<typename C, typename R, typename... Args>
|
||||
struct function_traits<R(C::*)(Args...) const> : function_traits<R(Args...)> {};
|
||||
|
||||
// Member function pointer (non-const)
|
||||
template<typename C, typename R, typename... Args>
|
||||
struct function_traits<R(C::*)(Args...)> : function_traits<R(Args...)> {};
|
||||
|
||||
// Callable (lambda / std::function) — delegate to operator()
|
||||
template<typename F>
|
||||
struct function_traits : function_traits<decltype(&F::operator())> {};
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
template<typename F>
|
||||
using return_t = typename function_traits<std::remove_cvref_t<F>>::return_t;
|
||||
|
||||
template<typename F>
|
||||
using args_t = typename function_traits<std::remove_cvref_t<F>>::args;
|
||||
|
||||
template<typename F>
|
||||
inline constexpr std::size_t arity_v = function_traits<std::remove_cvref_t<F>>::arity;
|
||||
|
||||
// ── Tuple detection ───────────────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
struct is_tuple : std::false_type {};
|
||||
|
||||
template<typename... Ts>
|
||||
struct is_tuple<std::tuple<Ts...>> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
inline constexpr bool is_tuple_v = is_tuple<T>::value;
|
||||
|
||||
// ── Normalise return type to always be a tuple ────────────────────────────────
|
||||
// void → std::tuple<>
|
||||
// T (non-tup) → std::tuple<T>
|
||||
// tuple<...> → tuple<...> (unchanged)
|
||||
|
||||
template<typename T>
|
||||
struct normalise_return {
|
||||
using type = std::tuple<T>;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct normalise_return<void> {
|
||||
using type = std::tuple<>;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
struct normalise_return<std::tuple<Ts...>> {
|
||||
using type = std::tuple<Ts...>;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using normalised_return_t = typename normalise_return<T>::type;
|
||||
|
||||
// ── Output count from a function type ────────────────────────────────────────
|
||||
|
||||
template<typename F>
|
||||
inline constexpr std::size_t output_count_v =
|
||||
std::tuple_size_v<normalised_return_t<return_t<F>>>;
|
||||
|
||||
} // namespace kpn
|
||||
@@ -1,249 +0,0 @@
|
||||
#pragma once
|
||||
#include "channel.hpp"
|
||||
#include "node.hpp"
|
||||
#include "traits.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
#include <variant>
|
||||
|
||||
namespace kpn {
|
||||
|
||||
// ── unique_types TMP helper ───────────────────────────────────────────────────
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename Result, typename... Ts>
|
||||
struct unique_types_impl;
|
||||
|
||||
template<typename... Unique>
|
||||
struct unique_types_impl<std::tuple<Unique...>> {
|
||||
using type = std::tuple<Unique...>;
|
||||
};
|
||||
|
||||
template<typename... Unique, typename Head, typename... Tail>
|
||||
struct unique_types_impl<std::tuple<Unique...>, Head, Tail...> {
|
||||
using type = std::conditional_t<
|
||||
(std::is_same_v<Head, Unique> || ...),
|
||||
typename unique_types_impl<std::tuple<Unique...>, Tail...>::type,
|
||||
typename unique_types_impl<std::tuple<Unique..., Head>, Tail...>::type
|
||||
>;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
using unique_types_t = typename unique_types_impl<std::tuple<>, Ts...>::type;
|
||||
|
||||
template<typename Tup>
|
||||
struct tuple_to_variant;
|
||||
|
||||
template<typename... Ts>
|
||||
struct tuple_to_variant<std::tuple<Ts...>> {
|
||||
using type = std::variant<Ts...>;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── IVariantChannel ───────────────────────────────────────────────────────────
|
||||
// Type-erased channel surface used by PyNetwork.
|
||||
// The underlying FIFO stores raw T — variant conversion happens only at push/pop.
|
||||
|
||||
template<typename Variant>
|
||||
class IVariantChannel {
|
||||
public:
|
||||
virtual ~IVariantChannel() = default;
|
||||
virtual void push(Variant v) = 0;
|
||||
virtual Variant pop() = 0;
|
||||
virtual std::type_index type_index() const = 0;
|
||||
virtual std::string type_name() const = 0;
|
||||
virtual void enable() = 0;
|
||||
virtual void disable() = 0;
|
||||
};
|
||||
|
||||
// ── VariantChannel<T, Variant> ────────────────────────────────────────────────
|
||||
// Shares a Channel<T> with the node that owns the input slot.
|
||||
// push(): std::get<T> from Variant → raw T into the queue.
|
||||
// pop(): raw T from queue → wrapped into Variant.
|
||||
|
||||
template<typename T, typename Variant>
|
||||
class VariantChannel : public IVariantChannel<Variant> {
|
||||
public:
|
||||
explicit VariantChannel(std::shared_ptr<Channel<T>> ch)
|
||||
: channel_(std::move(ch)) {}
|
||||
|
||||
void push(Variant v) override {
|
||||
channel_->push(std::get<T>(std::move(v)));
|
||||
}
|
||||
Variant pop() override {
|
||||
return Variant{ channel_->pop() };
|
||||
}
|
||||
std::type_index type_index() const override { return std::type_index(typeid(T)); }
|
||||
std::string type_name() const override { return typeid(T).name(); }
|
||||
void enable() override { channel_->enable(); }
|
||||
void disable() override { channel_->disable(); }
|
||||
|
||||
Channel<T>* raw_ptr() { return channel_.get(); }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Channel<T>> channel_;
|
||||
};
|
||||
|
||||
// ── IVariantNode ──────────────────────────────────────────────────────────────
|
||||
|
||||
template<typename Variant>
|
||||
class IVariantNode : public INode,
|
||||
public std::enable_shared_from_this<IVariantNode<Variant>> {
|
||||
public:
|
||||
void set_name(std::string name) override { name_ = std::move(name); }
|
||||
const std::string& name() const { return name_; }
|
||||
|
||||
virtual std::size_t input_count() const = 0;
|
||||
virtual std::size_t output_count() const = 0;
|
||||
virtual std::type_index input_type(std::size_t i) const = 0;
|
||||
virtual std::type_index output_type(std::size_t i) const = 0;
|
||||
|
||||
// Returns the IVariantChannel wrapping this node's input slot i.
|
||||
// PyNetwork calls this on the *destination* node to get the channel to wire upstream into.
|
||||
virtual std::shared_ptr<IVariantChannel<Variant>> input_channel(std::size_t i) = 0;
|
||||
|
||||
// Wires this node's output slot i into ch (ch belongs to the downstream node's input).
|
||||
virtual void set_output_channel(std::size_t i,
|
||||
std::shared_ptr<IVariantChannel<Variant>> ch) = 0;
|
||||
private:
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
// ── VariantNodeWrapper<Func, Variant, InTag, OutTag> ──────────────────────────
|
||||
// Wraps a Node<Func,...> for use inside a PyNetwork.
|
||||
//
|
||||
// At construction: for each input port I, builds a shared_ptr<Channel<ArgI>> and
|
||||
// installs it in both the wrapped node (via set_input_channel) and a
|
||||
// VariantChannel<ArgI> adapter. PyNetwork passes the adapter to the upstream
|
||||
// node's set_output_channel so the upstream raw pointer points at the same Channel<T>.
|
||||
//
|
||||
// At connect (output side): downcasts the provided IVariantChannel to
|
||||
// VariantChannel<RetI>, then calls node_.set_output_channel with the raw ptr.
|
||||
|
||||
template<auto Func, typename Variant,
|
||||
typename InputTag = in<>,
|
||||
typename OutputTag = out<>>
|
||||
class VariantNodeWrapper;
|
||||
|
||||
template<auto Func, typename Variant,
|
||||
fixed_string... InNames, fixed_string... OutNames>
|
||||
class VariantNodeWrapper<Func, Variant, in<InNames...>, out<OutNames...>>
|
||||
: public IVariantNode<Variant>
|
||||
{
|
||||
using NodeT = Node<Func, in<InNames...>, out<OutNames...>>;
|
||||
|
||||
public:
|
||||
using args_tuple = typename NodeT::args_tuple;
|
||||
using return_tuple = typename NodeT::return_tuple;
|
||||
|
||||
static constexpr std::size_t n_in = NodeT::input_count;
|
||||
static constexpr std::size_t n_out = NodeT::output_count;
|
||||
|
||||
explicit VariantNodeWrapper(std::size_t fifo_capacity = 5)
|
||||
: node_(fifo_capacity)
|
||||
, in_channels_(n_in)
|
||||
, out_channels_(n_out)
|
||||
, out_type_indices_(n_out, std::type_index(typeid(void)))
|
||||
{
|
||||
init_inputs(std::make_index_sequence<n_in>{}, fifo_capacity);
|
||||
init_out_types(std::make_index_sequence<n_out>{});
|
||||
}
|
||||
|
||||
// ── INode ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void start() override { node_.start(); }
|
||||
void stop() override { node_.stop(); }
|
||||
bool running() const override { return node_.running(); }
|
||||
const NodeStats& stats() const override { return node_.stats(); }
|
||||
void set_name(std::string name) override { node_.set_name(std::move(name)); }
|
||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||
return node_.node_snapshot(name, elapsed_s);
|
||||
}
|
||||
|
||||
// ── IVariantNode ──────────────────────────────────────────────────────────
|
||||
|
||||
std::size_t input_count() const override { return n_in; }
|
||||
std::size_t output_count() const override { return n_out; }
|
||||
|
||||
std::type_index input_type(std::size_t i) const override {
|
||||
return in_channels_[i]->type_index();
|
||||
}
|
||||
std::type_index output_type(std::size_t i) const override {
|
||||
return out_type_indices_[i];
|
||||
}
|
||||
|
||||
std::shared_ptr<IVariantChannel<Variant>> input_channel(std::size_t i) override {
|
||||
return in_channels_[i];
|
||||
}
|
||||
|
||||
void set_output_channel(std::size_t i,
|
||||
std::shared_ptr<IVariantChannel<Variant>> ch) override {
|
||||
set_output_impl(i, std::move(ch), std::make_index_sequence<n_out>{});
|
||||
}
|
||||
|
||||
private:
|
||||
template<std::size_t... Is>
|
||||
void init_inputs(std::index_sequence<Is...>, std::size_t cap) {
|
||||
((init_one_input<Is>(cap)), ...);
|
||||
}
|
||||
|
||||
template<std::size_t I>
|
||||
void init_one_input(std::size_t cap) {
|
||||
using T = std::tuple_element_t<I, args_tuple>;
|
||||
auto shared_ch = std::make_shared<Channel<T>>(cap);
|
||||
// Install the shared Channel<T> in the node's input slot
|
||||
node_.template set_input_channel<I>(shared_ch);
|
||||
// Wrap it in a VariantChannel for PyNetwork to use
|
||||
in_channels_[I] = std::make_shared<VariantChannel<T, Variant>>(std::move(shared_ch));
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
void init_out_types(std::index_sequence<Is...>) {
|
||||
((out_type_indices_[Is] =
|
||||
std::type_index(typeid(std::tuple_element_t<Is, return_tuple>))), ...);
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
void set_output_impl(std::size_t port,
|
||||
std::shared_ptr<IVariantChannel<Variant>> ch,
|
||||
std::index_sequence<Is...>) {
|
||||
bool matched = false;
|
||||
((Is == port && (set_output_at<Is>(std::move(ch)), matched = true)), ...);
|
||||
if (!matched)
|
||||
throw std::out_of_range("set_output_channel: port index out of range");
|
||||
}
|
||||
|
||||
template<std::size_t I>
|
||||
void set_output_at(std::shared_ptr<IVariantChannel<Variant>> ch) {
|
||||
using T = std::tuple_element_t<I, return_tuple>;
|
||||
auto* typed = dynamic_cast<VariantChannel<T, Variant>*>(ch.get());
|
||||
if (!typed)
|
||||
throw std::runtime_error(
|
||||
"set_output_channel: type mismatch at output port " + std::to_string(I));
|
||||
// The downstream VariantChannel holds a shared Channel<T>; point our output at it.
|
||||
// We need a raw ptr — extract it via a getter.
|
||||
node_.template set_output_channel<I>(typed->raw_ptr());
|
||||
out_channels_[I] = std::move(ch);
|
||||
}
|
||||
|
||||
NodeT node_;
|
||||
std::vector<std::shared_ptr<IVariantChannel<Variant>>> in_channels_;
|
||||
std::vector<std::shared_ptr<IVariantChannel<Variant>>> out_channels_;
|
||||
std::vector<std::type_index> out_type_indices_;
|
||||
};
|
||||
|
||||
// ── PythonConverter ───────────────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
struct PythonConverter {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"PythonConverter<T> must be specialised for each type used in a PyNetwork");
|
||||
};
|
||||
|
||||
} // namespace kpn
|
||||
@@ -1,346 +0,0 @@
|
||||
#pragma once
|
||||
// Only compiled when KPN_WEB_DEBUG is defined. network.hpp includes this conditionally.
|
||||
|
||||
#include "diagnostics.hpp"
|
||||
#include <httplib.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace kpn::web_debug {
|
||||
|
||||
// ── Minimal JSON serialiser ───────────────────────────────────────────────────
|
||||
|
||||
static std::string escape_json(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (c == '"') out += "\\\"";
|
||||
else if (c == '\\') out += "\\\\";
|
||||
else if (c == '\n') out += "\\n";
|
||||
else if (c == '\r') out += "\\r";
|
||||
else if (c == '\t') out += "\\t";
|
||||
else out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Parse "src_name:N → dst_name:M" into {src_name, dst_name}.
|
||||
// Stored separately so the browser doesn't need to regex-parse a UTF-8 arrow.
|
||||
static std::pair<std::string,std::string> parse_edge_name(const std::string& name) {
|
||||
// Format: "<src>:<idx> → <dst>:<idx>"
|
||||
auto arrow = name.find(" \xe2\x86\x92 "); // UTF-8 for →
|
||||
if (arrow == std::string::npos) return {name, name};
|
||||
std::string src_part = name.substr(0, arrow);
|
||||
std::string dst_part = name.substr(arrow + 5); // " → " = 5 bytes (space + 3-byte arrow + space)
|
||||
// Strip ":N" index suffix
|
||||
auto sc1 = src_part.rfind(':');
|
||||
auto sc2 = dst_part.rfind(':');
|
||||
if (sc1 != std::string::npos) src_part = src_part.substr(0, sc1);
|
||||
if (sc2 != std::string::npos) dst_part = dst_part.substr(0, sc2);
|
||||
return {src_part, dst_part};
|
||||
}
|
||||
|
||||
static std::string to_json(const std::vector<NodeSnapshot>& nodes,
|
||||
const std::vector<ChannelSnapshot>& channels,
|
||||
double elapsed_s = 0.0) {
|
||||
std::ostringstream o;
|
||||
o << std::fixed;
|
||||
o.precision(2);
|
||||
|
||||
o << "{\"nodes\":[";
|
||||
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||
const auto& n = nodes[i];
|
||||
if (i) o << ',';
|
||||
o << "{\"id\":\"" << 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 << "],\"edges\":[";
|
||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||
const auto& c = channels[i];
|
||||
if (i) o << ',';
|
||||
auto [src, dst] = parse_edge_name(c.name);
|
||||
o << "{\"name\":\"" << escape_json(c.name) << "\""
|
||||
<< ",\"source\":\"" << escape_json(src) << "\""
|
||||
<< ",\"target\":\"" << 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(elapsed_s)
|
||||
<< "}";
|
||||
}
|
||||
o << "]}";
|
||||
return o.str();
|
||||
}
|
||||
|
||||
// ── Embedded single-page HTML ─────────────────────────────────────────────────
|
||||
|
||||
static const char* HTML = R"html(<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>KPN++ Web Debug</title>
|
||||
<style>
|
||||
body { margin: 0; background: #1a1a2e; color: #eee; font-family: monospace; }
|
||||
#header { padding: 12px 20px; background: #16213e; border-bottom: 1px solid #0f3460; }
|
||||
#header h1 { margin: 0; font-size: 18px; color: #e94560; }
|
||||
#header span { font-size: 12px; color: #888; margin-left: 16px; }
|
||||
#graph { width: 100vw; height: calc(100vh - 50px); }
|
||||
.node circle { stroke: #fff; stroke-width: 1.5px; }
|
||||
.node text { font-size: 11px; fill: #eee; pointer-events: none; text-anchor: middle; }
|
||||
.node .stats { font-size: 9px; fill: #aaa; }
|
||||
.link { fill: none; stroke-width: 2px; }
|
||||
.link-label { font-size: 9px; fill: #ccc; }
|
||||
.arrowhead { fill: #888; }
|
||||
#tooltip {
|
||||
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="header"><h1>KPN++ Web Debug</h1><span id="status">connecting...</span></div>
|
||||
<svg id="graph"></svg>
|
||||
<div id="tooltip"></div>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script>
|
||||
const nodeRadius = 30;
|
||||
|
||||
const svg = d3.select('#graph');
|
||||
const width = () => window.innerWidth;
|
||||
const height = () => window.innerHeight - 50;
|
||||
|
||||
// Arrow marker defs
|
||||
const defs = svg.append('defs');
|
||||
['green','#f0c040','#e94560'].forEach((col, i) => {
|
||||
defs.append('marker')
|
||||
.attr('id', 'arrow' + 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)));
|
||||
|
||||
let sim, linkSel, nodeSel, labelSel, edgeLabelSel;
|
||||
let nodes = [], links = [];
|
||||
|
||||
function edgeColor(fill_pct) {
|
||||
if (fill_pct >= 80) return '#e94560';
|
||||
if (fill_pct >= 50) return '#f0c040';
|
||||
return '#4CAF50';
|
||||
}
|
||||
function edgeArrow(fill_pct) {
|
||||
if (fill_pct >= 80) return 'url(#arrow2)';
|
||||
if (fill_pct >= 50) return 'url(#arrow1)';
|
||||
return 'url(#arrow0)';
|
||||
}
|
||||
function nodeColor(ema) {
|
||||
if (ema > 100) return '#e94560';
|
||||
if (ema > 50) return '#e07040';
|
||||
if (ema > 10) return '#f0c040';
|
||||
return '#4CAF50';
|
||||
}
|
||||
|
||||
function init(data) {
|
||||
nodes = data.nodes.map(n => ({ ...n, x: width()/2, y: height()/2 }));
|
||||
const nodeById = Object.fromEntries(nodes.map(n => [n.id, n]));
|
||||
|
||||
links = data.edges.map(e => ({
|
||||
...e,
|
||||
source: nodeById[e.source],
|
||||
target: nodeById[e.target],
|
||||
})).filter(e => e.source && e.target);
|
||||
|
||||
sim = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).distance(160).strength(0.5))
|
||||
.force('charge', d3.forceManyBody().strength(-400))
|
||||
.force('center', d3.forceCenter(width()/2, height()/2))
|
||||
.force('collide', d3.forceCollide(nodeRadius + 20))
|
||||
.on('tick', ticked);
|
||||
|
||||
// Links
|
||||
linkSel = g.append('g').selectAll('line').data(links).join('line')
|
||||
.attr('class', 'link')
|
||||
.attr('stroke', d => edgeColor(d.fill_pct))
|
||||
.attr('marker-end', d => edgeArrow(d.fill_pct));
|
||||
|
||||
edgeLabelSel = g.append('g').selectAll('text').data(links).join('text')
|
||||
.attr('class', 'link-label')
|
||||
.text(d => `${d.fill_pct.toFixed(0)}%`);
|
||||
|
||||
// Nodes
|
||||
const nodeG = 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; }));
|
||||
|
||||
nodeG.append('circle').attr('r', nodeRadius).attr('fill', d => nodeColor(d.ema_exec_ms));
|
||||
nodeG.append('text').attr('dy', 4).text(d => d.id);
|
||||
nodeG.append('text').attr('class', 'stats').attr('dy', 18)
|
||||
.text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
|
||||
|
||||
nodeSel = nodeG;
|
||||
|
||||
// Tooltips
|
||||
const tip = d3.select('#tooltip');
|
||||
nodeG.on('mousemove', (e, d) => {
|
||||
tip.style('display','block')
|
||||
.style('left', (e.pageX+12)+'px').style('top', (e.pageY+12)+'px')
|
||||
.text(
|
||||
`node: ${d.id}\nframes: ${d.frames}\nexec (ema): ${d.ema_exec_ms.toFixed(2)} ms` +
|
||||
`\nexec (max): ${d.max_exec_ms.toFixed(2)} ms\nblocked: ${d.blocked_ms.toFixed(2)} ms` +
|
||||
`\nfps: ${d.fps.toFixed(2)}\ncpu total: ${d.total_cpu_ms.toFixed(1)} ms` +
|
||||
`\ncpu util: ${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(
|
||||
`channel: ${d.name}\nfill: ${d.fill_pct.toFixed(1)}% peak: ${d.peak_pct.toFixed(1)}%` +
|
||||
`\ncapacity: ${d.capacity} current: ${d.current}` +
|
||||
`\npushes: ${d.pushes} drops: ${d.drops} overflows: ${d.overflows}` +
|
||||
`\nbandwidth: ${d.bw_mbs.toFixed(2)} MB/s item: ${d.item_bytes} B`);
|
||||
}).on('mouseleave', () => tip.style('display','none'));
|
||||
}
|
||||
|
||||
function update(data) {
|
||||
// Update node stats in-place (preserve simulation x/y positions)
|
||||
const byId = Object.fromEntries(data.nodes.map(n => [n.id, n]));
|
||||
nodes.forEach(n => {
|
||||
const fresh = byId[n.id];
|
||||
if (fresh) {
|
||||
n.frames = fresh.frames; n.ema_exec_ms = fresh.ema_exec_ms;
|
||||
n.max_exec_ms = fresh.max_exec_ms; n.blocked_ms = fresh.blocked_ms;
|
||||
n.fps = fresh.fps; n.total_cpu_ms = fresh.total_cpu_ms;
|
||||
n.cpu_util_pct = fresh.cpu_util_pct;
|
||||
}
|
||||
});
|
||||
|
||||
// Update edge stats in-place (source/target are already D3 node refs — don't overwrite)
|
||||
data.edges.forEach((e, i) => {
|
||||
if (!links[i]) return;
|
||||
links[i].fill_pct = e.fill_pct; links[i].peak_pct = e.peak_pct;
|
||||
links[i].pushes = e.pushes; links[i].drops = e.drops;
|
||||
links[i].overflows = e.overflows; links[i].current = e.current;
|
||||
links[i].bw_mbs = e.bw_mbs;
|
||||
});
|
||||
|
||||
// Re-color
|
||||
nodeSel.select('circle').attr('fill', d => nodeColor(d.ema_exec_ms));
|
||||
nodeSel.select('.stats').text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
|
||||
linkSel.attr('stroke', d => edgeColor(d.fill_pct))
|
||||
.attr('marker-end', d => edgeArrow(d.fill_pct));
|
||||
edgeLabelSel.text(d => `${d.fill_pct.toFixed(0)}%`);
|
||||
}
|
||||
|
||||
function ticked() {
|
||||
// Clamp nodes to viewport
|
||||
nodes.forEach(d => {
|
||||
d.x = Math.max(nodeRadius, Math.min(width() - nodeRadius, d.x));
|
||||
d.y = Math.max(nodeRadius, Math.min(height() - nodeRadius, d.y));
|
||||
});
|
||||
|
||||
linkSel
|
||||
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||
.attr('x2', d => { // shorten to node edge
|
||||
const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
|
||||
const dist = Math.sqrt(dx*dx+dy*dy) || 1;
|
||||
return d.target.x - (dx/dist)*(nodeRadius+8);
|
||||
})
|
||||
.attr('y2', d => {
|
||||
const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
|
||||
const dist = Math.sqrt(dx*dx+dy*dy) || 1;
|
||||
return d.target.y - (dy/dist)*(nodeRadius+8);
|
||||
});
|
||||
|
||||
edgeLabelSel
|
||||
.attr('x', d => (d.source.x + d.target.x)/2)
|
||||
.attr('y', d => (d.source.y + d.target.y)/2 - 6);
|
||||
|
||||
nodeSel.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
}
|
||||
|
||||
let initialised = false;
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch('/api/snapshot');
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
const data = await r.json();
|
||||
document.getElementById('status').textContent =
|
||||
`last update: ${new Date().toLocaleTimeString()} • ${data.nodes.length} nodes, ${data.edges.length} edges`;
|
||||
if (!initialised) { init(data); initialised = true; }
|
||||
else { update(data); }
|
||||
} catch(e) {
|
||||
document.getElementById('status').textContent = `error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, 500);
|
||||
window.addEventListener('resize', () => sim && sim.force('center', d3.forceCenter(width()/2, height()/2)).alpha(0.1).restart());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)html";
|
||||
|
||||
// ── WebDebugServer ────────────────────────────────────────────────────────────
|
||||
|
||||
class WebDebugServer {
|
||||
public:
|
||||
using SnapshotFn = std::function<std::string()>;
|
||||
|
||||
explicit WebDebugServer(uint16_t port, SnapshotFn fn)
|
||||
: port_(port), snapshot_fn_(std::move(fn)) {}
|
||||
|
||||
void start() {
|
||||
svr_.Get("/", [](const httplib::Request&, httplib::Response& res) {
|
||||
res.set_content(HTML, "text/html");
|
||||
});
|
||||
svr_.Get("/api/snapshot", [this](const httplib::Request&, httplib::Response& res) {
|
||||
res.set_content(snapshot_fn_(), "application/json");
|
||||
});
|
||||
thread_ = std::thread([this] {
|
||||
svr_.listen("0.0.0.0", static_cast<int>(port_));
|
||||
});
|
||||
}
|
||||
|
||||
void stop() {
|
||||
svr_.stop();
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
~WebDebugServer() { stop(); }
|
||||
|
||||
WebDebugServer(const WebDebugServer&) = delete;
|
||||
WebDebugServer& operator=(const WebDebugServer&) = delete;
|
||||
|
||||
private:
|
||||
uint16_t port_;
|
||||
SnapshotFn snapshot_fn_;
|
||||
httplib::Server svr_;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
} // namespace kpn::web_debug
|
||||
+1017
File diff suppressed because it is too large
Load Diff
+1154
File diff suppressed because it is too large
Load Diff
+1148
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
nanobind_add_module(kpn_python kpn_python.cpp)
|
||||
target_link_libraries(kpn_python PRIVATE kpn)
|
||||
target_compile_definitions(kpn_python PRIVATE KPN_BUILD_PYTHON)
|
||||
@@ -1,164 +0,0 @@
|
||||
#define KPN_BUILD_PYTHON
|
||||
#include <kpn/python/bindings.hpp>
|
||||
#include <nanobind/nanobind.h>
|
||||
#include <nanobind/stl/shared_ptr.h>
|
||||
#include <nanobind/stl/string.h>
|
||||
#include <nanobind/stl/vector.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
|
||||
namespace nb = nanobind;
|
||||
using namespace kpn;
|
||||
using namespace kpn::python;
|
||||
|
||||
// ── Node functions for the hello-pipeline examples ────────────────────────────
|
||||
|
||||
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'; }
|
||||
|
||||
// ── Variant type ──────────────────────────────────────────────────────────────
|
||||
// Deduplicated port types across all registered node functions.
|
||||
// produce: () → int | double_it: int → int | print_it: int → void
|
||||
// Unique types: int
|
||||
|
||||
using KpnVariant = std::variant<int>;
|
||||
using Net = PyNetwork<KpnVariant>;
|
||||
using ProduceNode = VariantNodeWrapper<produce, KpnVariant>;
|
||||
using DoubleItNode= VariantNodeWrapper<double_it, KpnVariant>;
|
||||
using PrintItNode = VariantNodeWrapper<print_it, KpnVariant>;
|
||||
|
||||
// ── Converter helpers for int ─────────────────────────────────────────────────
|
||||
|
||||
static nb::object int_to_py(const KpnVariant& v) {
|
||||
return nb::int_(std::get<int>(v));
|
||||
}
|
||||
|
||||
static KpnVariant int_from_py(nb::object o) {
|
||||
return KpnVariant{ nb::cast<int>(o) };
|
||||
}
|
||||
|
||||
// ── Type name resolver (extensible) ──────────────────────────────────────────
|
||||
|
||||
static std::type_index resolve_type(const std::string& name) {
|
||||
if (name == "int") return std::type_index(typeid(int));
|
||||
throw std::runtime_error("unknown type name '" + name +
|
||||
"' — only 'int' is registered in this module");
|
||||
}
|
||||
|
||||
// ── Ensure converters are registered on a Net instance ───────────────────────
|
||||
|
||||
static void ensure_converters(Net& net) {
|
||||
net.register_type<int>(
|
||||
[](const int& v) -> nb::object { return nb::int_(v); },
|
||||
[](nb::object o) -> int { return nb::cast<int>(o); }
|
||||
);
|
||||
net.register_tap_factory<int>();
|
||||
}
|
||||
|
||||
NB_MODULE(kpn_python, m) {
|
||||
m.doc() = "KPN++ Python bindings — Kahn Process Network library";
|
||||
|
||||
// ── IVariantNode base ─────────────────────────────────────────────────────
|
||||
nb::class_<IVariantNode<KpnVariant>>(m, "INode");
|
||||
|
||||
// ── Concrete C++ node wrappers ─────────────────────────────────────────────
|
||||
// Factories return shared_ptr so Python can pass them to net.add().
|
||||
nb::class_<ProduceNode, IVariantNode<KpnVariant>>(m, "ProduceNode")
|
||||
.def("__init__", [](ProduceNode* self, std::size_t cap) {
|
||||
new (self) ProduceNode(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
nb::class_<DoubleItNode, IVariantNode<KpnVariant>>(m, "DoubleItNode")
|
||||
.def("__init__", [](DoubleItNode* self, std::size_t cap) {
|
||||
new (self) DoubleItNode(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
nb::class_<PrintItNode, IVariantNode<KpnVariant>>(m, "PrintItNode")
|
||||
.def("__init__", [](PrintItNode* self, std::size_t cap) {
|
||||
new (self) PrintItNode(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
// Expose factory functions that return shared_ptr — these are what net.add() accepts.
|
||||
m.def("make_produce", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
|
||||
return std::make_shared<ProduceNode>(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
m.def("make_double_it", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
|
||||
return std::make_shared<DoubleItNode>(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
m.def("make_print_it", [](std::size_t cap) -> std::shared_ptr<IVariantNode<KpnVariant>> {
|
||||
return std::make_shared<PrintItNode>(cap);
|
||||
}, nb::arg("capacity") = 5);
|
||||
|
||||
// ── Network ───────────────────────────────────────────────────────────────
|
||||
nb::class_<Net>(m, "Network")
|
||||
.def(nb::init<>())
|
||||
|
||||
// add(name, c++_node) — for pre-constructed C++ nodes
|
||||
.def("add", [](Net& self, std::string name,
|
||||
std::shared_ptr<IVariantNode<KpnVariant>> node) {
|
||||
self.add(std::move(name), std::move(node));
|
||||
}, nb::arg("name"), nb::arg("node"))
|
||||
|
||||
// add_node(name, callable, inputs=[type_names], outputs=[type_names])
|
||||
// Creates a pure Python processing node.
|
||||
.def("add_node", [](Net& self,
|
||||
std::string name,
|
||||
nb::object callable,
|
||||
std::vector<std::string> in_names,
|
||||
std::vector<std::string> out_names,
|
||||
std::size_t capacity)
|
||||
{
|
||||
std::vector<std::type_index> in_types, out_types;
|
||||
for (auto& s : in_names) in_types.push_back(resolve_type(s));
|
||||
for (auto& s : out_names) out_types.push_back(resolve_type(s));
|
||||
|
||||
std::map<std::type_index, std::function<nb::object(const KpnVariant&)>> to_py;
|
||||
std::map<std::type_index, std::function<KpnVariant(nb::object)>> from_py;
|
||||
std::map<std::type_index, PyNode<KpnVariant>::ChannelFactory> ch_factories;
|
||||
|
||||
auto int_idx = std::type_index(typeid(int));
|
||||
to_py[int_idx] = int_to_py;
|
||||
from_py[int_idx] = int_from_py;
|
||||
ch_factories[int_idx] = [](std::size_t cap) -> std::shared_ptr<IVariantChannel<KpnVariant>> {
|
||||
auto ch = std::make_shared<Channel<int>>(cap);
|
||||
return std::make_shared<VariantChannel<int, KpnVariant>>(std::move(ch));
|
||||
};
|
||||
|
||||
auto node = std::make_shared<PyNode<KpnVariant>>(
|
||||
std::move(callable),
|
||||
std::move(in_types),
|
||||
std::move(out_types),
|
||||
std::move(to_py),
|
||||
std::move(from_py),
|
||||
std::move(ch_factories),
|
||||
capacity
|
||||
);
|
||||
self.add(std::move(name), std::move(node));
|
||||
},
|
||||
nb::arg("name"), nb::arg("callable"),
|
||||
nb::arg("inputs") = std::vector<std::string>{},
|
||||
nb::arg("outputs") = std::vector<std::string>{},
|
||||
nb::arg("capacity") = 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)
|
||||
|
||||
// read(node, out_idx=0) — blocking pop from a C++ node's output port into Python
|
||||
.def("read", [](Net& self, const std::string& node, std::size_t out_idx) {
|
||||
ensure_converters(self);
|
||||
return self.read(node, out_idx);
|
||||
}, nb::arg("node"), nb::arg("out_idx") = 0)
|
||||
|
||||
// write(node, in_idx, value) — push a Python value into a node's input port
|
||||
.def("write", [](Net& self, const std::string& node,
|
||||
std::size_t in_idx, nb::object value) {
|
||||
ensure_converters(self);
|
||||
self.write(node, in_idx, std::move(value));
|
||||
}, nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mkdocs>=1.5
|
||||
mkdocs-material>=9.5
|
||||
pymdown-extensions>=10.0
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
</urlset>
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
// network.cpp — orchestrator/watchdog implementation details.
|
||||
// Most of the Network class is header-only (template-heavy).
|
||||
// Non-template implementation lives here once the watchdog grows
|
||||
// beyond the stub in network.hpp.
|
||||
|
||||
#include <kpn/network.hpp>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user