From fb352ee5575e6c9009c1742f3a1e60286d3c7914 Mon Sep 17 00:00:00 2001 From: Powfu-zwx <1011046478@qq.com> Date: Tue, 15 Sep 2026 15:26:09 +0800 Subject: [PATCH 1/3] Make the quality gates check facts, not prose The gates had drifted from checking invariants into freezing the current implementation, and several of them could not fail. Delete the checks that asserted authoring choices: README and homepage copy frozen as Python literals, a wordmark check whose two branches both reported an error, and the per-chapter counts (exactly one .tex, exactly three figure PDFs) that froze the current 14 chapters. Keep the invariants that can break: every .tex has its compiled PDF beside it, every notebook parses, every TOC entry resolves, every local link resolves, language attributes match. Fix the checks that judged at the wrong layer: - check_tex ran its regexes over raw TeX, so the template's commented-out example was a false positive and the fix had been a filename exemption (`fig1_xxx`). Strip comments first, counting preceding backslashes so `\%` stays a literal, and drop the exemption. - check_bilingual only enumerated English assets, so a Chinese asset with a missing English twin was never looked at. Pair every asset with its twin. - check_site re-derived the Chinese-page rule that breakrl_locale owns, and the language toggle kept a 120-line page map by hand where the repository already enforces a naming convention. Both now read the one rule. - prepare_site_assets kept the file suffix and the base64 flag in two tables. Pin the Colab bootstrap to the release tag that ships it, replacing the hand-maintained `?v=2` counter, and stop importing repo-internal helpers in a module delivered alone by raw URL into an empty runtime: runpy does not add the script's directory to sys.path, so the import failed exactly where it mattered. That import is the only notebook change: 1 line per notebook, 28 files. sync_colab_bootstrap keeps its in-place text edit. An nbformat round-trip is content-preserving but rewrites 1178 lines across 20 of 28 notebooks. Co-Authored-By: Claude Opus 5 --- README.md | 2 - README.zh.md | 2 - book/_config.yml | 1 - book/_static/breakrl.js | 71 ++--- book/_static/lang-toggle.js | 252 ++++-------------- book/index-zh.md | 2 - book/index.md | 2 - book/notes/actor-critic/ac_experiments.ipynb | 2 +- .../actor-critic/ac_experiments_en.ipynb | 2 +- .../decision-transformer_experiments.ipynb | 2 +- .../decision-transformer_experiments_en.ipynb | 2 +- book/notes/dpo/dpo_experiments.ipynb | 2 +- book/notes/dpo/dpo_experiments_en.ipynb | 2 +- book/notes/dqn/dqn_experiments.ipynb | 2 +- book/notes/dqn/dqn_experiments_en.ipynb | 2 +- book/notes/grpo/grpo_experiments.ipynb | 2 +- book/notes/grpo/grpo_experiments_en.ipynb | 2 +- book/notes/mdp/mdp_experiments.ipynb | 2 +- book/notes/mdp/mdp_experiments_en.ipynb | 2 +- .../model-based-rl_experiments.ipynb | 2 +- .../model-based-rl_experiments_en.ipynb | 2 +- .../multi-armed-bandit_experiments.ipynb | 2 +- .../multi-armed-bandit_experiments_en.ipynb | 2 +- .../offline-rl/offline-rl_experiments.ipynb | 2 +- .../offline-rl_experiments_en.ipynb | 2 +- .../policy-gradient/pg_experiments.ipynb | 2 +- .../policy-gradient/pg_experiments_en.ipynb | 2 +- book/notes/ppo/ppo_experiments.ipynb | 2 +- book/notes/ppo/ppo_experiments_en.ipynb | 2 +- book/notes/rlhf/rlhf_experiments.ipynb | 2 +- book/notes/rlhf/rlhf_experiments_en.ipynb | 2 +- book/notes/sac/sac_experiments.ipynb | 2 +- book/notes/sac/sac_experiments_en.ipynb | 2 +- ...oral-difference-learning_experiments.ipynb | 2 +- ...l-difference-learning_experiments_en.ipynb | 2 +- docs/CONTRIBUTING.md | 8 +- docs/CONTRIBUTING.zh.md | 8 +- scripts/check_consistency.py | 214 ++++----------- scripts/check_site.py | 102 +++---- scripts/colab_setup.py | 16 +- scripts/paths.py | 1 - scripts/prepare_site_assets.py | 45 ++-- scripts/sync_colab_bootstrap.py | 15 +- scripts/test_colab_setup.py | 47 +++- scripts/test_lang_toggle.js | 68 +++-- 45 files changed, 319 insertions(+), 593 deletions(-) diff --git a/README.md b/README.md index 801451f..59aa0df 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,6 @@ Each chapter is an ablation: from bandits through PPO and SAC, then offline RL, BreakRL is a bilingual, failure-first RL textbook (English default, Chinese edition). Each chapter explains a mechanism, then removes it so you can see the failure. Chapters 12–14 use toy sequence tasks and two-digit addition; they are not a substitute for production RLHF, DPO, or GRPO training. -**Current textbook:** v1.2.x on `main`. A V2 rewrite is an unstarted internal spec; it will not replace or empty this tree. -

Minimum demo: at low data coverage, TD loss falls while actual return collapses

diff --git a/README.zh.md b/README.zh.md index 537e3dd..e37de1b 100644 --- a/README.zh.md +++ b/README.zh.md @@ -27,8 +27,6 @@ BreakRL 是一本双语、失败优先的强化学习教材(英文为默认语言,另有中文版)。每章先讲机制,再去掉它,让你看见失败。第 12–14 章用玩具序列和两位数加法讲机制,不能替代真实的 RLHF、DPO 或 GRPO 训练。 -**当前教材:** `main` 上的 v1.2.x。V2 仍是未开工的内部规格,不会替换或清空本仓库。 -

最小演示:低数据覆盖率时 TD loss 下降,实际回报塌缩

diff --git a/book/_config.yml b/book/_config.yml index 9149bba..d3e5321 100644 --- a/book/_config.yml +++ b/book/_config.yml @@ -31,7 +31,6 @@ sphinx: language: en exclude_patterns: - ".github/**" - - "PROJECT_AUDIT.md" html_css_files: - lang-toggle.css - breakrl.css diff --git a/book/_static/breakrl.js b/book/_static/breakrl.js index 8b711bb..34f5ec1 100644 --- a/book/_static/breakrl.js +++ b/book/_static/breakrl.js @@ -1,36 +1,7 @@ (function () { 'use strict'; - function pagename() { - if (window.DOCUMENTATION_OPTIONS && DOCUMENTATION_OPTIONS.pagename) { - return String(DOCUMENTATION_OPTIONS.pagename); - } - var path = String(window.location.pathname || ''); - var trimmed = path.replace(/\/+$/, ''); - if (!trimmed || /(?:^|\/)index\.html$/.test(trimmed) || /\/$/.test(path)) { - return 'index'; - } - var marker = '/BreakRL/'; - var start = trimmed.indexOf(marker); - var relative = start >= 0 ? trimmed.slice(start + marker.length) : trimmed.replace(/^.*\//, ''); - return relative.replace(/\.html$/, ''); - } - - function setSurface() { - var page = pagename(); - var home = page === 'index' || page === 'index-zh'; - var demo = page === 'demo'; - var atlas = page === 'failure-atlas' || page === 'failure-atlas-en'; - var pdf = page === 'offline-rl-text' || page === 'offline-rl-text-en'; - document.documentElement.classList.toggle('breakrl-home', home); - document.documentElement.classList.toggle('breakrl-demo-page', demo); - document.documentElement.classList.toggle('breakrl-atlas', atlas); - document.documentElement.classList.toggle('breakrl-reader', pdf); - document.body.classList.toggle('breakrl-home', home); - document.body.classList.toggle('breakrl-demo-page', demo); - document.body.classList.toggle('breakrl-atlas', atlas); - document.body.classList.toggle('breakrl-reader', pdf); - } + var CHINESE_FOOTER = 'BreakRL 是一本双语、失败优先的强化学习教材。正文 CC BY 4.0,代码 MIT。'; function brandHref() { var brand = document.querySelector('a.navbar-brand'); @@ -77,6 +48,20 @@ } } + // Give an element its Chinese label while remembering what the page itself + // says, so switching back to English restores the text `_toc.yml` or + // `_config.yml` holds instead of a copy kept here. + function swapToChinese(selector, chinese, lang) { + document.querySelectorAll(selector).forEach(function (node) { + var original = node.getAttribute('data-breakrl-original'); + if (original === null) { + original = (node.textContent || '').trim(); + node.setAttribute('data-breakrl-original', original); + } + node.textContent = lang === 'zh' ? chinese : original; + }); + } + function relabelToc() { var lang = document.documentElement.getAttribute('data-breakrl-lang') || 'en'; document.querySelectorAll('.bd-sidebar p.caption .caption-text').forEach(function (node) { @@ -84,38 +69,20 @@ if (!node.getAttribute('data-breakrl-caption')) { node.setAttribute('data-breakrl-caption', original); } - if (original === '从这里开始 / Start here' || original === 'Start here') { + if (original === 'Start here') { node.textContent = lang === 'zh' ? '从这里开始' : 'Start here'; - } else if (original === 'English' || original === 'Chapters') { - node.textContent = 'Chapters'; - } else if (original === '中文版' || original === '章节') { + } else if (original === '中文版') { node.textContent = lang === 'zh' ? '章节' : '中文版'; } }); - document.querySelectorAll('.bd-sidebar a[href$="demo.html"]').forEach(function (node) { - node.textContent = lang === 'zh' ? '最小演示' : 'Minimum demo'; - }); - document.querySelectorAll('.breakrl-footer').forEach(function (node) { - node.textContent = lang === 'zh' - ? 'BreakRL 是一本双语、失败优先的强化学习教材。正文 CC BY 4.0,代码 MIT。' - : 'BreakRL is a bilingual, failure-first RL textbook. Text CC BY 4.0. Code MIT.'; - }); - } - - function keepHomepageVisible() { - var article = document.getElementById('breakrl'); - var english = document.getElementById('breakrl-english'); - if (article && !english && article.hidden) { - article.hidden = false; - } + swapToChinese('.bd-sidebar a[href$="demo.html"]', '最小演示', lang); + swapToChinese('.breakrl-footer', CHINESE_FOOTER, lang); } function init() { - setSurface(); useTextBrand(); installHeaderBrand(); relabelToc(); - keepHomepageVisible(); } if (document.readyState === 'loading') { diff --git a/book/_static/lang-toggle.js b/book/_static/lang-toggle.js index 37475c4..aee67ea 100644 --- a/book/_static/lang-toggle.js +++ b/book/_static/lang-toggle.js @@ -5,129 +5,27 @@ var LANG_ZH = 'zh'; var LANG_EN = 'en'; - // Only book pages have language counterparts. Shared utility pages stay put. - var PAGE_MAP = { - 'index.html': { zh: 'index-zh.html', en: 'index.html' }, - 'index-zh.html': { zh: 'index-zh.html', en: 'index.html' }, - 'search.html': { zh: 'search.html', en: 'search.html' }, - 'genindex.html': { zh: 'genindex.html', en: 'genindex.html' }, - 'failure-atlas.html': { zh: 'failure-atlas.html', en: 'failure-atlas-en.html' }, - 'failure-atlas-en.html': { zh: 'failure-atlas.html', en: 'failure-atlas-en.html' }, - 'offline-rl-text.html': { zh: 'offline-rl-text.html', en: 'offline-rl-text-en.html' }, - 'offline-rl-text-en.html': { zh: 'offline-rl-text.html', en: 'offline-rl-text-en.html' }, - 'notes/multi-armed-bandit/multi-armed-bandit_experiments.html': { - zh: 'notes/multi-armed-bandit/multi-armed-bandit_experiments.html', - en: 'notes/multi-armed-bandit/multi-armed-bandit_experiments_en.html' - }, - 'notes/multi-armed-bandit/multi-armed-bandit_experiments_en.html': { - zh: 'notes/multi-armed-bandit/multi-armed-bandit_experiments.html', - en: 'notes/multi-armed-bandit/multi-armed-bandit_experiments_en.html' - }, - 'notes/mdp/mdp_experiments.html': { - zh: 'notes/mdp/mdp_experiments.html', - en: 'notes/mdp/mdp_experiments_en.html' - }, - 'notes/mdp/mdp_experiments_en.html': { - zh: 'notes/mdp/mdp_experiments.html', - en: 'notes/mdp/mdp_experiments_en.html' - }, - 'notes/temporal-difference-learning/temporal-difference-learning_experiments.html': { - zh: 'notes/temporal-difference-learning/temporal-difference-learning_experiments.html', - en: 'notes/temporal-difference-learning/temporal-difference-learning_experiments_en.html' - }, - 'notes/temporal-difference-learning/temporal-difference-learning_experiments_en.html': { - zh: 'notes/temporal-difference-learning/temporal-difference-learning_experiments.html', - en: 'notes/temporal-difference-learning/temporal-difference-learning_experiments_en.html' - }, - 'notes/dqn/dqn_experiments.html': { - zh: 'notes/dqn/dqn_experiments.html', - en: 'notes/dqn/dqn_experiments_en.html' - }, - 'notes/dqn/dqn_experiments_en.html': { - zh: 'notes/dqn/dqn_experiments.html', - en: 'notes/dqn/dqn_experiments_en.html' - }, - 'notes/policy-gradient/pg_experiments.html': { - zh: 'notes/policy-gradient/pg_experiments.html', - en: 'notes/policy-gradient/pg_experiments_en.html' - }, - 'notes/policy-gradient/pg_experiments_en.html': { - zh: 'notes/policy-gradient/pg_experiments.html', - en: 'notes/policy-gradient/pg_experiments_en.html' - }, - 'notes/actor-critic/ac_experiments.html': { - zh: 'notes/actor-critic/ac_experiments.html', - en: 'notes/actor-critic/ac_experiments_en.html' - }, - 'notes/actor-critic/ac_experiments_en.html': { - zh: 'notes/actor-critic/ac_experiments.html', - en: 'notes/actor-critic/ac_experiments_en.html' - }, - 'notes/ppo/ppo_experiments.html': { - zh: 'notes/ppo/ppo_experiments.html', - en: 'notes/ppo/ppo_experiments_en.html' - }, - 'notes/ppo/ppo_experiments_en.html': { - zh: 'notes/ppo/ppo_experiments.html', - en: 'notes/ppo/ppo_experiments_en.html' - }, - 'notes/sac/sac_experiments.html': { - zh: 'notes/sac/sac_experiments.html', - en: 'notes/sac/sac_experiments_en.html' - }, - 'notes/sac/sac_experiments_en.html': { - zh: 'notes/sac/sac_experiments.html', - en: 'notes/sac/sac_experiments_en.html' - }, - 'notes/offline-rl/offline-rl_experiments.html': { - zh: 'notes/offline-rl/offline-rl_experiments.html', - en: 'notes/offline-rl/offline-rl_experiments_en.html' - }, - 'notes/offline-rl/offline-rl_experiments_en.html': { - zh: 'notes/offline-rl/offline-rl_experiments.html', - en: 'notes/offline-rl/offline-rl_experiments_en.html' - }, - 'notes/model-based-rl/model-based-rl_experiments.html': { - zh: 'notes/model-based-rl/model-based-rl_experiments.html', - en: 'notes/model-based-rl/model-based-rl_experiments_en.html' - }, - 'notes/model-based-rl/model-based-rl_experiments_en.html': { - zh: 'notes/model-based-rl/model-based-rl_experiments.html', - en: 'notes/model-based-rl/model-based-rl_experiments_en.html' - }, - 'notes/decision-transformer/decision-transformer_experiments.html': { - zh: 'notes/decision-transformer/decision-transformer_experiments.html', - en: 'notes/decision-transformer/decision-transformer_experiments_en.html' - }, - 'notes/decision-transformer/decision-transformer_experiments_en.html': { - zh: 'notes/decision-transformer/decision-transformer_experiments.html', - en: 'notes/decision-transformer/decision-transformer_experiments_en.html' - }, - 'notes/rlhf/rlhf_experiments.html': { - zh: 'notes/rlhf/rlhf_experiments.html', - en: 'notes/rlhf/rlhf_experiments_en.html' - }, - 'notes/rlhf/rlhf_experiments_en.html': { - zh: 'notes/rlhf/rlhf_experiments.html', - en: 'notes/rlhf/rlhf_experiments_en.html' - }, - 'notes/dpo/dpo_experiments.html': { - zh: 'notes/dpo/dpo_experiments.html', - en: 'notes/dpo/dpo_experiments_en.html' - }, - 'notes/dpo/dpo_experiments_en.html': { - zh: 'notes/dpo/dpo_experiments.html', - en: 'notes/dpo/dpo_experiments_en.html' - }, - 'notes/grpo/grpo_experiments.html': { - zh: 'notes/grpo/grpo_experiments.html', - en: 'notes/grpo/grpo_experiments_en.html' - }, - 'notes/grpo/grpo_experiments_en.html': { - zh: 'notes/grpo/grpo_experiments.html', - en: 'notes/grpo/grpo_experiments_en.html' - } - }; + // Pages pair by filename. Chapter notebooks follow the convention + // `scripts/check_consistency.py` enforces — the English edition of + // `notes//_experiments.html` is its `_en` sibling — so a new + // chapter's toggle works without an edit here. The site's own text pages are + // listed instead, because their names do not say which edition they are. + // Sphinx's search and genindex pages, and the single-language demo, pair with + // nothing and stay put. + var TEXT_PAGES = indexPairs([ + ['index-zh.html', 'index.html'], + ['failure-atlas.html', 'failure-atlas-en.html'], + ['offline-rl-text.html', 'offline-rl-text-en.html'] + ]); + + function indexPairs(pairs) { + var index = {}; + pairs.forEach(function (pair) { + index[pair[0]] = { zh: pair[0], en: pair[1] }; + index[pair[1]] = { zh: pair[0], en: pair[1] }; + }); + return index; + } var UI_LABELS = { zh: { @@ -164,46 +62,41 @@ var originalTitle = null; - function pageKey(pathname) { + // A pathname split into the directory that holds the page and the page's own + // filename. A URL ending in `/` serves that directory's index page. + function pageLocation(pathname) { var path = String(pathname || '').split(/[?#]/)[0]; - var trimmed = path.replace(/\/+$/, ''); - if (!trimmed || /(?:^|\/)index\.html$/.test(trimmed) || /\/$/.test(path)) { - return 'index.html'; - } - var keys = Object.keys(PAGE_MAP); - for (var i = 0; i < keys.length; i += 1) { - var key = keys[i]; - if (trimmed === key || trimmed.slice(-(key.length + 1)) === '/' + key) { - return key; - } - } - return null; + var cut = path.lastIndexOf('/') + 1; + return { directory: path.slice(0, cut), name: path.slice(cut) || 'index.html' }; } - function parallelPath(pathname, lang) { - var key = pageKey(pathname); - var page = key ? PAGE_MAP[key] : null; - if (!page || !page[lang] || page[lang] === key) { + function pagePair(location) { + if (TEXT_PAGES[location.name]) { + return TEXT_PAGES[location.name]; + } + if (!/(^|\/)notes\//.test(location.directory)) { return null; } - var path = String(pathname || '').split(/[?#]/)[0]; - if (path === key) { - return page[lang]; + if (/_experiments_en\.html$/.test(location.name)) { + return { zh: location.name.replace(/_en\.html$/, '.html'), en: location.name }; } - if (key === 'index.html') { - if (/\/$/.test(path)) { - return path + page[lang]; - } - if (/(?:^|\/)index\.html$/.test(path)) { - return path.replace(/index\.html$/, page[lang]); - } + if (/_experiments\.html$/.test(location.name)) { + return { zh: location.name, en: location.name.replace(/\.html$/, '_en.html') }; } - var marker = '/' + key; - var start = path.lastIndexOf(marker); - if (start < 0) { + return null; + } + + function pageName(pathname) { + return pageLocation(pathname).name; + } + + function parallelPath(pathname, lang) { + var location = pageLocation(pathname); + var pair = pagePair(location); + if (!pair || !pair[lang] || location.name === pair[lang]) { return null; } - return path.slice(0, start + 1) + page[lang]; + return location.directory + pair[lang]; } function parallelTarget(lang) { @@ -215,12 +108,12 @@ } function pageLanguage(pathname) { - var key = pageKey(pathname); - var page = key ? PAGE_MAP[key] : null; - if (!page || page.zh === page.en) { + var location = pageLocation(pathname); + var pair = pagePair(location); + if (!pair) { return null; } - return page.en === key ? LANG_EN : LANG_ZH; + return location.name === pair.en ? LANG_EN : LANG_ZH; } function storedLang() { @@ -231,10 +124,6 @@ return value === LANG_EN || value === LANG_ZH ? value : null; } - function currentLang() { - return pageLanguage(window.location.pathname) || storedLang() || LANG_EN; - } - function setDirectText(selector, text) { document.querySelectorAll(selector).forEach(function (node) { var textNode = null; @@ -292,13 +181,13 @@ } }); - var key = pageKey(window.location.pathname); + var name = pageName(window.location.pathname); if (originalTitle === null) { originalTitle = document.title; } - if (lang === LANG_EN && key === 'search.html') { + if (lang === LANG_EN && name === 'search.html') { document.title = 'Search - BreakRL'; - } else if (lang === LANG_EN && key === 'genindex.html') { + } else if (lang === LANG_EN && name === 'genindex.html') { document.title = 'Index - BreakRL'; } else { document.title = originalTitle; @@ -350,7 +239,6 @@ }); setUiLanguage(lang); filterToc(lang); - filterIndex(lang); } function filterToc(lang) { @@ -373,34 +261,6 @@ }); } - function filterIndex(lang) { - var cn = document.getElementById('breakrl'); - var en = document.getElementById('breakrl-english'); - // Split homepages only have one of these ids. Hiding #breakrl would - // conceal the entire English (or Chinese) article body. - if (!cn || !en) { - return; - } - cn.hidden = lang === LANG_EN; - en.hidden = lang === LANG_ZH; - - document.querySelectorAll('.bd-sidebar-secondary a').forEach(function (link) { - var href = link.getAttribute('href'); - var isEn = href === '#' || href === '#breakrl-english'; - var isZh = href === '#breakrl'; - if (!isZh && !isEn) { - return; - } - var entry = link.parentElement; - while (entry && entry.tagName !== 'LI') { - entry = entry.parentElement; - } - if (entry) { - entry.hidden = (lang === LANG_EN && isZh) || (lang === LANG_ZH && isEn); - } - }); - } - function applyLang(lang) { if (lang !== LANG_EN && lang !== LANG_ZH) { return; @@ -459,11 +319,9 @@ } window.BreakRLLanguageToggle = { - pageKey: pageKey, pageLanguage: pageLanguage, parallelPath: parallelPath, - parallelTarget: parallelTarget, - pageMap: PAGE_MAP + parallelTarget: parallelTarget }; if (document.readyState === 'loading') { diff --git a/book/index-zh.md b/book/index-zh.md index 0b7f170..7d84da7 100644 --- a/book/index-zh.md +++ b/book/index-zh.md @@ -4,8 +4,6 @@ BreakRL 是一本双语、失败优先的强化学习教材:每章先讲机制,再去掉它,让你看见失败。 -**当前教材:** v1.2.x。V2 仍是未开工的内部规格,不会替换或清空 `main`。 - ## 三分钟开始 1. 打开[最小演示](demo),拖动覆盖率滑块(教学示意,不训练模型); diff --git a/book/index.md b/book/index.md index c864602..50b2269 100644 --- a/book/index.md +++ b/book/index.md @@ -4,8 +4,6 @@ BreakRL is a bilingual, failure-first textbook. Each chapter explains a mechanism, then removes it so you can see the failure. -**Current textbook:** v1.2.x. A V2 rewrite is an unstarted internal spec; it will not replace or empty `main`. - ## Start in three minutes 1. Open the [minimum demo](demo) and drag the coverage slider (a teaching plot; it does not train a model); diff --git a/book/notes/actor-critic/ac_experiments.ipynb b/book/notes/actor-critic/ac_experiments.ipynb index 0884155..f5b0acf 100644 --- a/book/notes/actor-critic/ac_experiments.ipynb +++ b/book/notes/actor-critic/ac_experiments.ipynb @@ -53,7 +53,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/actor-critic/ac_experiments_en.ipynb b/book/notes/actor-critic/ac_experiments_en.ipynb index f5d774e..dd1226b 100644 --- a/book/notes/actor-critic/ac_experiments_en.ipynb +++ b/book/notes/actor-critic/ac_experiments_en.ipynb @@ -53,7 +53,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/decision-transformer/decision-transformer_experiments.ipynb b/book/notes/decision-transformer/decision-transformer_experiments.ipynb index 869c8e5..4060ddd 100644 --- a/book/notes/decision-transformer/decision-transformer_experiments.ipynb +++ b/book/notes/decision-transformer/decision-transformer_experiments.ipynb @@ -45,7 +45,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb b/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb index 7caa0b3..85b682e 100644 --- a/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb +++ b/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb @@ -45,7 +45,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/dpo/dpo_experiments.ipynb b/book/notes/dpo/dpo_experiments.ipynb index e123dca..d86a754 100644 --- a/book/notes/dpo/dpo_experiments.ipynb +++ b/book/notes/dpo/dpo_experiments.ipynb @@ -45,7 +45,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/dpo/dpo_experiments_en.ipynb b/book/notes/dpo/dpo_experiments_en.ipynb index b3c40b5..9e9d829 100644 --- a/book/notes/dpo/dpo_experiments_en.ipynb +++ b/book/notes/dpo/dpo_experiments_en.ipynb @@ -45,7 +45,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/dqn/dqn_experiments.ipynb b/book/notes/dqn/dqn_experiments.ipynb index 1b04ef8..fe9efe2 100644 --- a/book/notes/dqn/dqn_experiments.ipynb +++ b/book/notes/dqn/dqn_experiments.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/dqn/dqn_experiments_en.ipynb b/book/notes/dqn/dqn_experiments_en.ipynb index 0c9b128..92f0523 100644 --- a/book/notes/dqn/dqn_experiments_en.ipynb +++ b/book/notes/dqn/dqn_experiments_en.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/grpo/grpo_experiments.ipynb b/book/notes/grpo/grpo_experiments.ipynb index 88047f9..b5ac830 100644 --- a/book/notes/grpo/grpo_experiments.ipynb +++ b/book/notes/grpo/grpo_experiments.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/grpo/grpo_experiments_en.ipynb b/book/notes/grpo/grpo_experiments_en.ipynb index 9e4ca8d..b509e5b 100644 --- a/book/notes/grpo/grpo_experiments_en.ipynb +++ b/book/notes/grpo/grpo_experiments_en.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/mdp/mdp_experiments.ipynb b/book/notes/mdp/mdp_experiments.ipynb index c0d64f6..efe5c1a 100644 --- a/book/notes/mdp/mdp_experiments.ipynb +++ b/book/notes/mdp/mdp_experiments.ipynb @@ -46,7 +46,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/mdp/mdp_experiments_en.ipynb b/book/notes/mdp/mdp_experiments_en.ipynb index b26f279..70e6e0a 100644 --- a/book/notes/mdp/mdp_experiments_en.ipynb +++ b/book/notes/mdp/mdp_experiments_en.ipynb @@ -45,7 +45,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/model-based-rl/model-based-rl_experiments.ipynb b/book/notes/model-based-rl/model-based-rl_experiments.ipynb index ed2b894..f1aa615 100644 --- a/book/notes/model-based-rl/model-based-rl_experiments.ipynb +++ b/book/notes/model-based-rl/model-based-rl_experiments.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb b/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb index 09b1d68..1e90a01 100644 --- a/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb +++ b/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb b/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb index 7855bb7..f9dd854 100644 --- a/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb +++ b/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb b/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb index 39a37e5..4246f91 100644 --- a/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb +++ b/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/offline-rl/offline-rl_experiments.ipynb b/book/notes/offline-rl/offline-rl_experiments.ipynb index 4795aef..7c7da16 100644 --- a/book/notes/offline-rl/offline-rl_experiments.ipynb +++ b/book/notes/offline-rl/offline-rl_experiments.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/offline-rl/offline-rl_experiments_en.ipynb b/book/notes/offline-rl/offline-rl_experiments_en.ipynb index 5da97a7..5d7aa6b 100644 --- a/book/notes/offline-rl/offline-rl_experiments_en.ipynb +++ b/book/notes/offline-rl/offline-rl_experiments_en.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/policy-gradient/pg_experiments.ipynb b/book/notes/policy-gradient/pg_experiments.ipynb index e8cf1c1..ad4abac 100644 --- a/book/notes/policy-gradient/pg_experiments.ipynb +++ b/book/notes/policy-gradient/pg_experiments.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/policy-gradient/pg_experiments_en.ipynb b/book/notes/policy-gradient/pg_experiments_en.ipynb index 537ec33..d3706fc 100644 --- a/book/notes/policy-gradient/pg_experiments_en.ipynb +++ b/book/notes/policy-gradient/pg_experiments_en.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/ppo/ppo_experiments.ipynb b/book/notes/ppo/ppo_experiments.ipynb index 1e015a7..96e471a 100644 --- a/book/notes/ppo/ppo_experiments.ipynb +++ b/book/notes/ppo/ppo_experiments.ipynb @@ -42,7 +42,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/ppo/ppo_experiments_en.ipynb b/book/notes/ppo/ppo_experiments_en.ipynb index 709acf5..586d603 100644 --- a/book/notes/ppo/ppo_experiments_en.ipynb +++ b/book/notes/ppo/ppo_experiments_en.ipynb @@ -42,7 +42,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/rlhf/rlhf_experiments.ipynb b/book/notes/rlhf/rlhf_experiments.ipynb index 055e99b..a7abf8e 100644 --- a/book/notes/rlhf/rlhf_experiments.ipynb +++ b/book/notes/rlhf/rlhf_experiments.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/rlhf/rlhf_experiments_en.ipynb b/book/notes/rlhf/rlhf_experiments_en.ipynb index e8237f6..51fe725 100644 --- a/book/notes/rlhf/rlhf_experiments_en.ipynb +++ b/book/notes/rlhf/rlhf_experiments_en.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/sac/sac_experiments.ipynb b/book/notes/sac/sac_experiments.ipynb index cea1f15..f6c12ff 100644 --- a/book/notes/sac/sac_experiments.ipynb +++ b/book/notes/sac/sac_experiments.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/sac/sac_experiments_en.ipynb b/book/notes/sac/sac_experiments_en.ipynb index 4d1c322..67cbccf 100644 --- a/book/notes/sac/sac_experiments_en.ipynb +++ b/book/notes/sac/sac_experiments_en.ipynb @@ -43,7 +43,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb b/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb index 99b487d..6c25519 100644 --- a/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb +++ b/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb b/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb index 7d059c9..43c3092 100644 --- a/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb +++ b/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb @@ -47,7 +47,7 @@ "\n", " _setup = Path('/content/_breakrl_colab_setup.py')\n", " _req = urllib.request.Request(\n", - " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/main/scripts/colab_setup.py' + '?v=2',\n", + " 'https://raw.githubusercontent.com/Powfu-zwx/BreakRL/v1.2.2/scripts/colab_setup.py' + '?v=v1.2.2',\n", " headers={'Cache-Control': 'no-cache'},\n", " )\n", " _setup.write_bytes(urllib.request.urlopen(_req).read())\n", diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ed4d7bf..a249e93 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -18,17 +18,21 @@ python -m ipykernel install --user --name rl_env --display-name rl_env python scripts/run_jupyter.py # Windows certificate-store workaround; otherwise python -m jupyter lab ``` +`requirements.txt` keeps lower bounds so Colab resolves current packages. The committed figures and saved outputs were generated with Python 3.10.21, numpy 2.2.6, matplotlib 3.10.9, torch 2.9.0, and gymnasium 1.3.0, on the site toolchain jupyter-book 1.0.4.post1 / myst-nb 1.4.0 / Sphinx 7.4.7; a newer stack can move the last digits of an experiment. + ## Chapter layout -Each chapter lives in `book/notes//` and contains: +Each chapter lives in `book/notes//` and follows this convention: - one `.tex` file and the compiled `.pdf`; - one `_experiments.ipynb`; - three `fig*.pdf` experiment figures generated by the notebook. +That shape is a convention, not a schema. `scripts/check_consistency.py` only checks that every `.tex` has a compiled PDF beside it and that every experiment notebook parses; what a chapter holds beyond that is the author's call. + **Bilingual pair:** every chapter has a Chinese edition (`.tex` / `.pdf` / `_experiments.ipynb`) and an English edition (`_en.tex` / `_en.pdf` / `_experiments_en.ipynb`). English notebooks translate markdown cells only; code and saved outputs must match the Chinese notebook (`scripts/check_consistency.py` enforces this). Register both editions in the English and 中文版 parts of `book/_toc.yml`, in `README.md`, in `README.zh.md`, and in the `book/index.md` / `book/index-zh.md` tables. **If you change Chinese conclusions, numbers, or figures, update the English edition in the same change.** -The site language toggle (`book/_static/lang-toggle.js`) depends on filename pairs: `*_experiments` ↔ `*_experiments_en`, `failure-atlas` ↔ `failure-atlas-en`, `index.html` ↔ `index-zh.html`, and `offline-rl-text` ↔ `offline-rl-text-en`. Keep those names if you add or rename a page. The homepage is one language per page (English default). +The site language toggle (`book/_static/lang-toggle.js`) pairs pages by filename. Chapter notebooks pair automatically (`*_experiments` ↔ `*_experiments_en`, and `scripts/check_consistency.py` fails when one edition is missing); the site's own text pages are listed in the toggle's `TEXT_PAGES`, so a new one needs an entry there: `failure-atlas` ↔ `failure-atlas-en`, `index.html` ↔ `index-zh.html`, `offline-rl-text` ↔ `offline-rl-text-en`. The homepage is one language per page (English default). Chapter text follows [`book/notes/rl_note_template.tex`](../book/notes/rl_note_template.tex): story → formalism → mechanism (three ablation figures) → algorithm → comparison → place in the family → references. Use `[!htbp]` on figure environments. Notebooks use three Figure sections plus a short recap. diff --git a/docs/CONTRIBUTING.zh.md b/docs/CONTRIBUTING.zh.md index 3858195..b2cd80d 100644 --- a/docs/CONTRIBUTING.zh.md +++ b/docs/CONTRIBUTING.zh.md @@ -18,17 +18,21 @@ python -m ipykernel install --user --name rl_env --display-name rl_env python scripts/run_jupyter.py # Windows 证书库规避;无此问题可直接 python -m jupyter lab ``` +`requirements.txt` 只写下界,以便 Colab 解析到当前可用版本。已提交的图表与 notebook 输出由 Python 3.10.21、numpy 2.2.6、matplotlib 3.10.9、torch 2.9.0、gymnasium 1.3.0 生成,站点工具链为 jupyter-book 1.0.4.post1 / myst-nb 1.4.0 / Sphinx 7.4.7;换成更新的版本可能让实验的末位数字发生偏移。 + ## 章节结构约定 -每章一个目录 `book/notes//`,包含且仅包含: +每章一个目录 `book/notes//`,约定如下: - 一个 `.tex` 与同名编译产物 `.pdf`; - 一个 `_experiments.ipynb`; - 三张 `fig*.pdf` 实验图(由 notebook 生成)。 +这是写作约定而非结构校验:`scripts/check_consistency.py` 只保证每个 `.tex` 旁边有编译好的同名 PDF、每个实验 notebook 能被解析,章节里放什么由作者决定。 + **英文版(双语对齐)**:每章均含中文版(`.tex` / `.pdf` / `_experiments.ipynb`)与英文版(`_en.tex` / `_en.pdf` / `_experiments_en.ipynb`)。英文版只翻译 markdown 单元格,代码与输出和中文版保持一致(`scripts/check_consistency.py` 会强制校验英文版 Notebook 的代码单元与中文版一致),并登记进 `book/_toc.yml` 的 English 与中文版分区、`README.md`、`README.zh.md` 与 `book/index.md` / `book/index-zh.md` 的章节表。**修改中文版内容(正文结论、实验数值、图)时必须同步英文版**——两份不一致视为未完成。 -站点的「中文 / EN」语言切换按钮(`book/_static/lang-toggle.js`)依赖文件名约定:`*_experiments` ↔ `*_experiments_en`、`failure-atlas` ↔ `failure-atlas-en`、`index.html` ↔ `index-zh.html`、`offline-rl-text` ↔ `offline-rl-text-en` 互为平行页。首页每种语言单独一页(默认英文)。新增或改名页面需保持该约定,否则切换按钮失效。 +站点的「中文 / EN」语言切换按钮(`book/_static/lang-toggle.js`)按文件名配对。章节 notebook 自动配对(`*_experiments` ↔ `*_experiments_en`,缺一版时 `scripts/check_consistency.py` 报错);站点自己的文字页需登记在切换按钮的 `TEXT_PAGES` 里,新增时要在那里加一条:`failure-atlas` ↔ `failure-atlas-en`、`index.html` ↔ `index-zh.html`、`offline-rl-text` ↔ `offline-rl-text-en`。首页每种语言单独一页(默认英文)。 正文遵循模板 [`book/notes/rl_note_template.tex`](../book/notes/rl_note_template.tex) 的结构:先讲故事 → 形式化 → 机制(配三图消融)→ 算法流程 → 对比 → 体系位置 → 参考资料。figure 环境统一用 `[!htbp]`。notebook 按三个 Figure 小节 + 小结组织。 diff --git a/scripts/check_consistency.py b/scripts/check_consistency.py index f84e00f..10df11f 100644 --- a/scripts/check_consistency.py +++ b/scripts/check_consistency.py @@ -28,6 +28,30 @@ def _cell_source(cell: Any) -> str: return source.replace("\r\n", "\n") +def _strip_tex_comments(text: str) -> str: + """Drop TeX comments so commented-out source never drives a check. + + A percent sign starts a comment only when it is not escaped, so the + backslashes immediately before it are counted: ``\\%`` renders a literal + percent, while ``\\\\%`` (a line break followed by a comment) does not. + """ + lines = [] + for line in text.splitlines(): + for index, char in enumerate(line): + if char != "%": + continue + backslashes = 0 + cursor = index - 1 + while cursor >= 0 and line[cursor] == "\\": + backslashes += 1 + cursor -= 1 + if backslashes % 2 == 0: + line = line[:index] + break + lines.append(line) + return "\n".join(lines) + + def _load_notebook(path: Path, nbformat: Any, errors: list[str], label: str) -> Any: try: return nbformat.read(path, as_version=4) @@ -47,30 +71,13 @@ def check_chapters() -> list[str]: for chapter in chapter_dirs(): rel = chapter.relative_to(REPO_ROOT) - tex_files = sorted(chapter.glob("*.tex")) - en_tex = [path for path in tex_files if path.stem.endswith("_en")] - main_tex = [path for path in tex_files if not path.stem.endswith("_en")] - if len(main_tex) != 1: - errors.append(f"{rel}: expected exactly one main .tex, found {len(main_tex)}") - if len(en_tex) > 1: - errors.append(f"{rel}: expected at most one *_en.tex, found {len(en_tex)}") - - for tex in main_tex + en_tex: + for tex in sorted(chapter.glob("*.tex")): pdf = tex.with_suffix(".pdf") if not pdf.exists(): errors.append(f"{rel}: missing chapter PDF {pdf.name}") - notebooks = sorted(chapter.glob("*_experiments.ipynb")) - if len(notebooks) != 1: - errors.append( - f"{rel}: expected exactly one *_experiments.ipynb, found {len(notebooks)}" - ) - for path in notebooks: + for path in sorted(chapter.glob("*_experiments*.ipynb")): _load_notebook(path, nbformat, errors, str(path.relative_to(REPO_ROOT))) - - figures = sorted(chapter.glob("fig*.pdf")) - if len(figures) != 3: - errors.append(f"{rel}: expected exactly three figure PDFs, found {len(figures)}") return errors @@ -102,6 +109,18 @@ def _compare_notebooks(cn: Any, en: Any, rel: Path, errors: list[str]) -> None: break +def _paired_edition(path: Path) -> Path: + """Return the same asset in the other language edition. + + Chapters ship two editions of every tex and notebook and name them + ```` (Chinese) and ``_en`` (English). The site's language toggle + derives its links from that convention, so both editions must exist. + """ + stem = path.stem + twin = stem.removesuffix("_en") if stem.endswith("_en") else f"{stem}_en" + return path.with_name(f"{twin}{path.suffix}") + + def check_bilingual() -> list[str]: errors = [] try: @@ -111,42 +130,20 @@ def check_bilingual() -> list[str]: for chapter in chapter_dirs(): rel = chapter.relative_to(REPO_ROOT) - cn_notebooks = sorted(chapter.glob("*_experiments.ipynb")) - en_tex = sorted(chapter.glob("*_en.tex")) - en_notebooks = sorted(chapter.glob("*_experiments_en.ipynb")) - - if not en_tex and not en_notebooks: - continue - - for path in en_tex: - base = path.stem.removesuffix("_en") - cn_path = chapter / f"{base}_experiments.ipynb" - en_path = chapter / f"{base}_experiments_en.ipynb" - if not cn_path.exists(): - errors.append(f"{rel}: missing Chinese notebook {cn_path.name}") + for pattern in ("*.tex", "*_experiments*.ipynb"): + for path in sorted(chapter.glob(pattern)): + twin = _paired_edition(path) + if not twin.exists(): + errors.append(f"{rel}: {path.name} has no paired edition {twin.name}") + + for cn_path in sorted(chapter.glob("*_experiments.ipynb")): + en_path = _paired_edition(cn_path) if not en_path.exists(): - errors.append(f"{rel}: missing English notebook {en_path.name}") - if not cn_path.exists() or not en_path.exists(): continue cn = _load_notebook(cn_path, nbformat, errors, str(cn_path.relative_to(REPO_ROOT))) en = _load_notebook(en_path, nbformat, errors, str(en_path.relative_to(REPO_ROOT))) if cn is not None and en is not None: _compare_notebooks(cn, en, rel, errors) - - for path in en_notebooks: - base = path.stem.removesuffix("_experiments_en") - expected_tex = chapter / f"{base}_en.tex" - if not expected_tex.exists(): - errors.append(f"{rel}: English notebook has no paired TeX {expected_tex.name}") - - if len(en_tex) > 1: - errors.append(f"{rel}: expected at most one *_en.tex, found {len(en_tex)}") - if len(en_notebooks) > 1: - errors.append( - f"{rel}: expected at most one *_experiments_en.ipynb, found {len(en_notebooks)}" - ) - if not cn_notebooks: - errors.append(f"{rel}: English assets exist but the Chinese notebook is missing") return errors @@ -156,7 +153,7 @@ def check_tex() -> list[str]: return [f"missing {NOTES.relative_to(REPO_ROOT).as_posix()} directory"] for path in sorted(NOTES.rglob("*.tex")): try: - text = path.read_text(encoding="utf-8") + text = _strip_tex_comments(path.read_text(encoding="utf-8")) except OSError as error: errors.append(f"{path.relative_to(REPO_ROOT)}: cannot read TeX: {error}") continue @@ -167,8 +164,6 @@ def check_tex() -> list[str]: ) for match in INCLUDE_GRAPHIC.finditer(text): graphic = Path(match.group(1)) - if graphic.name.startswith("fig1_xxx"): - continue if graphic.suffix: candidates = [path.parent / graphic] else: @@ -200,21 +195,10 @@ def check_toc() -> list[str]: else: errors.append(f"{toc_label}: missing root entry") - toc_notebooks = set() for entry in entries: source = _resolve_toc_entry(entry) - if entry.startswith("notes/"): - toc_notebooks.add(source) if not source.exists(): errors.append(f"{toc_label}: missing source for entry {entry}") - - repo_notebooks = { - notebook - for chapter in chapter_dirs() - for notebook in chapter.glob("*_experiments.ipynb") - } - for notebook in sorted(repo_notebooks - toc_notebooks): - errors.append(f"{toc_label}: chapter notebook not listed: {notebook.relative_to(REPO_ROOT)}") return errors @@ -235,11 +219,7 @@ def check_colab_entry_points() -> list[str]: index_zh_label = (BOOK / "index-zh.md").relative_to(REPO_ROOT).as_posix() for chapter in chapter_dirs(): wanted = bootstrap_source(chapter.name) - notebooks = sorted(chapter.glob("*_experiments*.ipynb")) - if not notebooks: - errors.append(f"{chapter.relative_to(REPO_ROOT)}: missing experiment notebooks") - continue - for path in notebooks: + for path in sorted(chapter.glob("*_experiments*.ipynb")): rel = path.relative_to(REPO_ROOT) notebook = _load_notebook(path, nbformat, errors, str(rel)) if notebook is None: @@ -265,102 +245,6 @@ def check_colab_entry_points() -> list[str]: return errors -def _section_between(text: str, start: str, end: str, label: str, errors: list[str]) -> str: - start_at = text.find(start) - end_at = text.find(end) - if start_at < 0 or end_at < 0 or end_at <= start_at: - errors.append(f"{label}: missing section {start!r} .. {end!r}") - return "" - return text[start_at:end_at] - - -def check_flagship_ring() -> list[str]: - errors = [] - anchor = "#atlas-8-offline-loss" - readme_en = (REPO_ROOT / "README.md").read_text(encoding="utf-8") - readme_zh = (REPO_ROOT / "README.zh.md").read_text(encoding="utf-8") - index_en = (BOOK / "index.md").read_text(encoding="utf-8") - index_zh = (BOOK / "index-zh.md").read_text(encoding="utf-8") - demo = (BOOK / "demo.md").read_text(encoding="utf-8") - atlas_en = (BOOK / "failure-atlas-en.md").read_text(encoding="utf-8") - atlas_zh = (BOOK / "failure-atlas.md").read_text(encoding="utf-8") - - en_start = _section_between( - readme_en, "## Start in three minutes", "## Learning path", "README.md", errors - ) - zh_start = _section_between( - readme_zh, "## 三分钟开始", "## 学习路线", "README.zh.md", errors - ) - if en_start: - for needle in ("demo.html", f"failure-atlas-en.html{anchor}", "offline-rl"): - if needle not in en_start: - errors.append(f"README.md three-minute start is missing {needle}") - if "and start with Chapter 1" in en_start: - errors.append("README.md three-minute start still sends readers to Chapter 1") - if zh_start: - for needle in ("demo.html", f"failure-atlas.html{anchor}", "offline-rl"): - if needle not in zh_start: - errors.append(f"README.zh.md three-minute start is missing {needle}") - if "打开[在线教材]" in zh_start: - errors.append("README.zh.md three-minute start still sends readers to Chapter 1") - - if "三分钟开始" in index_en or "用失败学强化学习" in index_en: - errors.append("book/index.md still stacks the Chinese homepage body") - if "Start in three minutes" in index_zh or "Learn reinforcement learning through failure" in index_zh: - errors.append("book/index-zh.md still stacks the English homepage body") - for label, text, needles in ( - ("book/index.md", index_en, ("demo", "atlas-8-offline-loss", "offline-rl")), - ("book/index-zh.md", index_zh, ("demo", "atlas-8-offline-loss", "offline-rl")), - ): - for needle in needles: - if needle not in text: - errors.append(f"{label}: missing flagship link {needle}") - if "github.com/Powfu-zwx/BreakRL/blob/" in text and "offline-rl" in text: - if re.search( - r"github\.com/Powfu-zwx/BreakRL/blob/[^)\s]*offline-rl[^)\s]*\.pdf", - text, - ): - errors.append(f"{label}: Offline RL PDF still points at a GitHub blob") - - if anchor not in demo or "offline-rl" not in demo: - errors.append("book/demo.md must link Failure Atlas #8 and the Offline RL chapter") - if 'id="atlas-8-offline-loss"' not in atlas_en or "demo" not in atlas_en or "offline-rl" not in atlas_en: - errors.append("book/failure-atlas-en.md #8 must keep the demo → atlas → chapter loop") - if 'id="atlas-8-offline-loss"' not in atlas_zh or "demo" not in atlas_zh or "offline-rl" not in atlas_zh: - errors.append("book/failure-atlas.md #8 must keep the demo → atlas → chapter loop") - return errors - - -def check_flagship_look() -> list[str]: - """Keep the site wordmark recolorable and the demo h1 in the accessibility tree.""" - from check_site import css_hides_h1 - - errors = [] - config = (BOOK / "_config.yml").read_text(encoding="utf-8") - if re.search(r"(?m)^\s*logo\s*:", config): - errors.append("book/_config.yml must not set an image logo; navbar brand is CSS text / currentColor") - logo = REPO_ROOT / "assets" / "breakrl-logo.svg" - if logo.is_file(): - text = logo.read_text(encoding="utf-8", errors="ignore") - if "#171513" in text: - errors.append( - "assets/breakrl-logo.svg uses hardcoded fill #171513; " - "do not ship an image SVG the site cannot recolor" - ) - else: - errors.append("assets/breakrl-logo.svg must not ship; navbar brand is CSS text / currentColor") - demo = (BOOK / "demo.md").read_text(encoding="utf-8") - if not re.search(r"(?m)^# ", demo): - errors.append("book/demo.md must keep a markdown level-one heading") - for css_name in ("breakrl.css", "breakrl-demo.css", "lang-toggle.css"): - css_path = BOOK / "_static" / css_name - if not css_path.is_file(): - continue - if css_hides_h1(css_path.read_text(encoding="utf-8")): - errors.append(f"book/_static/{css_name}: must not display:none an h1") - return errors - - def main() -> int: errors = ( check_chapters() @@ -368,8 +252,6 @@ def main() -> int: + check_tex() + check_toc() + check_colab_entry_points() - + check_flagship_ring() - + check_flagship_look() ) if errors: for error in errors: diff --git a/scripts/check_site.py b/scripts/check_site.py index 41d045d..edb3eaa 100644 --- a/scripts/check_site.py +++ b/scripts/check_site.py @@ -9,9 +9,11 @@ if str(_SCRIPTS) not in sys.path: sys.path.insert(0, str(_SCRIPTS)) +from breakrl_locale import is_chinese_page # noqa: E402 from paths import BOOK_BUILD # noqa: E402 DEFAULT_BUILD = BOOK_BUILD +CHINESE_LANG_VALUES = {"zh-CN", "zh_CN", "zh"} _H1_IN_SELECTOR = re.compile(r"(^|[\s,>+~])h1($|[\s,:+.\[#>~])", re.I) _DISPLAY_NONE = re.compile(r"display\s*:\s*none", re.I) @@ -74,24 +76,31 @@ def _local_target(build_root: Path, source: Path, value: str) -> Path | None: return target +def page_files(build_root: Path) -> list[Path]: + """Every HTML page a reader can reach, in a stable order. + + Jupyter Book ships a few HTML macro templates in ``_static``; they contain + unresolved Sphinx expressions by design and are not public pages. Anything + under ``.github`` is repository metadata that leaked into the build. + """ + return sorted( + path + for path in build_root.rglob("*.html") + if "_static" not in path.relative_to(build_root).parts + and ".github" not in path.relative_to(build_root).parts + ) + + def check_site(build_root: Path = DEFAULT_BUILD) -> list[str]: errors = [] if not build_root.is_dir(): return [f"missing site build directory: {build_root}"] - all_html_files = sorted(build_root.rglob("*.html")) - for path in all_html_files: + for path in sorted(build_root.rglob("*.html")): if ".github" in path.relative_to(build_root).parts: errors.append(f"stray non-book page in build: {path.relative_to(build_root)}") - # Jupyter Book ships a few HTML macro templates in _static. They contain - # unresolved Sphinx expressions by design and are not public pages. - html_files = [ - path - for path in all_html_files - if "_static" not in path.relative_to(build_root).parts - and ".github" not in path.relative_to(build_root).parts - ] + html_files = page_files(build_root) if not html_files: return [f"no HTML pages found under {build_root}"] @@ -103,31 +112,21 @@ def check_site(build_root: Path = DEFAULT_BUILD) -> list[str]: except (OSError, UnicodeError) as error: errors.append(f"cannot read {relative}: {error}") continue - is_english_page = relative.name == "failure-atlas-en.html" or relative.name.endswith( - "_en.html" - ) - is_chinese_page = relative.name in {"failure-atlas.html", "index-zh.html", "offline-rl-text.html"} or ( - relative.name.endswith("_experiments.html") - and not relative.name.endswith("_experiments_en.html") - ) - if is_english_page and parser.html_lang != "en": - errors.append(f"{relative}: English page must declare html lang=\"en\"") - if is_english_page and parser.docsearch_language != "en": - errors.append(f"{relative}: English page must declare docsearch:language=en") - if is_chinese_page and parser.html_lang not in {"zh-CN", "zh_CN", "zh"}: - errors.append(f"{relative}: Chinese page must declare html lang=\"zh-CN\"") - if is_chinese_page and parser.docsearch_language not in {"zh-CN", "zh_CN", "zh"}: - errors.append(f"{relative}: Chinese page must declare docsearch:language=zh-CN") + if is_chinese_page(relative.name.removesuffix(".html")): + if parser.html_lang not in CHINESE_LANG_VALUES: + errors.append(f"{relative}: Chinese page must declare html lang=\"zh-CN\"") + if parser.docsearch_language not in CHINESE_LANG_VALUES: + errors.append(f"{relative}: Chinese page must declare docsearch:language=zh-CN") + else: + if parser.html_lang != "en": + errors.append(f"{relative}: English page must declare html lang=\"en\"") + if parser.docsearch_language != "en": + errors.append(f"{relative}: English page must declare docsearch:language=en") for value in parser.links: target = _local_target(build_root, path, value) if target is not None and not target.exists(): errors.append(f"{relative}: missing local target {value}") - for name in ("offline-rl.pdf", "offline-rl_en.pdf"): - pdf = build_root / "notes" / "offline-rl" / name - if not pdf.is_file(): - errors.append(f"missing flagship chapter PDF: notes/offline-rl/{name}") - for atlas_name in ("failure-atlas-en.html", "failure-atlas.html"): atlas = build_root / atlas_name if not atlas.is_file(): @@ -135,24 +134,16 @@ def check_site(build_root: Path = DEFAULT_BUILD) -> list[str]: elif 'id="atlas-8-offline-loss"' not in atlas.read_text(encoding="utf-8"): errors.append(f"{atlas_name}: missing id=\"atlas-8-offline-loss\"") - blob_pdf = re.compile( - r"github\.com/Powfu-zwx/BreakRL/blob/[^\"']*offline-rl[^\"']*\.pdf" - ) - homepage_checks = ( - (build_root / "index.html", "三分钟开始", "Chinese homepage body"), - (build_root / "index-zh.html", "Start in three minutes", "English homepage body"), - ) - for path, stacked_marker, label in homepage_checks: + # The homepage links the atlas across pages. The language toggle must not + # rewrite that into an in-page hash, which resolves to nothing. + for homepage in ("index.html", "index-zh.html"): + path = build_root / homepage if not path.is_file(): - errors.append(f"missing homepage: {path.relative_to(build_root)}") - continue - text = path.read_text(encoding="utf-8") - if stacked_marker in text: - errors.append(f"{path.name}: still stacks the {label}") - if blob_pdf.search(text): - errors.append(f"{path.name}: Offline RL PDF still points at a GitHub blob") - if 'href="#failure-atlas' in text: - errors.append(f"{path.name}: Failure Atlas #8 link was rewritten into a broken in-page hash") + errors.append(f"missing homepage: {homepage}") + elif 'href="#failure-atlas' in path.read_text(encoding="utf-8"): + errors.append( + f"{homepage}: Failure Atlas #8 link was rewritten into a broken in-page hash" + ) demo = build_root / "demo.html" if not demo.is_file(): @@ -166,16 +157,6 @@ def check_site(build_root: Path = DEFAULT_BUILD) -> list[str]: for css_path in sorted(static.glob("breakrl*.css")) + sorted(static.glob("lang-toggle.css")): if css_hides_h1(css_path.read_text(encoding="utf-8")): errors.append(f"{css_path.relative_to(build_root)}: must not display:none an h1") - - for svg in static.glob("breakrl-logo.svg"): - errors.append( - f"{svg.relative_to(build_root)}: do not ship an image wordmark SVG; navbar brand is CSS text / currentColor" - ) - for path in html_files: - if "breakrl-logo.svg" in path.read_text(encoding="utf-8"): - errors.append( - f"{path.relative_to(build_root)}: must not ship an image wordmark SVG; navbar brand is CSS text" - ) return errors @@ -186,12 +167,7 @@ def main() -> int: for error in errors: print(f"ERROR: {error}") return 1 - pages = sum( - 1 - for path in build_root.rglob("*.html") - if "_static" not in path.relative_to(build_root).parts - and ".github" not in path.relative_to(build_root).parts - ) + pages = len(page_files(build_root)) print(f"checked generated site: {pages} HTML pages and local links OK") return 0 diff --git a/scripts/colab_setup.py b/scripts/colab_setup.py index ded3d3f..a9785b3 100644 --- a/scripts/colab_setup.py +++ b/scripts/colab_setup.py @@ -8,15 +8,21 @@ import sys from pathlib import Path -from paths import NOTES_PREFIX - GITHUB_REPO = "Powfu-zwx/BreakRL" +# The bootstrap a notebook carries must be the one that shipped with it. Readers +# open `main`, so the clone below still tracks `main`; only the fetched setup +# module is pinned, and pinning it to the tag also busts the HTTP cache. +RELEASE_TAG = "v1.2.2" REPO_URL = f"https://github.com/{GITHUB_REPO}.git" RAW_SETUP_URL = ( - f"https://raw.githubusercontent.com/{GITHUB_REPO}/main/scripts/colab_setup.py" + f"https://raw.githubusercontent.com/{GITHUB_REPO}/{RELEASE_TAG}/scripts/colab_setup.py" ) COLAB_ROOT = Path("/content/BreakRL") -NOTES_DIR = Path(NOTES_PREFIX) +# This module is delivered alone, by raw URL, into a Colab runtime that has no +# checkout yet, so it must not import repo-internal helpers such as +# `scripts.paths`. The layout below is therefore restated here on purpose; +# `_repo_is_ready` fails loudly if it ever drifts from the repository. +NOTES_DIR = Path("book/notes") COLAB_NOTEBOOK_BASE = ( f"https://colab.research.google.com/github/{GITHUB_REPO}/blob/main" ) @@ -87,7 +93,7 @@ def bootstrap_source(chapter: str) -> str: "\n" " _setup = Path('/content/_breakrl_colab_setup.py')\n" " _req = urllib.request.Request(\n" - f" {RAW_SETUP_URL!r} + '?v=2',\n" + f" {RAW_SETUP_URL!r} + '?v={RELEASE_TAG}',\n" " headers={'Cache-Control': 'no-cache'},\n" " )\n" " _setup.write_bytes(urllib.request.urlopen(_req).read())\n" diff --git a/scripts/paths.py b/scripts/paths.py index 434d624..f740e96 100644 --- a/scripts/paths.py +++ b/scripts/paths.py @@ -5,4 +5,3 @@ BOOK = REPO_ROOT / "book" NOTES = BOOK / "notes" BOOK_BUILD = BOOK / "_build" / "html" -NOTES_PREFIX = "book/notes" diff --git a/scripts/prepare_site_assets.py b/scripts/prepare_site_assets.py index aa03e86..a131eda 100644 --- a/scripts/prepare_site_assets.py +++ b/scripts/prepare_site_assets.py @@ -13,36 +13,46 @@ from paths import BOOK, NOTES OUTPUT_FOLDER = BOOK / "_build" / "jupyter_execute" +# Each saved image MIME type carries both facts this module needs: the suffix of +# the file it writes, and whether nbformat stored the payload base64-encoded +# (everything binary) or as plain markup (``image/svg+xml``). IMAGE_TYPES = { - "image/png": ".png", - "image/jpeg": ".jpeg", - "image/gif": ".gif", - "image/svg+xml": ".svg", - "application/pdf": ".pdf", + "image/png": (".png", True), + "image/jpeg": (".jpeg", True), + "image/gif": (".gif", True), + "image/svg+xml": (".svg", False), + "application/pdf": (".pdf", True), } -def _output_bytes(mime_type: str, content: str | list[str]) -> bytes: +def _output_bytes(content: str | list[str], *, base64_encoded: bool) -> bytes: if isinstance(content, list): content = "".join(content) - if mime_type in {"image/png", "image/jpeg", "image/gif", "application/pdf"}: + if base64_encoded: return base64.b64decode(content) return content.replace("\r\n", "\n").encode("utf-8") def prepare_assets(output_folder: Path = OUTPUT_FOLDER) -> int: + """Write each saved image output as a content-addressed file. + + myst-nb runs with ``execute_notebooks: "off"`` and resolves notebook image + outputs from this folder by the SHA-256 of the decoded bytes. The naming is + an internal contract of the installed myst-nb version, so re-check it before + upgrading ``jupyter-book``. + """ output_folder.mkdir(parents=True, exist_ok=True) written = 0 for notebook in sorted(NOTES.rglob("*_experiments*.ipynb")): document = json.loads(notebook.read_text(encoding="utf-8")) for cell in document.get("cells", []): for output in cell.get("outputs", []): - for mime_type, extension in IMAGE_TYPES.items(): + for mime_type, (suffix, base64_encoded) in IMAGE_TYPES.items(): content = output.get("data", {}).get(mime_type) if not content: continue - data = _output_bytes(mime_type, content) - target = output_folder / f"{hashlib.sha256(data).hexdigest()}{extension}" + data = _output_bytes(content, base64_encoded=base64_encoded) + target = output_folder / f"{hashlib.sha256(data).hexdigest()}{suffix}" if not target.exists(): target.write_bytes(data) written += 1 @@ -53,17 +63,20 @@ def prepare_build_assets(app): prepare_assets(output_folder=Path(app.outdir).parent / "jupyter_execute") -FLAGSHIP_CHAPTER_PDFS = ( +# The site renders saved outputs, so a chapter PDF that a text page embeds +# inline must exist in the build; Jupyter Book copies linked files but not +# `` targets. +INLINE_CHAPTER_PDFS = ( NOTES / "offline-rl" / "offline-rl.pdf", NOTES / "offline-rl" / "offline-rl_en.pdf", ) -def copy_flagship_chapter_pdfs(outdir: Path) -> int: +def copy_inline_chapter_pdfs(outdir: Path) -> int: dest_dir = Path(outdir) / "notes" / "offline-rl" dest_dir.mkdir(parents=True, exist_ok=True) copied = 0 - for src in FLAGSHIP_CHAPTER_PDFS: + for src in INLINE_CHAPTER_PDFS: if not src.is_file(): raise FileNotFoundError(src) shutil.copy2(src, dest_dir / src.name) @@ -71,15 +84,15 @@ def copy_flagship_chapter_pdfs(outdir: Path) -> int: return copied -def copy_flagship_chapter_pdfs_on_build(app, exception): +def copy_inline_chapter_pdfs_on_build(app, exception): if exception: return - copy_flagship_chapter_pdfs(Path(app.outdir)) + copy_inline_chapter_pdfs(Path(app.outdir)) def setup(app): app.connect("builder-inited", prepare_build_assets) - app.connect("build-finished", copy_flagship_chapter_pdfs_on_build) + app.connect("build-finished", copy_inline_chapter_pdfs_on_build) return { "version": "1.1", "parallel_read_safe": True, diff --git a/scripts/sync_colab_bootstrap.py b/scripts/sync_colab_bootstrap.py index 04de2ff..d813792 100644 --- a/scripts/sync_colab_bootstrap.py +++ b/scripts/sync_colab_bootstrap.py @@ -3,6 +3,11 @@ The cell source is owned by ``colab_setup.bootstrap_source``. Re-run this script after changing that template. Idempotent. Edits only the first code cell so saved outputs keep their original JSON. + +This rewrites the notebook text in place rather than round-tripping through +``nbformat``: the committed notebooks are not uniformly serialized, so a +read/write cycle reindents cells this script has no business touching +(measured: 1178 changed lines across 20 of 28 notebooks). """ from __future__ import annotations @@ -19,7 +24,6 @@ from colab_setup import bootstrap_source from paths import REPO_ROOT FIRST_CODE_CELL = '\n {\n "cell_type": "code",' -LEGACY_PIP_MARKERS = ("# !pip install gymnasium", "# If needed, uncomment") def _source_text(source: list[str] | str) -> str: @@ -39,10 +43,6 @@ def _is_bootstrap(source: str) -> bool: return source.lstrip().startswith("BREAKRL_CHAPTER = ") -def _is_legacy_pip_cell(source: str) -> bool: - return any(marker in source for marker in LEGACY_PIP_MARKERS) - - def _matching_brace(text: str, open_index: int) -> int: if text[open_index] != "{": raise ValueError("expected '{'") @@ -111,11 +111,6 @@ def sync_notebook(path: Path, chapter: str) -> str: block = _format_cell(_bootstrap_cell(chapter, cell_id=str(cell_id))) text = text[:start] + block + text[close + 1 :] action = "updated" - elif _is_legacy_pip_cell(current): - cell_id = first_code.get("id") or uuid.uuid4().hex[:8] - block = _format_cell(_bootstrap_cell(chapter, cell_id=str(cell_id))) - text = text[:start] + block + text[close + 1 :] - action = "updated" else: block = _format_cell(_bootstrap_cell(chapter)) text = text[: start - 1] + "\n" + block + "," + text[start - 1 :] diff --git a/scripts/test_colab_setup.py b/scripts/test_colab_setup.py index 6f84184..bf33b92 100644 --- a/scripts/test_colab_setup.py +++ b/scripts/test_colab_setup.py @@ -2,6 +2,7 @@ from __future__ import annotations import os +import subprocess import sys from pathlib import Path @@ -19,9 +20,11 @@ setup, validate_chapter, ) -from paths import NOTES, NOTES_PREFIX # noqa: E402 +from paths import NOTES # noqa: E402 -NOTES_DIR = Path(NOTES_PREFIX) +# The repository's own layout, used to build the mock clone: the bootstrap must +# resolve the chapter where the repository actually keeps it. +NOTES_RELATIVE = NOTES.relative_to(ROOT) def _fail(errors: list[str], message: str) -> None: @@ -77,16 +80,45 @@ def test_bootstrap_source() -> list[str]: def test_colab_url() -> list[str]: errors = [] - url = colab_notebook_url(f"{NOTES_PREFIX}/ppo/ppo_experiments_en.ipynb") + notebook = f"{NOTES_RELATIVE.as_posix()}/ppo/ppo_experiments_en.ipynb" + url = colab_notebook_url(notebook) expected = ( "https://colab.research.google.com/github/Powfu-zwx/BreakRL/" - f"blob/main/{NOTES_PREFIX}/ppo/ppo_experiments_en.ipynb" + f"blob/main/{notebook}" ) if url != expected: _fail(errors, f"colab_notebook_url mismatch: {url}") return errors +def test_standalone_delivery(tmp_path: Path) -> list[str]: + """The module is fetched alone by raw URL and run before any checkout exists. + + Colab downloads this file to a bare directory and executes it, so every + import it makes must resolve without the repository on ``sys.path``. A + subprocess with a clean path is the only faithful way to check that; an + in-process import would find ``scripts/`` and mask the failure. + """ + errors = [] + delivered = tmp_path / "_breakrl_colab_setup.py" + delivered.write_text( + (SCRIPTS / "colab_setup.py").read_text(encoding="utf-8"), encoding="utf-8" + ) + result = subprocess.run( + [sys.executable, "-c", f"import runpy; runpy.run_path({str(delivered)!r})"], + cwd=tmp_path, + capture_output=True, + text=True, + ) + if result.returncode != 0: + _fail( + errors, + "the delivered Colab bootstrap does not run outside a checkout: " + + result.stderr.strip().splitlines()[-1], + ) + return errors + + def test_setup_local_noop() -> list[str]: errors = [] before = Path.cwd() @@ -110,7 +142,7 @@ def runner(command, cwd=None): # noqa: ANN001 root = Path(cmd[-1]) (root / ".git").mkdir(parents=True) (root / "scripts").mkdir() - (root / NOTES_DIR / "offline-rl").mkdir(parents=True) + (root / NOTES_RELATIVE / "offline-rl").mkdir(parents=True) (root / "requirements.txt").write_text( (ROOT / "requirements.txt").read_text(encoding="utf-8"), encoding="utf-8", @@ -124,7 +156,7 @@ def runner(command, cwd=None): # noqa: ANN001 dest=dest, runner=runner, ) - if chapter_dir != dest / NOTES_DIR / "offline-rl": + if chapter_dir != dest / NOTES_RELATIVE / "offline-rl": _fail(errors, f"setup returned unexpected chapter dir: {chapter_dir}") if Path.cwd() != chapter_dir: _fail(errors, "Colab setup did not chdir into the chapter directory") @@ -162,7 +194,7 @@ def runner(command, cwd=None): # noqa: ANN001 if len(cmd) > 1 and cmd[1] == "clone": root = Path(cmd[-1]) (root / ".git").mkdir(parents=True) - (root / NOTES_DIR / "offline-rl").mkdir(parents=True) + (root / NOTES_RELATIVE / "offline-rl").mkdir(parents=True) (root / "requirements.txt").write_text("numpy>=2\n", encoding="utf-8") before = Path.cwd() @@ -301,6 +333,7 @@ def main(argv: list[str] | None = None) -> int: + test_setup_local_noop() ) with tempfile.TemporaryDirectory() as tmp: + errors += test_standalone_delivery(Path(tmp)) errors += test_setup_colab_mock(Path(tmp)) errors += test_incomplete_clone_is_replaced(Path(tmp) / "incomplete") if smoke: diff --git a/scripts/test_lang_toggle.js b/scripts/test_lang_toggle.js index f33a310..8a090ed 100644 --- a/scripts/test_lang_toggle.js +++ b/scripts/test_lang_toggle.js @@ -24,30 +24,25 @@ vm.runInNewContext(source, sandbox, { filename: sourcePath }); const toggle = sandbox.window.BreakRLLanguageToggle; assert.ok(toggle, 'language toggle test API is available'); +// Chapter notebooks pair by the `_en` suffix, so the expected pairs come from the +// repository rather than a list kept here: a new chapter is covered on arrival. +const notesDir = path.join(__dirname, '..', 'book', 'notes'); +const chapterPairs = fs + .readdirSync(notesDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((chapter) => + fs + .readdirSync(path.join(notesDir, chapter.name)) + .filter((file) => file.endsWith('_experiments.ipynb')) + .map((file) => { + const zh = `notes/${chapter.name}/${file.replace(/\.ipynb$/, '.html')}`; + return [zh, zh.replace(/\.html$/, '_en.html')]; + }) + ); +assert.ok(chapterPairs.length > 0, 'found chapter notebooks to pair'); + const pairs = [ - [ - 'notes/multi-armed-bandit/multi-armed-bandit_experiments.html', - 'notes/multi-armed-bandit/multi-armed-bandit_experiments_en.html' - ], - ['notes/mdp/mdp_experiments.html', 'notes/mdp/mdp_experiments_en.html'], - [ - 'notes/temporal-difference-learning/temporal-difference-learning_experiments.html', - 'notes/temporal-difference-learning/temporal-difference-learning_experiments_en.html' - ], - ['notes/dqn/dqn_experiments.html', 'notes/dqn/dqn_experiments_en.html'], - ['notes/policy-gradient/pg_experiments.html', 'notes/policy-gradient/pg_experiments_en.html'], - ['notes/actor-critic/ac_experiments.html', 'notes/actor-critic/ac_experiments_en.html'], - ['notes/ppo/ppo_experiments.html', 'notes/ppo/ppo_experiments_en.html'], - ['notes/sac/sac_experiments.html', 'notes/sac/sac_experiments_en.html'], - ['notes/offline-rl/offline-rl_experiments.html', 'notes/offline-rl/offline-rl_experiments_en.html'], - ['notes/model-based-rl/model-based-rl_experiments.html', 'notes/model-based-rl/model-based-rl_experiments_en.html'], - [ - 'notes/decision-transformer/decision-transformer_experiments.html', - 'notes/decision-transformer/decision-transformer_experiments_en.html' - ], - ['notes/rlhf/rlhf_experiments.html', 'notes/rlhf/rlhf_experiments_en.html'], - ['notes/dpo/dpo_experiments.html', 'notes/dpo/dpo_experiments_en.html'], - ['notes/grpo/grpo_experiments.html', 'notes/grpo/grpo_experiments_en.html'], + ...chapterPairs, ['failure-atlas.html', 'failure-atlas-en.html'], ['offline-rl-text.html', 'offline-rl-text-en.html'], ['index-zh.html', 'index.html'] @@ -68,15 +63,21 @@ assert.equal(toggle.pageLanguage('/BreakRL/'), 'en'); assert.equal(toggle.pageLanguage('/BreakRL/index.html'), 'en'); assert.equal(toggle.pageLanguage('/BreakRL/index-zh.html'), 'zh'); -for (const shared of ['/BreakRL/search.html', '/BreakRL/genindex.html']) { - assert.equal(toggle.parallelPath(shared, 'zh'), null); - assert.equal(toggle.parallelPath(shared, 'en'), null); +// Sphinx's search and index pages, and the single-language demo, have no +// counterpart: the toggle must neither navigate away nor claim a language. +for (const alone of ['/BreakRL/search.html', '/BreakRL/genindex.html', '/BreakRL/demo.html']) { + assert.equal(toggle.parallelPath(alone, 'zh'), null); + assert.equal(toggle.parallelPath(alone, 'en'), null); + assert.equal(toggle.pageLanguage(alone), null); } +// Editions pair inside the chapter directories, where they are published +// together; a page named like a notebook anywhere else pairs with nothing. +assert.equal(toggle.parallelPath('/BreakRL/setup_experiments.html', 'en'), null); + assert.equal(toggle.parallelTarget('en'), '/BreakRL/notes/dqn/dqn_experiments_en.html?tab=loss#figure-3'); assert.equal(toggle.pageLanguage('/BreakRL/notes/dqn/dqn_experiments.html'), 'zh'); assert.equal(toggle.pageLanguage('/BreakRL/notes/dqn/dqn_experiments_en.html'), 'en'); -assert.equal(toggle.pageLanguage('/BreakRL/search.html'), null); function preferredRedirect(pathname, savedLanguage) { let callback; @@ -117,8 +118,11 @@ assert.equal( '/BreakRL/notes/dqn/dqn_experiments_en.html?from=deep-link#section' ); -assert.match(source, /saved \|\| pageLang \|\| LANG_EN/); -assert.match(source, /\[LANG_EN, LANG_ZH\]/); +assert.equal( + preferredRedirect('/BreakRL/notes/dqn/dqn_experiments.html', 'en'), + '/BreakRL/notes/dqn/dqn_experiments_en.html?from=deep-link#section' +); + assert.equal( preferredRedirect('/BreakRL/index.html', 'zh'), '/BreakRL/index-zh.html?from=deep-link#section' @@ -127,11 +131,5 @@ assert.equal( preferredRedirect('/BreakRL/', 'zh'), '/BreakRL/index-zh.html?from=deep-link#section' ); -assert.match(source, /index-zh\.html/); -assert.match(source, /if \(!cn \|\| !en\)/); -assert.match(source, /setAttribute\('aria-pressed'/); -assert.match(source, /role', 'group'/); -assert.match(source, /bd-sidebar-secondary/); -assert.doesNotMatch(source, /replace\(\/\\\.html\$\//); console.log(`language toggle regression checks passed (${pairs.length} explicit pairs)`); From 05c301f3f0f3dc99daaaca24ede223f5cbee42c0 Mon Sep 17 00:00:00 2001 From: Powfu-zwx <1011046478@qq.com> Date: Tue, 15 Sep 2026 15:40:12 +0800 Subject: [PATCH 2/3] Give the page locale one owner, and serve the chapter PDFs The build emitted `` on every Chinese page. That is a Python locale name, not a BCP 47 tag, and Sphinx writes the value into `` and `docsearch:language` verbatim. Two consequences followed. `check_site` listed `zh_CN` as an accepted value, so the gate could not see it. The language toggle derived each page's own language from the filename instead of reading the tag the build wrote, so on a built page it disagreed with the document and read Chinese pages as English. The build now writes `zh-CN` from one constant in breakrl_locale, the gate accepts only real tags, and the toggle reads `` rather than answering the same question a second way. Chapter PDFs were linked as `github.com/.../blob/main/...` so readers left the site to read them. Link them at the paths the book builds instead. Jupyter Book rewrites and copies files that markdown links point at, so nothing has to be mirrored by hand. The embedded-PDF copy step kept a hand-written pair of paths for the one chapter that uses ``. Jupyter Book copies neither those nor their targets, so the list would silently rot the moment another chapter embedded a PDF. Read the targets out of the page markup instead. Two facts that lived only in prose now have gates: every text page the toggle pairs by hand has a book source, and the entry path still links the minimum demo, Failure Atlas #8, and the Offline RL chapter. Each check asks for link targets and leaves the wording to the author. Co-Authored-By: Claude Opus 5 --- book/_static/lang-toggle.js | 21 +++++++------ book/index-zh.md | 26 ++++++++-------- book/index.md | 26 ++++++++-------- scripts/breakrl_locale.py | 9 ++++-- scripts/check_consistency.py | 40 +++++++++++++++++++++++++ scripts/check_site.py | 16 +++++----- scripts/prepare_site_assets.py | 54 ++++++++++++++++++++++------------ scripts/test_lang_toggle.js | 39 ++++++++++++------------ 8 files changed, 148 insertions(+), 83 deletions(-) diff --git a/book/_static/lang-toggle.js b/book/_static/lang-toggle.js index aee67ea..ba37af2 100644 --- a/book/_static/lang-toggle.js +++ b/book/_static/lang-toggle.js @@ -107,13 +107,12 @@ return target + (window.location.search || '') + (window.location.hash || ''); } - function pageLanguage(pathname) { - var location = pageLocation(pathname); - var pair = pagePair(location); - if (!pair) { - return null; - } - return location.name === pair.en ? LANG_EN : LANG_ZH; + // The build states each page's own language in the HTML it generates + // (`scripts/breakrl_locale.py` owns that rule), so the toggle reads it rather + // than deriving the same answer a second time. + function pageLanguage() { + var lang = document.documentElement.getAttribute('lang') || ''; + return /^zh\b/i.test(lang) ? LANG_ZH : LANG_EN; } function storedLang() { @@ -289,9 +288,9 @@ } function init() { + var pageLang = pageLanguage(); var saved = storedLang(); - var pageLang = pageLanguage(window.location.pathname); - if (saved && pageLang && saved !== pageLang) { + if (saved && saved !== pageLang) { var target = parallelTarget(saved); if (target) { window.location.replace(target); @@ -315,11 +314,11 @@ } } hosts.forEach(installButton); - setLangState(saved || pageLang || LANG_EN); + setLangState(saved || pageLang); } window.BreakRLLanguageToggle = { - pageLanguage: pageLanguage, + textPages: TEXT_PAGES, parallelPath: parallelPath, parallelTarget: parallelTarget }; diff --git a/book/index-zh.md b/book/index-zh.md index 7d84da7..e5ff3aa 100644 --- a/book/index-zh.md +++ b/book/index-zh.md @@ -24,20 +24,20 @@ BreakRL 是一本双语、失败优先的强化学习教材:每章先讲机制 | 章节 | 正文 | 实验 | 运行 | | --- | --- | --- | --- | -| 1. 多臂老虎机:探索与利用 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit.pdf) | [在线阅读](notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb) | -| 2. 马尔可夫决策过程 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp.pdf) | [在线阅读](notes/mdp/mdp_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp_experiments.ipynb) | -| 3. 时序差分学习 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning.pdf) | [在线阅读](notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb) | -| 4. DQN:神经网络价值学习 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn.pdf) | [在线阅读](notes/dqn/dqn_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn_experiments.ipynb) | -| 5. Policy Gradient / REINFORCE | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg.pdf) | [在线阅读](notes/policy-gradient/pg_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg_experiments.ipynb) | -| 6. Actor-Critic / A2C | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac.pdf) | [在线阅读](notes/actor-critic/ac_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac_experiments.ipynb) | -| 7. PPO:约束策略更新 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo.pdf) | [在线阅读](notes/ppo/ppo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo_experiments.ipynb) | -| 8. SAC:最大熵连续控制 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac.pdf) | [在线阅读](notes/sac/sac_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac_experiments.ipynb) | +| 1. 多臂老虎机:探索与利用 | [PDF](notes/multi-armed-bandit/multi-armed-bandit.pdf) | [在线阅读](notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit_experiments.ipynb) | +| 2. 马尔可夫决策过程 | [PDF](notes/mdp/mdp.pdf) | [在线阅读](notes/mdp/mdp_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp_experiments.ipynb) | +| 3. 时序差分学习 | [PDF](notes/temporal-difference-learning/temporal-difference-learning.pdf) | [在线阅读](notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning_experiments.ipynb) | +| 4. DQN:神经网络价值学习 | [PDF](notes/dqn/dqn.pdf) | [在线阅读](notes/dqn/dqn_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn_experiments.ipynb) | +| 5. Policy Gradient / REINFORCE | [PDF](notes/policy-gradient/pg.pdf) | [在线阅读](notes/policy-gradient/pg_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg_experiments.ipynb) | +| 6. Actor-Critic / A2C | [PDF](notes/actor-critic/ac.pdf) | [在线阅读](notes/actor-critic/ac_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac_experiments.ipynb) | +| 7. PPO:约束策略更新 | [PDF](notes/ppo/ppo.pdf) | [在线阅读](notes/ppo/ppo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo_experiments.ipynb) | +| 8. SAC:最大熵连续控制 | [PDF](notes/sac/sac.pdf) | [在线阅读](notes/sac/sac_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac_experiments.ipynb) | | 9. 离线强化学习:CQL 与 IQL | [PDF](offline-rl-text) | [在线阅读](notes/offline-rl/offline-rl_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/offline-rl/offline-rl_experiments.ipynb) | -| 10. 模型式强化学习:Dyna-Q | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl.pdf) | [在线阅读](notes/model-based-rl/model-based-rl_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl_experiments.ipynb) | -| 11. Decision Transformer | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer.pdf) | [在线阅读](notes/decision-transformer/decision-transformer_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer_experiments.ipynb) | -| 12. RLHF:从偏好到奖励 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf.pdf) | [在线阅读](notes/rlhf/rlhf_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf_experiments.ipynb) | -| 13. DPO:不训奖励模型的偏好优化 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo.pdf) | [在线阅读](notes/dpo/dpo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo_experiments.ipynb) | -| 14. GRPO 与 RLVR:可验证奖励 | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo.pdf) | [在线阅读](notes/grpo/grpo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo_experiments.ipynb) | +| 10. 模型式强化学习:Dyna-Q | [PDF](notes/model-based-rl/model-based-rl.pdf) | [在线阅读](notes/model-based-rl/model-based-rl_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl_experiments.ipynb) | +| 11. Decision Transformer | [PDF](notes/decision-transformer/decision-transformer.pdf) | [在线阅读](notes/decision-transformer/decision-transformer_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer_experiments.ipynb) | +| 12. RLHF:从偏好到奖励 | [PDF](notes/rlhf/rlhf.pdf) | [在线阅读](notes/rlhf/rlhf_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf_experiments.ipynb) | +| 13. DPO:不训奖励模型的偏好优化 | [PDF](notes/dpo/dpo.pdf) | [在线阅读](notes/dpo/dpo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo_experiments.ipynb) | +| 14. GRPO 与 RLVR:可验证奖励 | [PDF](notes/grpo/grpo.pdf) | [在线阅读](notes/grpo/grpo_experiments.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo_experiments.ipynb) | 第 12–14 章用玩具序列和两位数加法讲机制,不能替代真实的 RLHF、DPO 或 GRPO 训练。 diff --git a/book/index.md b/book/index.md index 50b2269..326e1df 100644 --- a/book/index.md +++ b/book/index.md @@ -24,20 +24,20 @@ When training does not converge, look up the symptom in the [RL Failure Atlas](f | Chapter | Text | Experiments | Run | | --- | --- | --- | --- | -| 1. Multi-armed bandits | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit_en.pdf) | [Read online](notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb) | -| 2. Markov decision processes | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp_en.pdf) | [Read online](notes/mdp/mdp_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp_experiments_en.ipynb) | -| 3. Temporal-difference learning | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning_en.pdf) | [Read online](notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb) | -| 4. DQN: neural value learning | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn_en.pdf) | [Read online](notes/dqn/dqn_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn_experiments_en.ipynb) | -| 5. Policy gradient / REINFORCE | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg_en.pdf) | [Read online](notes/policy-gradient/pg_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg_experiments_en.ipynb) | -| 6. Actor-Critic / A2C | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac_en.pdf) | [Read online](notes/actor-critic/ac_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac_experiments_en.ipynb) | -| 7. PPO: constrained policy updates | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo_en.pdf) | [Read online](notes/ppo/ppo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo_experiments_en.ipynb) | -| 8. SAC: maximum-entropy continuous control | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac_en.pdf) | [Read online](notes/sac/sac_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac_experiments_en.ipynb) | +| 1. Multi-armed bandits | [PDF](notes/multi-armed-bandit/multi-armed-bandit_en.pdf) | [Read online](notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/multi-armed-bandit/multi-armed-bandit_experiments_en.ipynb) | +| 2. Markov decision processes | [PDF](notes/mdp/mdp_en.pdf) | [Read online](notes/mdp/mdp_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/mdp/mdp_experiments_en.ipynb) | +| 3. Temporal-difference learning | [PDF](notes/temporal-difference-learning/temporal-difference-learning_en.pdf) | [Read online](notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/temporal-difference-learning/temporal-difference-learning_experiments_en.ipynb) | +| 4. DQN: neural value learning | [PDF](notes/dqn/dqn_en.pdf) | [Read online](notes/dqn/dqn_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dqn/dqn_experiments_en.ipynb) | +| 5. Policy gradient / REINFORCE | [PDF](notes/policy-gradient/pg_en.pdf) | [Read online](notes/policy-gradient/pg_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/policy-gradient/pg_experiments_en.ipynb) | +| 6. Actor-Critic / A2C | [PDF](notes/actor-critic/ac_en.pdf) | [Read online](notes/actor-critic/ac_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/actor-critic/ac_experiments_en.ipynb) | +| 7. PPO: constrained policy updates | [PDF](notes/ppo/ppo_en.pdf) | [Read online](notes/ppo/ppo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/ppo/ppo_experiments_en.ipynb) | +| 8. SAC: maximum-entropy continuous control | [PDF](notes/sac/sac_en.pdf) | [Read online](notes/sac/sac_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/sac/sac_experiments_en.ipynb) | | 9. Offline RL: CQL and IQL | [PDF](offline-rl-text-en) | [Read online](notes/offline-rl/offline-rl_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/offline-rl/offline-rl_experiments_en.ipynb) | -| 10. Model-based RL: Dyna-Q | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl_en.pdf) | [Read online](notes/model-based-rl/model-based-rl_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb) | -| 11. Decision Transformer | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer_en.pdf) | [Read online](notes/decision-transformer/decision-transformer_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb) | -| 12. RLHF: from preferences to rewards | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf_en.pdf) | [Read online](notes/rlhf/rlhf_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf_experiments_en.ipynb) | -| 13. DPO: preference optimization without a reward model | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo_en.pdf) | [Read online](notes/dpo/dpo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo_experiments_en.ipynb) | -| 14. GRPO and RLVR: verifiable rewards | [PDF](https://github.com/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo_en.pdf) | [Read online](notes/grpo/grpo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo_experiments_en.ipynb) | +| 10. Model-based RL: Dyna-Q | [PDF](notes/model-based-rl/model-based-rl_en.pdf) | [Read online](notes/model-based-rl/model-based-rl_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/model-based-rl/model-based-rl_experiments_en.ipynb) | +| 11. Decision Transformer | [PDF](notes/decision-transformer/decision-transformer_en.pdf) | [Read online](notes/decision-transformer/decision-transformer_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/decision-transformer/decision-transformer_experiments_en.ipynb) | +| 12. RLHF: from preferences to rewards | [PDF](notes/rlhf/rlhf_en.pdf) | [Read online](notes/rlhf/rlhf_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/rlhf/rlhf_experiments_en.ipynb) | +| 13. DPO: preference optimization without a reward model | [PDF](notes/dpo/dpo_en.pdf) | [Read online](notes/dpo/dpo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/dpo/dpo_experiments_en.ipynb) | +| 14. GRPO and RLVR: verifiable rewards | [PDF](notes/grpo/grpo_en.pdf) | [Read online](notes/grpo/grpo_experiments_en.ipynb) | [Colab](https://colab.research.google.com/github/Powfu-zwx/BreakRL/blob/main/book/notes/grpo/grpo_experiments_en.ipynb) | Chapters 12–14 use toy sequence tasks and two-digit addition. They show the mechanisms; they are not a substitute for production RLHF, DPO, or GRPO training. diff --git a/scripts/breakrl_locale.py b/scripts/breakrl_locale.py index c9f36fe..8de240d 100644 --- a/scripts/breakrl_locale.py +++ b/scripts/breakrl_locale.py @@ -1,10 +1,13 @@ """Set the generated HTML locale for Chinese book pages. The site default language is English. Chinese experiment notebooks and the -Chinese failure atlas still need zh_CN so screen readers, search, and the -language toggle see the correct page locale. +Chinese failure atlas still need a Chinese tag so screen readers, search, and +the language toggle see the correct page locale. Sphinx writes this value into +`` and `docsearch:language` verbatim, so it has to be a BCP 47 tag. """ +CHINESE_LANGUAGE = "zh-CN" + def is_chinese_page(pagename): if pagename in {"failure-atlas", "index-zh", "offline-rl-text"}: @@ -14,7 +17,7 @@ def is_chinese_page(pagename): def set_page_language(app, pagename, templatename, context, doctree): if is_chinese_page(pagename): - context["language"] = "zh_CN" + context["language"] = CHINESE_LANGUAGE def setup(app): diff --git a/scripts/check_consistency.py b/scripts/check_consistency.py index 10df11f..0f58407 100644 --- a/scripts/check_consistency.py +++ b/scripts/check_consistency.py @@ -13,6 +13,19 @@ FIGURE_ENVIRONMENT = re.compile(r"\\begin\{figure\}\[([^]]+)\]") INCLUDE_GRAPHIC = re.compile(r"\\includegraphics(?:\[[^]]*\])?\{([^}]+)\}") TOC_FILE_ENTRY = re.compile(r"^\s*-\s+file:\s+(\S+)", re.MULTILINE) +MARKDOWN_LINK = re.compile(r"\]\(([^)\s]+)") +HTML_HREF = re.compile(r'href="([^"]+)"') + +# The reader's entry path: the minimum demo, Failure Atlas #8, then the Offline +# RL chapter. Each of these pages restates it in its own markup, so the check +# asks for the link targets and leaves the wording around them to the author. +# It is a page-level check: it says the path is still offered, not where. +ENTRY_PAGES = ( + ("README.md", "failure-atlas-en.html"), + ("README.zh.md", "failure-atlas.html"), + ("book/index.md", "failure-atlas-en.html"), + ("book/index-zh.md", "failure-atlas.html"), +) def chapter_dirs() -> list[Path]: @@ -202,6 +215,32 @@ def check_toc() -> list[str]: return errors +def _link_targets(text: str) -> list[str]: + """Every link target in a markdown document, raw HTML anchors included.""" + return MARKDOWN_LINK.findall(text) + HTML_HREF.findall(text) + + +def check_entry_path() -> list[str]: + errors = [] + for name, atlas in ENTRY_PAGES: + path = REPO_ROOT / name + if not path.is_file(): + errors.append(f"missing entry page {name}") + continue + targets = _link_targets(path.read_text(encoding="utf-8")) + wanted = { + "the minimum demo": [t for t in targets if t.rstrip("/").endswith(("demo", "demo.html"))], + f"{atlas}#atlas-8-offline-loss": [ + t for t in targets if t.endswith(f"{atlas}#atlas-8-offline-loss") + ], + "the Offline RL chapter": [t for t in targets if "offline-rl" in t], + } + for label, matches in wanted.items(): + if not matches: + errors.append(f"{name}: entry path no longer links {label}") + return errors + + def check_colab_entry_points() -> list[str]: from colab_setup import bootstrap_source, colab_notebook_url @@ -251,6 +290,7 @@ def main() -> int: + check_bilingual() + check_tex() + check_toc() + + check_entry_path() + check_colab_entry_points() ) if errors: diff --git a/scripts/check_site.py b/scripts/check_site.py index edb3eaa..76a630c 100644 --- a/scripts/check_site.py +++ b/scripts/check_site.py @@ -13,7 +13,10 @@ from paths import BOOK_BUILD # noqa: E402 DEFAULT_BUILD = BOOK_BUILD -CHINESE_LANG_VALUES = {"zh-CN", "zh_CN", "zh"} +# Sphinx writes the language into `` and `docsearch:language` +# verbatim, so these are BCP 47 tags. `zh_CN` is a Python locale name, not a +# tag, and belongs here only as a failure. +CHINESE_LANG_VALUES = {"zh-CN", "zh"} _H1_IN_SELECTOR = re.compile(r"(^|[\s,>+~])h1($|[\s,:+.\[#>~])", re.I) _DISPLAY_NONE = re.compile(r"display\s*:\s*none", re.I) @@ -27,18 +30,17 @@ def __init__(self) -> None: self.html_lang: str | None = None self.docsearch_language: str | None = None - def _collect(self, attrs: list[tuple[str, str | None]]) -> None: - for name, value in attrs: - if name in {"href", "src"} and value: - self.links.append(value) - def _process_tag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: attributes = dict(attrs) if tag == "html": self.html_lang = attributes.get("lang") if tag == "meta" and attributes.get("name") == "docsearch:language": self.docsearch_language = attributes.get("content") - self._collect(attrs) + # A page embeds its own PDFs with ``, and Jupyter Book + # neither rewrites nor copies those. Any other `data` attribute is a + # value the site scripts read, not a file. + names = {"href", "src"} | ({"data"} if tag in {"object", "embed"} else set()) + self.links.extend(value for name, value in attrs if name in names and value) def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: self._process_tag(tag, attrs) diff --git a/scripts/prepare_site_assets.py b/scripts/prepare_site_assets.py index a131eda..34bf8be 100644 --- a/scripts/prepare_site_assets.py +++ b/scripts/prepare_site_assets.py @@ -1,10 +1,17 @@ -"""Materialize saved notebook image outputs for a no-execution book build.""" +"""Place the assets the site links but Jupyter Book does not copy. + +Saved notebook image outputs, which myst-nb resolves from disk by content hash +because the book is not executed, and the files pages embed as raw HTML, which +Jupyter Book only copies when a markdown link points at them. +""" import base64 import hashlib import json +import re import shutil import sys from pathlib import Path +from urllib.parse import urlsplit _SCRIPTS = Path(__file__).resolve().parent if str(_SCRIPTS) not in sys.path: @@ -63,36 +70,47 @@ def prepare_build_assets(app): prepare_assets(output_folder=Path(app.outdir).parent / "jupyter_execute") -# The site renders saved outputs, so a chapter PDF that a text page embeds -# inline must exist in the build; Jupyter Book copies linked files but not -# `` targets. -INLINE_CHAPTER_PDFS = ( - NOTES / "offline-rl" / "offline-rl.pdf", - NOTES / "offline-rl" / "offline-rl_en.pdf", -) +# A page that embeds a file with `` gets no help from Jupyter +# Book: it rewrites markdown links into `_downloads/` and copies the target, but +# leaves raw HTML alone. The embedded files therefore have to be found in the +# pages and copied to the path the page names. +INLINE_OBJECT = re.compile(r"]*\sdata=\"([^\"]+)\"") + + +def inline_assets() -> list[Path]: + found = set() + for page in sorted(BOOK.rglob("*.md")): + if "_build" in page.parts: + continue + for target in INLINE_OBJECT.findall(page.read_text(encoding="utf-8")): + parsed = urlsplit(target) + if parsed.scheme or parsed.netloc or parsed.path.startswith("/"): + continue + asset = (page.parent / parsed.path).resolve() + if asset.is_file() and asset.is_relative_to(BOOK): + found.add(asset) + return sorted(found) -def copy_inline_chapter_pdfs(outdir: Path) -> int: - dest_dir = Path(outdir) / "notes" / "offline-rl" - dest_dir.mkdir(parents=True, exist_ok=True) +def copy_inline_assets(outdir: Path) -> int: copied = 0 - for src in INLINE_CHAPTER_PDFS: - if not src.is_file(): - raise FileNotFoundError(src) - shutil.copy2(src, dest_dir / src.name) + for src in inline_assets(): + dest = Path(outdir) / src.relative_to(BOOK) + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) copied += 1 return copied -def copy_inline_chapter_pdfs_on_build(app, exception): +def copy_inline_assets_on_build(app, exception): if exception: return - copy_inline_chapter_pdfs(Path(app.outdir)) + copy_inline_assets(Path(app.outdir)) def setup(app): app.connect("builder-inited", prepare_build_assets) - app.connect("build-finished", copy_inline_chapter_pdfs_on_build) + app.connect("build-finished", copy_inline_assets_on_build) return { "version": "1.1", "parallel_read_safe": True, diff --git a/scripts/test_lang_toggle.js b/scripts/test_lang_toggle.js index 8a090ed..ed90a72 100644 --- a/scripts/test_lang_toggle.js +++ b/scripts/test_lang_toggle.js @@ -59,16 +59,19 @@ assert.equal(toggle.parallelPath('/BreakRL/', 'zh'), '/BreakRL/index-zh.html'); assert.equal(toggle.parallelPath('/BreakRL/index.html', 'zh'), '/BreakRL/index-zh.html'); assert.equal(toggle.parallelPath('/BreakRL/index-zh.html', 'en'), '/BreakRL/index.html'); assert.equal(toggle.parallelPath('/BreakRL/index.html', 'en'), null); -assert.equal(toggle.pageLanguage('/BreakRL/'), 'en'); -assert.equal(toggle.pageLanguage('/BreakRL/index.html'), 'en'); -assert.equal(toggle.pageLanguage('/BreakRL/index-zh.html'), 'zh'); + +// The three text pages are listed by hand, so a rename that skips the list +// would leave the toggle pointing at a page the book does not build. +for (const page of Object.keys(toggle.textPages)) { + const source = path.join(__dirname, '..', 'book', `${page.replace(/\.html$/, '')}.md`); + assert.ok(fs.existsSync(source), `no book source for text page ${page}`); +} // Sphinx's search and index pages, and the single-language demo, have no -// counterpart: the toggle must neither navigate away nor claim a language. +// counterpart: the toggle must not offer a path off them. for (const alone of ['/BreakRL/search.html', '/BreakRL/genindex.html', '/BreakRL/demo.html']) { assert.equal(toggle.parallelPath(alone, 'zh'), null); assert.equal(toggle.parallelPath(alone, 'en'), null); - assert.equal(toggle.pageLanguage(alone), null); } // Editions pair inside the chapter directories, where they are published @@ -76,10 +79,11 @@ for (const alone of ['/BreakRL/search.html', '/BreakRL/genindex.html', '/BreakRL assert.equal(toggle.parallelPath('/BreakRL/setup_experiments.html', 'en'), null); assert.equal(toggle.parallelTarget('en'), '/BreakRL/notes/dqn/dqn_experiments_en.html?tab=loss#figure-3'); -assert.equal(toggle.pageLanguage('/BreakRL/notes/dqn/dqn_experiments.html'), 'zh'); -assert.equal(toggle.pageLanguage('/BreakRL/notes/dqn/dqn_experiments_en.html'), 'en'); -function preferredRedirect(pathname, savedLanguage) { +// A saved language sends the reader to the other edition of the page they are +// on. The page's own language comes from the `` the build emits, so +// each case states what the build would have written. +function preferredRedirect(pathname, savedLanguage, htmlLang) { let callback; let redirect = null; const testWindow = { @@ -102,7 +106,11 @@ function preferredRedirect(pathname, savedLanguage) { addEventListener(_event, handler) { callback = handler; }, - documentElement: {} + documentElement: { + getAttribute(name) { + return name === 'lang' ? htmlLang : null; + } + } }; vm.runInNewContext(source, { window: testWindow, document: testDocument }, { filename: sourcePath }); callback(); @@ -110,25 +118,20 @@ function preferredRedirect(pathname, savedLanguage) { } assert.equal( - preferredRedirect('/BreakRL/notes/dqn/dqn_experiments_en.html', 'zh'), + preferredRedirect('/BreakRL/notes/dqn/dqn_experiments_en.html', 'zh', 'en'), '/BreakRL/notes/dqn/dqn_experiments.html?from=deep-link#section' ); assert.equal( - preferredRedirect('/BreakRL/notes/dqn/dqn_experiments.html', 'en'), - '/BreakRL/notes/dqn/dqn_experiments_en.html?from=deep-link#section' -); - -assert.equal( - preferredRedirect('/BreakRL/notes/dqn/dqn_experiments.html', 'en'), + preferredRedirect('/BreakRL/notes/dqn/dqn_experiments.html', 'en', 'zh-CN'), '/BreakRL/notes/dqn/dqn_experiments_en.html?from=deep-link#section' ); assert.equal( - preferredRedirect('/BreakRL/index.html', 'zh'), + preferredRedirect('/BreakRL/index.html', 'zh', 'en'), '/BreakRL/index-zh.html?from=deep-link#section' ); assert.equal( - preferredRedirect('/BreakRL/', 'zh'), + preferredRedirect('/BreakRL/', 'zh', 'en'), '/BreakRL/index-zh.html?from=deep-link#section' ); From a2d2225932ac16a78a40bcd94881336efa673c84 Mon Sep 17 00:00:00 2001 From: Powfu-zwx <1011046478@qq.com> Date: Tue, 15 Sep 2026 15:42:35 +0800 Subject: [PATCH 3/3] Replace the hand-rolled notebook JSON surgery with nbformat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync_colab_bootstrap found the first code cell by searching for the literal text of a code-cell opening brace, located its closing brace with a hand-written string-aware bracket matcher, then reformatted a cell dict back into 2-space-indented JSON at the right character offset. All of that work exists to avoid parsing the notebook. Read and write it as a notebook instead: a read, a source assignment, and a write. nbformat is already a dependency of the two gates that validate these files, so this removes code rather than adding a dependency. One trap the old path did not have to face. nbformat.write opens the target with newline translation on, so writing by path turns every line of the notebook into CRLF on Windows — 809 lines in a single file. Write through an explicit newline="\n" handle instead. A notebook already carrying the wanted cell is still not written at all, so the script stays idempotent and a no-op run leaves the tree untouched. Verified on the committed notebooks: all 28 report unchanged and git status stays empty. The churn this could have caused is real — an nbformat round-trip reindents 1178 lines across 20 of the 28 — but the short-circuit keeps it from ever landing. Both write paths behave as before, checked against a copy of a real notebook: a stale bootstrap is replaced in place, keeping its cell id, its position and its neighbours; a missing one is inserted before the first code cell with a fresh id and the remove-cell tag. Notebooks keep LF endings either way. Co-Authored-By: Claude Opus 5 --- scripts/sync_colab_bootstrap.py | 124 ++++++++++---------------------- 1 file changed, 36 insertions(+), 88 deletions(-) diff --git a/scripts/sync_colab_bootstrap.py b/scripts/sync_colab_bootstrap.py index d813792..3a5365b 100644 --- a/scripts/sync_colab_bootstrap.py +++ b/scripts/sync_colab_bootstrap.py @@ -1,29 +1,27 @@ """Write the Colab bootstrap cell into every experiment notebook. The cell source is owned by ``colab_setup.bootstrap_source``. Re-run this -script after changing that template. Idempotent. Edits only the first code -cell so saved outputs keep their original JSON. - -This rewrites the notebook text in place rather than round-tripping through -``nbformat``: the committed notebooks are not uniformly serialized, so a -read/write cycle reindents cells this script has no business touching -(measured: 1178 changed lines across 20 of 28 notebooks). +script after changing that template. Idempotent: a notebook already carrying +the wanted cell is not written at all, so the diff of a no-op run is empty. +Only the first code cell is touched, so saved outputs stay as the reader +published them. """ from __future__ import annotations -import json import sys -import uuid from pathlib import Path _SCRIPTS = Path(__file__).resolve().parent if str(_SCRIPTS) not in sys.path: sys.path.insert(0, str(_SCRIPTS)) +import nbformat + from check_consistency import chapter_dirs from colab_setup import bootstrap_source from paths import REPO_ROOT -FIRST_CODE_CELL = '\n {\n "cell_type": "code",' + +BOOTSTRAP_TAGS = ["remove-cell"] def _source_text(source: list[str] | str) -> str: @@ -32,92 +30,42 @@ def _source_text(source: list[str] | str) -> str: return source -def _source_lines(text: str) -> list[str]: - text = text.replace("\r\n", "\n") - if not text.endswith("\n"): - text += "\n" - return text.splitlines(keepends=True) - - def _is_bootstrap(source: str) -> bool: return source.lstrip().startswith("BREAKRL_CHAPTER = ") -def _matching_brace(text: str, open_index: int) -> int: - if text[open_index] != "{": - raise ValueError("expected '{'") - depth = 0 - in_string = False - escape = False - for index in range(open_index, len(text)): - char = text[index] - if in_string: - if escape: - escape = False - elif char == "\\": - escape = True - elif char == '"': - in_string = False - continue - if char == '"': - in_string = True - elif char == "{": - depth += 1 - elif char == "}": - depth -= 1 - if depth == 0: - return index - raise ValueError("unbalanced cell object") - - -def _first_code_span(text: str) -> tuple[int, int]: - marker_at = text.find(FIRST_CODE_CELL) - if marker_at < 0: - raise ValueError("notebook has no code cell") - brace = marker_at + len("\n ") - close = _matching_brace(text, brace) - return marker_at + 1, close - - -def _format_cell(cell: dict) -> str: - dumped = json.dumps(cell, indent=1, ensure_ascii=False) - return "\n".join(f" {line}" if line else line for line in dumped.splitlines()) - - -def _bootstrap_cell(chapter: str, cell_id: str | None = None) -> dict: - return { - "cell_type": "code", - "execution_count": None, - "id": cell_id or uuid.uuid4().hex[:8], - "metadata": {"tags": ["remove-cell"]}, - "outputs": [], - "source": _source_lines(bootstrap_source(chapter)), - } +def _bootstrap_cell(chapter: str): + return nbformat.v4.new_code_cell( + source=bootstrap_source(chapter).splitlines(keepends=True), + metadata={"tags": list(BOOTSTRAP_TAGS)}, + ) def sync_notebook(path: Path, chapter: str) -> str: - text = path.read_text(encoding="utf-8") - notebook = json.loads(text) - first_code = next(cell for cell in notebook["cells"] if cell.get("cell_type") == "code") - current = _source_text(first_code.get("source", "")) - wanted = _source_lines(bootstrap_source(chapter)) - start, close = _first_code_span(text) - wanted_tags = ["remove-cell"] - has_tag = first_code.get("metadata", {}).get("tags") == wanted_tags - if _is_bootstrap(current): - if _source_lines(current) == wanted and has_tag: - return "unchanged" - cell_id = first_code.get("id") or uuid.uuid4().hex[:8] - block = _format_cell(_bootstrap_cell(chapter, cell_id=str(cell_id))) - text = text[:start] + block + text[close + 1 :] - action = "updated" - else: - block = _format_cell(_bootstrap_cell(chapter)) - text = text[: start - 1] + "\n" + block + "," + text[start - 1 :] + notebook = nbformat.read(path, as_version=4) + wanted = bootstrap_source(chapter) + index = next( + (i for i, cell in enumerate(notebook.cells) if cell.cell_type == "code"), None + ) + if index is None: + raise ValueError(f"{path}: notebook has no code cell") + cell = notebook.cells[index] + current = _source_text(cell.source) + + if not _is_bootstrap(current): + notebook.cells.insert(index, _bootstrap_cell(chapter)) action = "inserted" - if not text.endswith("\n"): - text += "\n" - path.write_text(text, encoding="utf-8", newline="\n") + elif current == wanted and cell.metadata.get("tags") == BOOTSTRAP_TAGS: + return "unchanged" + else: + cell.source = wanted.splitlines(keepends=True) + cell.metadata["tags"] = list(BOOTSTRAP_TAGS) + action = "updated" + + # nbformat opens the file with newline translation on, which on Windows + # would rewrite every line of the notebook as CRLF. + with open(path, "w", encoding="utf-8", newline="\n") as handle: + nbformat.write(notebook, handle) return action