diff --git a/.github/workflows/site-checks.yml b/.github/workflows/site-checks.yml
new file mode 100644
index 0000000..9bb68db
--- /dev/null
+++ b/.github/workflows/site-checks.yml
@@ -0,0 +1,20 @@
+name: Site checks
+
+on:
+ pull_request:
+ push:
+ branches: [master]
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Check landing page structure and links
+ run: python3 scripts/check_site.py
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b40d457
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# NILMTK ecosystem site
+
+This repository publishes the canonical NILMTK ecosystem router at
+ . It explains which repository to use, the supported
+installation paths, the container policy, citation guidance, and the route from
+a model contribution to a leaderboard result.
+
+The site is deliberately dependency-free: semantic HTML, one stylesheet, and a
+small progressive-enhancement script. Existing generated core API documentation
+under `nilmtk/` remains separate from the landing page.
+
+## Preview
+
+```bash
+python3 -m http.server 8000
+```
+
+Then open .
+
+## Verify
+
+```bash
+python3 scripts/check_site.py
+```
+
+The check rejects duplicate IDs, broken local assets and fragment links,
+insecure public URLs, missing canonical repository or citation links, and
+accidental reintroduction of the retired Bootstrap/jQuery stack.
+
+## Editing policy
+
+- Keep this page a router; detailed package APIs belong in their repositories.
+- Keep one recommended Python path and one general onboarding image.
+- Treat NILMbench's CPU and CUDA images as scientific runtimes, not per-model
+ development images.
+- Update citation guidance only when the canonical repository papers change.
+- Verify commands against the target repository before publishing them here.
diff --git a/css/landing-page.css b/css/landing-page.css
index 0f686dc..f085a15 100644
--- a/css/landing-page.css
+++ b/css/landing-page.css
@@ -1,165 +1,1625 @@
-body, html {
- height: 100%;
- width: 100%;
-}
-
-body, h1, h2, h3, h4, h5, h6 {
- font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
- font-weight: 700;
-}
-
-.lead {
- font-size: 18px;
- font-weight: 400;
-}
-
-.intro-header {
- padding-top: 50px; /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */
- padding-bottom: 50px;
- color: #f8f8f8;
- background: url(../img/intro-bg.jpg) no-repeat center center;
- background-size: cover;
- text-align: center;
-}
-
-.intro-message {
- position: relative;
- padding-top: 20%;
- padding-bottom: 20%;
-}
-
-.intro-message > h1 {
- margin: 0;
- font-size: 5em;
- text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
-}
-
-.intro-divider {
- width: 400px;
- border-top: 1px solid #f8f8f8;
- border-bottom: 1px solid rgba(0, 0, 0, 0.2);
-}
-
-.intro-message > h3 {
- text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
-}
-
-@media (max-width: 767px) {
- .intro-message {
- padding-bottom: 15%;
- }
-
- .intro-message > h1 {
- font-size: 3em;
- }
-
- ul.intro-social-buttons > li {
- display: block;
- margin-bottom: 20px;
- padding: 0;
- }
-
- ul.intro-social-buttons > li:last-child {
- margin-bottom: 0;
- }
-
- .intro-divider {
- width: 100%;
- }
-}
-
-.network-name {
- text-transform: uppercase;
- font-size: 14px;
- font-weight: 400;
- letter-spacing: 2px;
-}
-
-.content-section-a {
- background-color: #f8f8f8;
- padding: 50px 0;
-}
-
-.content-section-b {
- border-top: 1px solid #e7e7e7;
- border-bottom: 1px solid #e7e7e7;
- padding: 50px 0;
-}
-
-.section-heading {
- margin-bottom: 30px;
-}
-
-.section-heading-spacer {
- border-top: 3px solid #e7e7e7;
- width: 200px;
- float: left;
-}
-
-.banner {
- padding: 20px 0;
- color: #f8f8f8;
- background: url(../img/banner-bg.jpg) no-repeat center center;
- background-size: cover;
- bottom:0px;
-
-}
-
-.banner h2 {
- margin: 0;
- text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
- font-size: 3em;
-}
-
-.banner ul {
- margin-bottom: 0;
-}
-
-.banner-social-buttons {
- float: right;
- margin-top: 0;
-}
-
-@media (max-width: 1199px) {
- ul.banner-social-buttons {
- float: left;
- margin-top: 15px;
- }
-}
-
-@media (max-width: 767px) {
- .banner h2 {
- margin: 0;
- text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6);
- font-size: 3em;
- }
-
- ul.banner-social-buttons > li {
- display: block;
- margin-bottom: 20px;
- padding: 0;
- }
-
- ul.banner-social-buttons > li:last-child {
- margin-bottom: 0;
- }
-}
-
-footer {
- background-color: #f8f8f8;
- padding: 50px 0;
-}
-
-p.copyright {
- margin: 15px 0 0;
-}
-
-.img-circular{
- width: 116px;
- height: 116px;
- background-size: cover;
- display: block;
- border-radius: 100px;
- -webkit-border-radius: 100px;
- -moz-border-radius: 100px;
-}
\ No newline at end of file
+:root {
+ --ink: #14231f;
+ --ink-soft: #41524d;
+ --paper: #f4f1e8;
+ --paper-deep: #e9e4d7;
+ --surface: #fbfaf6;
+ --forest: #173c33;
+ --forest-deep: #0e2a24;
+ --green: #0d6a54;
+ --green-bright: #41c99c;
+ --amber: #e49a3a;
+ --blue: #4c88b7;
+ --rose: #c86a61;
+ --line: rgba(20, 35, 31, 0.16);
+ --line-light: rgba(255, 255, 255, 0.18);
+ --shadow: 0 26px 70px rgba(19, 47, 39, 0.11);
+ --sans: "Manrope", "Helvetica Neue", Arial, sans-serif;
+ --serif: "Newsreader", Georgia, serif;
+ --mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
+ --shell: min(1180px, calc(100vw - 48px));
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 90px;
+}
+
+body {
+ margin: 0;
+ color: var(--ink);
+ background: var(--paper);
+ font-family: var(--sans);
+ font-size: 16px;
+ line-height: 1.65;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+}
+
+body::before {
+ position: fixed;
+ z-index: 100;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 3px;
+ background: linear-gradient(90deg, var(--green), var(--green-bright) 48%, var(--amber));
+ content: "";
+}
+
+a {
+ color: inherit;
+ text-underline-offset: 0.18em;
+}
+
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+
+button {
+ font: inherit;
+}
+
+:focus-visible {
+ outline: 3px solid var(--amber);
+ outline-offset: 4px;
+}
+
+.skip-link {
+ position: fixed;
+ z-index: 1000;
+ top: 12px;
+ left: 12px;
+ padding: 10px 15px;
+ color: white;
+ background: var(--forest-deep);
+ font-weight: 700;
+ transform: translateY(-160%);
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+.shell {
+ width: var(--shell);
+ margin-inline: auto;
+}
+
+.section {
+ padding: 112px 0;
+}
+
+.section-dark {
+ color: #f7f5ed;
+ background: var(--forest-deep);
+}
+
+.section-paper {
+ background: var(--surface);
+}
+
+.eyebrow {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0 0 20px;
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ line-height: 1.4;
+ text-transform: uppercase;
+}
+
+.eyebrow > span {
+ display: block;
+ width: 25px;
+ height: 1px;
+ background: currentColor;
+}
+
+.eyebrow-light {
+ color: var(--green-bright);
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1,
+h2 {
+ font-family: var(--serif);
+ font-weight: 500;
+ letter-spacing: -0.035em;
+}
+
+h1 {
+ margin-bottom: 28px;
+ font-size: clamp(3.5rem, 7.7vw, 6.85rem);
+ line-height: 0.93;
+}
+
+h1 em {
+ color: var(--green);
+ font-style: italic;
+ font-weight: 500;
+}
+
+h2 {
+ margin-bottom: 0;
+ font-size: clamp(2.65rem, 5vw, 4.7rem);
+ line-height: 0.98;
+}
+
+h3 {
+ font-size: 1.1rem;
+ line-height: 1.35;
+}
+
+.site-header {
+ position: sticky;
+ z-index: 200;
+ top: 0;
+ border-bottom: 1px solid transparent;
+ background: rgba(244, 241, 232, 0.88);
+ backdrop-filter: blur(18px);
+ transition: border-color 180ms ease, box-shadow 180ms ease;
+}
+
+.site-header.is-scrolled {
+ border-color: var(--line);
+ box-shadow: 0 8px 25px rgba(20, 35, 31, 0.04);
+}
+
+.header-inner {
+ display: flex;
+ min-height: 76px;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ color: var(--forest);
+ font-size: 0.92rem;
+ font-weight: 800;
+ letter-spacing: 0.13em;
+ text-decoration: none;
+}
+
+.brand-mark {
+ width: 43px;
+ overflow: visible;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 2.4;
+}
+
+.primary-nav {
+ display: flex;
+ align-items: center;
+ gap: 31px;
+}
+
+.primary-nav a {
+ color: var(--ink-soft);
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.015em;
+ text-decoration: none;
+}
+
+.primary-nav a:hover {
+ color: var(--green);
+}
+
+.primary-nav .nav-github {
+ padding: 9px 15px;
+ border: 1px solid var(--ink);
+ border-radius: 999px;
+ color: var(--ink);
+}
+
+.primary-nav .nav-github:hover {
+ color: white;
+ background: var(--forest);
+}
+
+.menu-button {
+ display: none;
+}
+
+.hero {
+ position: relative;
+ overflow: hidden;
+ padding: 94px 0 80px;
+}
+
+.hero::before {
+ position: absolute;
+ z-index: -1;
+ top: -190px;
+ right: -240px;
+ width: 660px;
+ height: 660px;
+ border: 1px solid rgba(13, 106, 84, 0.13);
+ border-radius: 50%;
+ box-shadow: 0 0 0 80px rgba(13, 106, 84, 0.035), 0 0 0 160px rgba(13, 106, 84, 0.02);
+ content: "";
+}
+
+.hero-grid {
+ display: grid;
+ align-items: end;
+ gap: 58px;
+ grid-template-columns: minmax(0, 1.35fr) minmax(350px, 0.65fr);
+}
+
+.hero-copy {
+ min-width: 0;
+}
+
+.hero-lede {
+ max-width: 670px;
+ margin-bottom: 34px;
+ color: var(--ink-soft);
+ font-size: clamp(1.04rem, 1.55vw, 1.25rem);
+ line-height: 1.75;
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 65px;
+}
+
+.button {
+ display: inline-flex;
+ min-height: 51px;
+ align-items: center;
+ justify-content: center;
+ gap: 18px;
+ padding: 12px 20px;
+ border: 1px solid var(--forest);
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ text-decoration: none;
+ transition: transform 160ms ease, background-color 160ms ease, color 160ms ease;
+}
+
+.button:hover {
+ transform: translateY(-2px);
+}
+
+.button-primary {
+ color: white;
+ background: var(--forest);
+}
+
+.button-primary:hover {
+ background: var(--green);
+}
+
+.button-secondary {
+ color: var(--forest);
+ background: transparent;
+}
+
+.button-secondary:hover {
+ color: white;
+ background: var(--forest);
+}
+
+.button-light {
+ border-color: var(--surface);
+ color: var(--forest-deep);
+ background: var(--surface);
+}
+
+.hero-facts {
+ display: grid;
+ max-width: 720px;
+ margin: 0;
+ padding: 0;
+ border-top: 1px solid var(--line);
+ list-style: none;
+ grid-template-columns: repeat(3, 1fr);
+}
+
+.hero-facts li {
+ padding: 18px 22px 0 0;
+}
+
+.hero-facts li + li {
+ padding-left: 22px;
+ border-left: 1px solid var(--line);
+}
+
+.hero-facts strong,
+.hero-facts span {
+ display: block;
+}
+
+.hero-facts strong {
+ margin-bottom: 3px;
+ color: var(--forest);
+ font-size: 0.82rem;
+}
+
+.hero-facts span {
+ color: var(--ink-soft);
+ font-size: 0.68rem;
+}
+
+.signal-panel {
+ position: relative;
+ margin-bottom: 2px;
+ padding: 24px 25px 20px;
+ border: 1px solid rgba(20, 35, 31, 0.22);
+ border-radius: 3px;
+ background: rgba(251, 250, 246, 0.58);
+ box-shadow: var(--shadow);
+}
+
+.signal-panel::before {
+ position: absolute;
+ z-index: -1;
+ right: -16px;
+ bottom: -16px;
+ width: 68%;
+ height: 54%;
+ border: 1px solid rgba(13, 106, 84, 0.26);
+ border-top: 0;
+ border-left: 0;
+ content: "";
+}
+
+.signal-heading,
+.signal-legend,
+.signal-note {
+ display: flex;
+ color: var(--ink-soft);
+ font-family: var(--mono);
+ font-size: 0.61rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.signal-heading {
+ justify-content: space-between;
+ padding-bottom: 13px;
+ border-bottom: 1px solid var(--line);
+}
+
+.signal {
+ display: block;
+ width: 100%;
+ height: auto;
+ margin: 12px 0 2px;
+ overflow: visible;
+}
+
+.grid-lines {
+ fill: none;
+ stroke: rgba(20, 35, 31, 0.07);
+ stroke-width: 1;
+}
+
+.aggregate-line,
+.appliance-line {
+ fill: none;
+ stroke-linecap: square;
+ stroke-linejoin: miter;
+ vector-effect: non-scaling-stroke;
+}
+
+.aggregate-line {
+ stroke: var(--forest-deep);
+ stroke-width: 3;
+}
+
+.appliance-line {
+ stroke-width: 2.3;
+}
+
+.line-one { stroke: var(--green); }
+.line-two { stroke: var(--blue); }
+.line-three { stroke: var(--amber); }
+
+.signal-legend {
+ flex-wrap: wrap;
+ gap: 13px;
+ margin-bottom: 22px;
+ text-transform: none;
+}
+
+.signal-legend span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.aggregate-dot { color: var(--forest-deep); }
+.one-dot { color: var(--green); }
+.two-dot { color: var(--blue); }
+.three-dot { color: var(--amber); }
+
+.signal-note {
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 0 0;
+ border-top: 1px solid var(--line);
+ color: var(--green);
+}
+
+.signal-note svg {
+ width: 70px;
+ fill: none;
+ stroke: var(--green);
+ stroke-width: 1;
+}
+
+.router {
+ background: var(--surface);
+}
+
+.section-intro {
+ margin-bottom: 65px;
+}
+
+.split-intro {
+ display: grid;
+ gap: 80px;
+ grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.65fr);
+}
+
+.split-intro > p {
+ align-self: end;
+ margin: 0 0 4px;
+ color: var(--ink-soft);
+ font-size: 1.04rem;
+ line-height: 1.85;
+}
+
+.split-intro > p a {
+ color: var(--green);
+}
+
+.repo-router {
+ margin: 0;
+ padding: 0;
+ border-top: 1px solid var(--ink);
+ list-style: none;
+}
+
+.repo-row {
+ position: relative;
+ display: grid;
+ min-height: 178px;
+ align-items: center;
+ gap: 28px;
+ padding: 30px 0;
+ border-bottom: 1px solid var(--line);
+ grid-template-columns: 55px 190px minmax(0, 1fr) 155px;
+ transition: padding 180ms ease, background-color 180ms ease;
+}
+
+.repo-row::before {
+ position: absolute;
+ top: 0;
+ bottom: -1px;
+ left: -18px;
+ width: 4px;
+ background: var(--row-accent, var(--green));
+ content: "";
+ opacity: 0;
+ transform: scaleY(0.2);
+ transition: opacity 180ms ease, transform 180ms ease;
+}
+
+.repo-row:hover {
+ padding-right: 16px;
+ padding-left: 16px;
+ background: rgba(13, 106, 84, 0.035);
+}
+
+.repo-row:hover::before {
+ opacity: 1;
+ transform: scaleY(1);
+}
+
+.repo-core { --row-accent: var(--green); }
+.repo-metadata { --row-accent: var(--amber); }
+.repo-models { --row-accent: var(--blue); }
+.repo-bench { --row-accent: var(--rose); }
+
+.repo-number {
+ color: var(--ink-soft);
+ font-family: var(--mono);
+ font-size: 0.72rem;
+}
+
+.repo-kicker {
+ margin-bottom: 4px;
+ color: var(--ink-soft);
+ font-family: var(--mono);
+ font-size: 0.63rem;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+}
+
+.repo-name h3 {
+ margin: 0;
+ color: var(--forest);
+ font-family: var(--serif);
+ font-size: 1.85rem;
+ font-weight: 600;
+ letter-spacing: -0.025em;
+}
+
+.repo-purpose strong {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--ink);
+ font-size: 0.93rem;
+}
+
+.repo-purpose p {
+ max-width: 585px;
+ margin: 0;
+ color: var(--ink-soft);
+ font-size: 0.85rem;
+}
+
+.repo-links {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 7px;
+}
+
+.repo-links a,
+.text-link,
+.contribution-flow a {
+ color: var(--green);
+ font-size: 0.73rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.repo-links a:hover,
+.text-link:hover,
+.contribution-flow a:hover {
+ text-decoration: underline;
+}
+
+.architecture {
+ position: relative;
+ overflow: hidden;
+}
+
+.architecture::after {
+ position: absolute;
+ top: -230px;
+ right: -220px;
+ width: 600px;
+ height: 600px;
+ border: 1px solid rgba(65, 201, 156, 0.15);
+ border-radius: 50%;
+ content: "";
+}
+
+.architecture-grid {
+ display: grid;
+ align-items: center;
+ gap: 75px;
+ grid-template-columns: minmax(280px, 0.63fr) minmax(0, 1.37fr);
+}
+
+.architecture-copy {
+ position: relative;
+ z-index: 2;
+}
+
+.architecture-copy h2 {
+ margin-bottom: 28px;
+}
+
+.architecture-copy > p:not(.eyebrow) {
+ margin-bottom: 25px;
+ color: rgba(247, 245, 237, 0.7);
+ font-size: 0.94rem;
+}
+
+.text-link-light {
+ color: var(--green-bright);
+}
+
+.pipeline {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ align-items: stretch;
+ gap: 8px;
+ padding-top: 65px;
+ grid-template-columns: 1fr 20px 1fr 20px 1fr 20px 1fr;
+}
+
+.metadata-rail {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ display: grid;
+ height: 35px;
+ align-items: center;
+ border: 1px solid rgba(65, 201, 156, 0.48);
+ color: var(--green-bright);
+ font-family: var(--mono);
+ font-size: 0.61rem;
+ letter-spacing: 0.08em;
+ text-align: center;
+ text-transform: uppercase;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+}
+
+.metadata-rail span {
+ grid-column: 1 / -1;
+}
+
+.metadata-rail i {
+ position: absolute;
+ top: 34px;
+ height: 31px;
+ border-left: 1px dashed rgba(65, 201, 156, 0.35);
+}
+
+.metadata-rail i:nth-of-type(1) { left: 37%; }
+.metadata-rail i:nth-of-type(2) { left: 63%; }
+.metadata-rail i:nth-of-type(3) { left: 88%; }
+
+.pipeline-step {
+ display: flex;
+ min-height: 145px;
+ justify-content: flex-end;
+ flex-direction: column;
+ padding: 17px;
+ border: 1px solid var(--line-light);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.pipeline-step > span {
+ margin-bottom: auto;
+ color: var(--green-bright);
+ font-family: var(--mono);
+ font-size: 0.62rem;
+}
+
+.pipeline-step strong,
+.pipeline-step small {
+ display: block;
+}
+
+.pipeline-step strong {
+ margin-bottom: 5px;
+ font-size: 0.81rem;
+}
+
+.pipeline-step small {
+ color: rgba(247, 245, 237, 0.55);
+ font-size: 0.59rem;
+}
+
+.pipeline-output {
+ border-color: rgba(65, 201, 156, 0.65);
+ background: rgba(65, 201, 156, 0.07);
+}
+
+.pipeline-arrow {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--green-bright);
+ font-family: var(--mono);
+}
+
+.start {
+ background: var(--paper);
+}
+
+.workbench {
+ display: grid;
+ min-height: 430px;
+ margin-bottom: 56px;
+ overflow: hidden;
+ border: 1px solid var(--forest);
+ border-radius: 4px;
+ box-shadow: var(--shadow);
+ grid-template-columns: 250px minmax(0, 1fr);
+}
+
+.workbench-tabs {
+ display: flex;
+ flex-direction: column;
+ background: var(--surface);
+}
+
+.workbench-tabs button {
+ position: relative;
+ display: flex;
+ min-height: 107px;
+ align-items: flex-start;
+ justify-content: center;
+ flex-direction: column;
+ padding: 20px 25px;
+ border: 0;
+ border-right: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ color: var(--ink-soft);
+ background: transparent;
+ cursor: pointer;
+ font-size: 0.83rem;
+ font-weight: 700;
+ text-align: left;
+}
+
+.workbench-tabs button:last-child {
+ border-bottom: 0;
+}
+
+.workbench-tabs button span {
+ display: block;
+ margin-bottom: 5px;
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.56rem;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.workbench-tabs button[aria-selected="true"] {
+ border-right-color: var(--forest-deep);
+ color: white;
+ background: var(--forest-deep);
+}
+
+.workbench-tabs button[aria-selected="true"] span {
+ color: var(--green-bright);
+}
+
+.workbench-tabs button[aria-selected="true"]::after {
+ position: absolute;
+ top: 50%;
+ right: -9px;
+ width: 17px;
+ height: 17px;
+ background: var(--forest-deep);
+ content: "";
+ transform: translateY(-50%) rotate(45deg);
+}
+
+.workbench-code {
+ min-width: 0;
+ color: #e9f1ed;
+ background: #102923;
+}
+
+.code-panel {
+ height: 100%;
+}
+
+.code-heading {
+ display: flex;
+ height: 59px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 25px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ color: rgba(233, 241, 237, 0.55);
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ letter-spacing: 0.04em;
+}
+
+.code-heading button {
+ padding: 5px 10px;
+ border: 1px solid rgba(255, 255, 255, 0.23);
+ border-radius: 3px;
+ color: rgba(233, 241, 237, 0.78);
+ background: transparent;
+ cursor: pointer;
+ font-family: var(--mono);
+ font-size: 0.58rem;
+}
+
+.code-heading button:hover {
+ border-color: var(--green-bright);
+ color: var(--green-bright);
+}
+
+.code-panel pre {
+ margin: 0;
+ padding: 54px 44px;
+ overflow: auto;
+ font-family: var(--mono);
+ font-size: clamp(0.7rem, 1.2vw, 0.86rem);
+ line-height: 2;
+ white-space: pre-wrap;
+}
+
+.code-panel code {
+ font-family: inherit;
+}
+
+.code-panel .comment {
+ color: #75988c;
+}
+
+.code-panel .string {
+ color: #76d6b4;
+}
+
+.install-notes {
+ display: grid;
+ border-top: 1px solid var(--ink);
+ border-bottom: 1px solid var(--line);
+ grid-template-columns: repeat(3, 1fr);
+}
+
+.install-notes article {
+ display: flex;
+ gap: 18px;
+ padding: 30px 28px 30px 0;
+}
+
+.install-notes article + article {
+ padding-left: 28px;
+ border-left: 1px solid var(--line);
+}
+
+.note-index {
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.68rem;
+}
+
+.install-notes h3 {
+ margin-bottom: 8px;
+ font-size: 0.84rem;
+}
+
+.install-notes p {
+ margin: 0;
+ color: var(--ink-soft);
+ font-size: 0.76rem;
+}
+
+.citation {
+ border-top: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+}
+
+.citation-grid {
+ display: grid;
+ align-items: start;
+ gap: 80px;
+ grid-template-columns: minmax(270px, 0.55fr) minmax(0, 1.45fr);
+}
+
+.citation-intro {
+ position: sticky;
+ top: 110px;
+}
+
+.citation-intro h2 {
+ margin-bottom: 27px;
+}
+
+.citation-intro > p:not(.eyebrow) {
+ margin-bottom: 24px;
+ color: var(--ink-soft);
+ font-size: 0.9rem;
+}
+
+.citation-table-wrap {
+ min-width: 0;
+}
+
+.citation-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.78rem;
+}
+
+.citation-table th {
+ padding: 0 16px 15px;
+ border-bottom: 1px solid var(--ink);
+ color: var(--ink-soft);
+ font-family: var(--mono);
+ font-size: 0.57rem;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ text-align: left;
+ text-transform: uppercase;
+}
+
+.citation-table th:first-child,
+.citation-table td:first-child {
+ padding-left: 0;
+}
+
+.citation-table th:last-child,
+.citation-table td:last-child {
+ padding-right: 0;
+}
+
+.citation-table td {
+ padding: 24px 16px;
+ border-bottom: 1px solid var(--line);
+ vertical-align: top;
+}
+
+.citation-table td:first-child {
+ width: 38%;
+}
+
+.citation-table td:nth-child(2) {
+ width: 38%;
+ color: var(--ink-soft);
+}
+
+.citation-table td strong,
+.citation-table td span {
+ display: block;
+}
+
+.citation-table td strong {
+ margin-bottom: 5px;
+ font-size: 0.78rem;
+}
+
+.citation-table td span {
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.57rem;
+ letter-spacing: 0.04em;
+}
+
+.citation-table td a {
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.68rem;
+}
+
+.citation-footnote {
+ margin: 24px 0 0;
+ padding: 15px 18px;
+ border-left: 3px solid var(--amber);
+ color: var(--ink-soft);
+ background: var(--paper);
+ font-size: 0.72rem;
+}
+
+.contribute {
+ padding-bottom: 0;
+}
+
+.contribution-flow {
+ display: grid;
+ margin: 0 0 90px;
+ padding: 0;
+ border-top: 1px solid var(--ink);
+ list-style: none;
+ grid-template-columns: repeat(5, 1fr);
+}
+
+.contribution-flow li {
+ display: flex;
+ min-height: 280px;
+ align-items: flex-start;
+ flex-direction: column;
+ padding: 25px 20px 20px;
+ border-bottom: 1px solid var(--line);
+}
+
+.contribution-flow li:first-child {
+ padding-left: 0;
+}
+
+.contribution-flow li + li {
+ border-left: 1px solid var(--line);
+}
+
+.contribution-flow li > span {
+ margin-bottom: 42px;
+ color: var(--green);
+ font-family: var(--mono);
+ font-size: 0.63rem;
+}
+
+.contribution-flow h3 {
+ margin-bottom: 9px;
+ font-size: 0.9rem;
+}
+
+.contribution-flow p {
+ margin-bottom: 20px;
+ color: var(--ink-soft);
+ font-size: 0.72rem;
+}
+
+.contribution-flow a {
+ margin-top: auto;
+ font-size: 0.66rem;
+}
+
+.contribute-cta {
+ display: grid;
+ align-items: end;
+ gap: 80px;
+ padding: 62px 68px;
+ color: white;
+ background: var(--green);
+ grid-template-columns: minmax(0, 1fr) auto;
+}
+
+.contribute-cta h2 {
+ max-width: 700px;
+ margin-bottom: 20px;
+ font-size: clamp(2.5rem, 5vw, 4.5rem);
+}
+
+.contribute-cta p:not(.eyebrow) {
+ max-width: 680px;
+ margin: 0;
+ color: rgba(255, 255, 255, 0.72);
+ font-size: 0.85rem;
+}
+
+.site-footer {
+ padding: 82px 0 28px;
+ color: rgba(247, 245, 237, 0.72);
+ background: var(--forest-deep);
+}
+
+.footer-grid {
+ display: grid;
+ gap: 70px;
+ padding-bottom: 65px;
+ grid-template-columns: minmax(250px, 0.75fr) minmax(0, 1.25fr);
+}
+
+.footer-brand {
+ margin-bottom: 18px;
+ color: white;
+}
+
+.footer-grid > div:first-child > p {
+ max-width: 410px;
+ font-size: 0.8rem;
+}
+
+.footer-links {
+ display: grid;
+ gap: 70px;
+ grid-template-columns: repeat(2, 1fr);
+}
+
+.footer-links > div {
+ display: flex;
+ flex-direction: column;
+ gap: 9px;
+}
+
+.footer-links strong {
+ margin-bottom: 7px;
+ color: var(--green-bright);
+ font-family: var(--mono);
+ font-size: 0.62rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.footer-links a {
+ width: fit-content;
+ color: rgba(247, 245, 237, 0.72);
+ font-size: 0.76rem;
+ text-decoration: none;
+}
+
+.footer-links a:hover {
+ color: white;
+}
+
+.footer-bottom {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 22px;
+ border-top: 1px solid var(--line-light);
+ color: rgba(247, 245, 237, 0.46);
+ font-family: var(--mono);
+ font-size: 0.56rem;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+@media (max-width: 1050px) {
+ .hero-grid {
+ align-items: center;
+ grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
+ }
+
+ .repo-row {
+ grid-template-columns: 45px 165px minmax(0, 1fr) 135px;
+ }
+
+ .architecture-grid {
+ gap: 50px;
+ grid-template-columns: 0.55fr 1.45fr;
+ }
+
+ .pipeline {
+ gap: 4px;
+ grid-template-columns: 1fr 14px 1fr 14px 1fr 14px 1fr;
+ }
+
+ .pipeline-step {
+ padding: 12px;
+ }
+
+ .contribution-flow {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .contribution-flow li:nth-child(4) {
+ padding-left: 0;
+ border-left: 0;
+ }
+}
+
+@media (max-width: 820px) {
+ :root {
+ --shell: min(100% - 34px, 720px);
+ }
+
+ .section {
+ padding: 82px 0;
+ }
+
+ .menu-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 7px 0;
+ border: 0;
+ color: var(--forest);
+ background: transparent;
+ cursor: pointer;
+ font-size: 0.7rem;
+ font-weight: 700;
+ }
+
+ .menu-icon,
+ .menu-icon::before,
+ .menu-icon::after {
+ display: block;
+ width: 19px;
+ height: 1px;
+ background: var(--forest);
+ content: "";
+ }
+
+ .menu-icon {
+ position: relative;
+ }
+
+ .menu-icon::before {
+ position: absolute;
+ top: -5px;
+ }
+
+ .menu-icon::after {
+ position: absolute;
+ top: 5px;
+ }
+
+ .primary-nav {
+ position: absolute;
+ top: 76px;
+ right: 0;
+ left: 0;
+ display: none;
+ align-items: stretch;
+ flex-direction: column;
+ gap: 0;
+ padding: 12px 17px 22px;
+ border-bottom: 1px solid var(--line);
+ background: var(--paper);
+ box-shadow: 0 20px 30px rgba(20, 35, 31, 0.08);
+ }
+
+ .primary-nav.is-open {
+ display: flex;
+ }
+
+ .primary-nav a {
+ padding: 13px 0;
+ border-bottom: 1px solid var(--line);
+ }
+
+ .primary-nav .nav-github {
+ width: fit-content;
+ margin-top: 14px;
+ padding: 8px 13px;
+ border-bottom: 1px solid var(--ink);
+ }
+
+ .hero {
+ padding: 70px 0 66px;
+ }
+
+ .hero-grid,
+ .split-intro,
+ .architecture-grid,
+ .citation-grid,
+ .contribute-cta,
+ .footer-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero-grid {
+ gap: 62px;
+ }
+
+ .hero-actions {
+ margin-bottom: 45px;
+ }
+
+ .signal-panel {
+ width: min(100%, 560px);
+ }
+
+ .split-intro {
+ gap: 28px;
+ }
+
+ .split-intro > p {
+ max-width: 620px;
+ }
+
+ .repo-row {
+ gap: 14px 20px;
+ grid-template-columns: 40px minmax(0, 1fr);
+ }
+
+ .repo-number {
+ grid-row: 1 / span 3;
+ }
+
+ .repo-purpose,
+ .repo-links {
+ grid-column: 2;
+ }
+
+ .repo-links {
+ flex-direction: row;
+ gap: 16px;
+ }
+
+ .architecture-grid {
+ gap: 60px;
+ }
+
+ .architecture-copy {
+ max-width: 600px;
+ }
+
+ .citation-intro {
+ position: static;
+ }
+
+ .citation-grid {
+ gap: 55px;
+ }
+
+ .workbench {
+ min-height: auto;
+ grid-template-columns: 1fr;
+ }
+
+ .workbench-tabs {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .workbench-tabs button {
+ min-height: 86px;
+ padding: 14px;
+ border-right: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ }
+
+ .workbench-tabs button[aria-selected="true"]::after {
+ top: auto;
+ right: auto;
+ bottom: -8px;
+ left: 50%;
+ transform: translateX(-50%) rotate(45deg);
+ }
+
+ .code-panel pre {
+ min-height: 330px;
+ padding: 44px 30px;
+ }
+
+ .install-notes {
+ grid-template-columns: 1fr;
+ }
+
+ .install-notes article,
+ .install-notes article + article {
+ padding: 24px 0;
+ border-left: 0;
+ }
+
+ .install-notes article + article {
+ border-top: 1px solid var(--line);
+ }
+
+ .contribution-flow {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .contribution-flow li:nth-child(3),
+ .contribution-flow li:nth-child(5) {
+ padding-left: 0;
+ border-left: 0;
+ }
+
+ .contribution-flow li:nth-child(4) {
+ padding-left: 20px;
+ border-left: 1px solid var(--line);
+ }
+
+ .contribute-cta {
+ gap: 35px;
+ padding: 50px 40px;
+ }
+}
+
+@media (max-width: 560px) {
+ :root {
+ --shell: calc(100% - 28px);
+ }
+
+ body {
+ font-size: 15px;
+ }
+
+ .section {
+ padding: 68px 0;
+ }
+
+ .header-inner {
+ min-height: 68px;
+ }
+
+ .primary-nav {
+ top: 68px;
+ }
+
+ .brand-mark {
+ width: 38px;
+ }
+
+ .menu-label {
+ display: none;
+ }
+
+ .hero {
+ padding-top: 54px;
+ }
+
+ h1 {
+ font-size: clamp(3.05rem, 16vw, 4.7rem);
+ }
+
+ h2 {
+ font-size: clamp(2.4rem, 12vw, 3.7rem);
+ }
+
+ .hero-lede {
+ font-size: 0.98rem;
+ }
+
+ .hero-actions {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .button {
+ justify-content: space-between;
+ }
+
+ .hero-facts {
+ grid-template-columns: 1fr;
+ }
+
+ .hero-facts li,
+ .hero-facts li + li {
+ padding: 13px 0;
+ border-left: 0;
+ }
+
+ .hero-facts li + li {
+ border-top: 1px solid var(--line);
+ }
+
+ .signal-panel {
+ padding: 18px 16px;
+ }
+
+ .signal-note svg {
+ width: 42px;
+ }
+
+ .repo-row {
+ padding: 25px 0;
+ }
+
+ .repo-row:hover {
+ padding-right: 8px;
+ padding-left: 8px;
+ }
+
+ .repo-purpose strong {
+ font-size: 0.86rem;
+ }
+
+ .pipeline {
+ padding-top: 55px;
+ grid-template-columns: 1fr;
+ }
+
+ .metadata-rail {
+ height: 32px;
+ }
+
+ .metadata-rail i {
+ display: none;
+ }
+
+ .pipeline-arrow {
+ height: 22px;
+ transform: rotate(90deg);
+ }
+
+ .pipeline-step {
+ min-height: 96px;
+ }
+
+ .workbench-tabs {
+ overflow-x: auto;
+ grid-template-columns: repeat(4, minmax(120px, 1fr));
+ }
+
+ .workbench-tabs button {
+ min-height: 78px;
+ }
+
+ .code-panel pre {
+ min-height: 350px;
+ padding: 34px 20px;
+ font-size: 0.66rem;
+ line-height: 1.85;
+ }
+
+ .citation-table {
+ display: block;
+ }
+
+ .citation-table thead {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ }
+
+ .citation-table tbody,
+ .citation-table tr,
+ .citation-table td {
+ display: block;
+ width: 100% !important;
+ }
+
+ .citation-table tr {
+ padding: 25px 0;
+ border-bottom: 1px solid var(--line);
+ }
+
+ .citation-table td,
+ .citation-table td:first-child,
+ .citation-table td:last-child {
+ padding: 6px 0;
+ border: 0;
+ }
+
+ .contribution-flow {
+ grid-template-columns: 1fr;
+ }
+
+ .contribution-flow li,
+ .contribution-flow li:first-child,
+ .contribution-flow li:nth-child(3),
+ .contribution-flow li:nth-child(4),
+ .contribution-flow li:nth-child(5) {
+ min-height: 230px;
+ padding: 24px 0;
+ border-left: 0;
+ }
+
+ .contribution-flow li > span {
+ margin-bottom: 27px;
+ }
+
+ .contribute-cta {
+ padding: 42px 25px;
+ }
+
+ .footer-grid {
+ gap: 42px;
+ }
+
+ .footer-links {
+ gap: 35px;
+ }
+
+ .footer-bottom {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 8px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/favicon.svg b/favicon.svg
new file mode 100644
index 0000000..9876dc2
--- /dev/null
+++ b/favicon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/index.html b/index.html
index f6ce386..42928df 100644
--- a/index.html
+++ b/index.html
@@ -1,312 +1,359 @@
-
-
-
-
-
-
-
-
-
-
- nilmtk
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Convert and import
- public data sets with two lines of code
-
Importers for REDD, GREEND,
- WikiEnergy, AMPds, iAWE, UK-DALE and more !
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Data set statistics,
- pre-processing and NILM metrics
-
Resample your data, filter out
- erroneous readings, find gaps in your data,
- find proportion of energy submetered,
- calculate F1 score etc etc..
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Compare against benchmark
- disaggregation algorithms
-
NILMTK v0.2 includes a combinatorial
- optimisation NILM algorithm. FHMM and Hart's
- 1985 algorithms coming soon to v0.2. More algorithms
- in the pipeline!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Publications
-
-
-
-
- Nipun Batra, Jack Kelly, Oliver
- Parson, Haimonti Dutta, William
- Knottenbelt, Alex Rogers, Amarjeet
- Singh, Mani Srivastava. NILMTK: An
- Open Source Toolkit for Non-intrusive
- Load Monitoring . In: 5th
- International Conference on Future
- Energy Systems (ACM e-Energy),
- Cambridge, UK. 2014.
- DOI:10.1145/2602044.2602051
- arXiv:1404.3878
- [slides ] [bib ]
- Nipun Batra, Jack Kelly, Oliver
- Parson, Haimonti Dutta, William
- Knottenbelt, Alex Rogers, Amarjeet
- Singh, Mani Srivastava. NILMTK: An
- Open Source Toolkit for Non-intrusive
- Load Monitoring" . In: NILM
- Workshop, Austin,
- US. 2014
- [pdf ]
- [Presented as a
- 30-minute IPython
- demo of NILMTK v0.1 and a poster.]
-
- Jack Kelly, Nipun Batra, Oliver
- Parson, Haimonti Dutta, William
- Knottenbelt, Alex Rogers, Amarjeet
- Singh, Mani Srivastava. Demo
- Abstract: NILMTK v0.2: A Non-intrusive
- Load Monitoring Toolkit for Large
- Scale Data Sets . In the first ACM
- Workshop On Embedded Systems For
- Energy-Efficient Buildings,
- 2014. DOI:10.1145/2674061.2675024 .
- arXiv:1409.5908
- [Won best demo award ]
- [bib ]
- [Presented as an
- IPython demo of v0.2
- and a poster ]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Core Development Team
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NILMTK · Open infrastructure for energy disaggregation
+
+
+ Skip to content
+
+
+
+
+
+
+
+
Open infrastructure for energy disaggregation
+
From household power to reproducible insight.
+
NILMTK is a research ecosystem for loading real energy datasets, resolving their meaning, implementing disaggregation models, and comparing them under frozen protocols.
+
+
+ Python 3.11 supported research path
+ Real data REDD · UK-DALE · REFIT
+ Open source Apache-2.0 ecosystem
+
+
+
+
+
+ aggregate mains
+ power / time
+
+
+ Aggregate power separated into appliance signals
+ A dark aggregate power trace branches into three coloured appliance-level traces.
+
+
+
+
+
+
+
+
+
+
+ mains
+ fridge
+ washer
+ kettle
+
+
+
One measured channel
+
+
Appliance estimates
+
+
+
+
+
+
+
+
+
+
Choose by job
+
Four repositories. Four distinct responsibilities.
+
+
Start with the question you are trying to answer. The packages work together, but each has one canonical job. This separation keeps data semantics, model code, and scientific claims reviewable.
+
+
+
+
+ 01
+
+
+
Load, convert, inspect, and score energy data.
+
Use core for dataset converters, buildings and meters, preprocessing, metrics, and classical baselines.
+
+
+
+
+ 02
+
+
Semantic layer
+
NILM Metadata
+
+
+
Make datasets mean the same thing.
+
Use metadata for the canonical schema, appliance taxonomy, synonyms, meter relationships, and dataset consistency.
+
+
+
+
+ 03
+
+
Model layer
+
nilmtk-contrib
+
+
+
Use or contribute a disaggregation model.
+
Use contrib for maintained classical, TensorFlow, and PyTorch implementations behind shared NILMTK contracts.
+
+
+
+
+ 04
+
+
Evidence layer
+
NILMbench
+
+
+
Reproduce protocols and compare real runs.
+
Use the benchmark for T1/T2/T3 tasks, provenance, CPU smoke tests, CUDA runs, immutable results, and the living leaderboard.
+
+
+
+
+
+
+
+
+
+
+
The system
+
One path from files to claims.
+
NILM Metadata supplies shared meaning across the entire path. Core turns physical files into consistent meters and windows. Contrib supplies models. NILMbench freezes the comparison and publishes only provenance-complete results.
+
Explore the organization ↗
+
+
+
NILM Metadata
+
01 Real datasets REDD · UK-DALE · REFIT
+
→
+
02 NILMTK core meters · windows · metrics
+
→
+
03 Contrib models shared model contracts
+
→
+
04 NILMbench protocols · results · rank
+
+
+
+
+
+
+
+
+
Start clean
+
A supported route, not another dated tutorial.
+
+
Use Python 3.11 and uv . Pick the smallest installation that matches your job. The model install brings core and metadata with it.
+
+
+
+
+
+ Recommended Try a model
+
+
+ Data work Core only
+
+
+ No setup Docker
+
+
+ Research Benchmark
+
+
+
+
+
PyTorch model environment Copy
+
+uv venv --python 3.11
+source .venv/bin/activate
+UV_TORCH_BACKEND=cpu uv pip install \
+ "nilmtk-contrib[torch] @ git+https://github.com/nilmtk/nilmtk-contrib.git"
+python -c "from nilmtk_contrib.torch import Seq2PointTorch; print('ready')"
+
+
+
NILMTK data environment Copy
+
+uv venv --python 3.11
+source .venv/bin/activate
+uv pip install \
+ "nilmtk @ git+https://github.com/nilmtk/nilmtk.git"
+nilmtk-convert --help
+
+
+
General NILMTK environment Copy
+
+git clone https://github.com/nilmtk/nilmtk-contrib.git
+cd nilmtk-contrib
+docker build -t nilmtk-contrib:latest .
+docker run --rm -it nilmtk-contrib:latest bash
+
+
+
Reproducible benchmark runner Copy
+
git clone https://github.com/nilmtk/nilmbench.git
+cd nilmbench
+uv venv --python 3.11
+source .venv/bin/activate
+UV_TORCH_BACKEND=cpu uv pip install ".[benchmark]"
+nilmbench list
+
+
+
+
+
+
+ A
+ Bring licensed data Datasets are not baked into packages or images. Download them from their official custodians, follow their licenses, and mount local NILMTK HDF5 conversions read-only.
+
+
+ B
+ Keep one general image The contrib Dockerfile defines the common starting environment. Model contributions do not create a new container tag or image family.
+
+
+ C
+ Separate benchmark runtimes NILMbench owns pinned CPU-smoke and CUDA-benchmark runtimes. Those images certify results; they are not per-algorithm development images.
+
+
+
+
+
+
+
+
+
Cite precisely
+
Credit the layer your work relies on.
+
There is no single citation that describes every part of the current ecosystem. Cite the infrastructure paper for each layer you use, plus the original model and dataset papers.
+
Copy BibTeX from the repositories ↗
+
+
+
+ If your work uses… Cite DOI
+
+
+ Dataset conversion, meters, preprocessing, or metrics NILMTK core
+ NILMTK: An Open Source Toolkit for Non-intrusive Load Monitoring
+ 10.1145/2602044.2602051
+
+
+ Schema, taxonomy, synonyms, or dataset semantics NILM Metadata
+ Towards Reproducible Research in Energy Disaggregation
+ 10.1109/COMPSACW.2014.97
+
+
+ The shared model suite or rapid experimentation API nilmtk-contrib
+ Towards Reproducible State-of-the-Art Energy Disaggregation
+ 10.1145/3360322.3360844
+
+
+ T1/T2/T3 protocols, runner, or leaderboard results NILMBench2026 · BuildSys '26
+ NILMBench2026: A Benchmark for Energy Disaggregation
+ 10.1145/3744256.3812587
+
+
+
+
+
+
+
+
+
+
+
+
+
Extend the ecosystem
+
One model contribution. One auditable route to rank.
+
+
Algorithm code and benchmark evidence move at different cadences. A model can merge once its implementation contract is sound. It enters the leaderboard only after the pinned benchmark matrix passes on real data.
+
+
+
+ 01 Implement Add the model to nilmtk-contrib using shared validation, windowing, checkpoint, and logging utilities.
Model repository ↗
+ 02 Break it on purpose Test malformed inputs, short chunks, lazy imports, determinism, serialization, CPU smoke, and the targeted CUDA path.
Model tests ↗
+ 03 Register Add only the adapter and search space to NILMbench. Dataset and task logic stays in the benchmark, never in a model notebook.
Benchmark repository ↗
+ 04 Run real data Freeze source revisions, image digest, data identity, hardware, seeds, and protocol. Candidate results remain separate.
Run protocol ↗
+ 05 Publish evidence Review immutable result bundles; CI regenerates the leaderboard and rejects hand-edited or incomparable ranks.
Living leaderboard ↗
+
+
+
+
+
New here?
+
Start with a reproducible failure.
+
Open an issue with the repository, exact command, operating system, Python version, dataset identity, and the smallest trace that reproduces the problem.
+
+
Choose a repository ↗
+
+
+
+
+
+
+
+
diff --git a/js/site.js b/js/site.js
new file mode 100644
index 0000000..a130831
--- /dev/null
+++ b/js/site.js
@@ -0,0 +1,80 @@
+const header = document.querySelector("[data-header]");
+const menuButton = document.querySelector("[data-menu-button]");
+const menu = document.querySelector("[data-menu]");
+const tabs = Array.from(document.querySelectorAll("[data-tab]"));
+
+function updateHeader() {
+ header?.classList.toggle("is-scrolled", window.scrollY > 8);
+}
+
+function closeMenu() {
+ menu?.classList.remove("is-open");
+ menuButton?.setAttribute("aria-expanded", "false");
+ menuButton?.setAttribute("aria-label", "Open navigation menu");
+}
+
+menuButton?.addEventListener("click", () => {
+ const isOpen = menu?.classList.toggle("is-open") ?? false;
+ menuButton.setAttribute("aria-expanded", String(isOpen));
+ menuButton.setAttribute(
+ "aria-label",
+ isOpen ? "Close navigation menu" : "Open navigation menu",
+ );
+});
+
+menu?.querySelectorAll("a").forEach((link) => link.addEventListener("click", closeMenu));
+
+document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") {
+ closeMenu();
+ menuButton?.focus();
+ }
+});
+
+window.addEventListener("scroll", updateHeader, { passive: true });
+window.addEventListener("resize", () => {
+ if (window.innerWidth > 820) closeMenu();
+});
+updateHeader();
+
+function selectTab(tab) {
+ tabs.forEach((candidate) => {
+ const selected = candidate === tab;
+ candidate.setAttribute("aria-selected", String(selected));
+ candidate.tabIndex = selected ? 0 : -1;
+ const panel = document.getElementById(candidate.dataset.tab);
+ if (panel) panel.hidden = !selected;
+ });
+}
+
+tabs.forEach((tab, index) => {
+ tab.addEventListener("click", () => selectTab(tab));
+ tab.addEventListener("keydown", (event) => {
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
+ event.preventDefault();
+ let nextIndex = index;
+ if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
+ if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
+ if (event.key === "Home") nextIndex = 0;
+ if (event.key === "End") nextIndex = tabs.length - 1;
+ selectTab(tabs[nextIndex]);
+ tabs[nextIndex].focus();
+ });
+});
+
+document.querySelectorAll("[data-copy]").forEach((button) => {
+ button.addEventListener("click", async () => {
+ const source = document.getElementById(button.dataset.copy);
+ if (!source) return;
+ const original = button.textContent;
+ try {
+ await navigator.clipboard.writeText(source.textContent.trim());
+ button.textContent = "Copied";
+ } catch {
+ button.textContent = "Select code";
+ }
+ window.setTimeout(() => {
+ button.textContent = original;
+ }, 1600);
+ });
+});
diff --git a/scripts/check_site.py b/scripts/check_site.py
new file mode 100644
index 0000000..42e42e5
--- /dev/null
+++ b/scripts/check_site.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""Dependency-free structural checks for the NILMTK ecosystem landing page."""
+
+from __future__ import annotations
+
+from collections import Counter
+from html.parser import HTMLParser
+from pathlib import Path
+import sys
+from urllib.parse import unquote, urlsplit
+
+
+ROOT = Path(__file__).resolve().parents[1]
+INDEX = ROOT / "index.html"
+
+REQUIRED_URLS = {
+ "https://github.com/nilmtk/nilmtk",
+ "https://github.com/nilmtk/nilm_metadata",
+ "https://github.com/nilmtk/nilmtk-contrib",
+ "https://github.com/nilmtk/nilmbench",
+ "https://nilmtk.github.io/nilmbench/",
+ "https://doi.org/10.1145/2602044.2602051",
+ "https://doi.org/10.1109/COMPSACW.2014.97",
+ "https://doi.org/10.1145/3360322.3360844",
+ "https://doi.org/10.1145/3744256.3812587",
+}
+
+
+class SiteParser(HTMLParser):
+ def __init__(self) -> None:
+ super().__init__(convert_charrefs=True)
+ self.ids: list[str] = []
+ self.links: list[tuple[str, str]] = []
+
+ def handle_starttag(
+ self, tag: str, attrs: list[tuple[str, str | None]]
+ ) -> None:
+ values = dict(attrs)
+ if identifier := values.get("id"):
+ self.ids.append(identifier)
+ for attribute in ("href", "src"):
+ if target := values.get(attribute):
+ self.links.append((attribute, target))
+
+
+def local_target(target: str) -> Path | None:
+ parsed = urlsplit(target)
+ if parsed.scheme or parsed.netloc or target.startswith(("mailto:", "tel:")):
+ return None
+ if not parsed.path:
+ return None
+ relative = unquote(parsed.path).lstrip("/")
+ return ROOT / relative
+
+
+def main() -> int:
+ source = INDEX.read_text(encoding="utf-8")
+ parser = SiteParser()
+ parser.feed(source)
+ errors: list[str] = []
+
+ duplicates = sorted(name for name, count in Counter(parser.ids).items() if count > 1)
+ if duplicates:
+ errors.append(f"duplicate HTML ids: {', '.join(duplicates)}")
+
+ known_ids = set(parser.ids)
+ for attribute, target in parser.links:
+ parsed = urlsplit(target)
+ if parsed.path in ("", INDEX.name) and parsed.fragment:
+ if parsed.fragment not in known_ids:
+ errors.append(f"broken fragment link: {target}")
+ path = local_target(target)
+ if path is not None and not path.exists():
+ errors.append(f"missing local {attribute}: {target}")
+
+ if "http://" in source:
+ errors.append("public page contains an insecure http:// URL")
+
+ missing_urls = sorted(url for url in REQUIRED_URLS if url not in source)
+ if missing_urls:
+ errors.append("missing canonical links: " + ", ".join(missing_urls))
+
+ retired_assets = ("jquery", "bootstrap.css", "bootstrap.js")
+ for asset in retired_assets:
+ if asset in source.lower():
+ errors.append(f"retired landing-page dependency referenced: {asset}")
+
+ required_assets = (ROOT / "css/landing-page.css", ROOT / "js/site.js", ROOT / "favicon.svg")
+ for asset in required_assets:
+ if not asset.is_file() or asset.stat().st_size == 0:
+ errors.append(f"required asset is absent or empty: {asset.relative_to(ROOT)}")
+
+ if errors:
+ print("Site checks failed:", file=sys.stderr)
+ for error in errors:
+ print(f"- {error}", file=sys.stderr)
+ return 1
+
+ print(
+ f"Site checks passed: {len(parser.ids)} ids, "
+ f"{len(parser.links)} links/assets, {len(REQUIRED_URLS)} canonical URLs."
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())