Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/preview/scholia/0.1.0/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Eric Yang (杨星宇)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
113 changes: 113 additions & 0 deletions packages/preview/scholia/0.1.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Scholia

Fill-in study notes for STEM: read one layer, *fill* the other. Theorem **knots**,
intuition **notes**, margin **threads**, and `fillin` blanks — in light or dark.

![Scholia, in light and dark](gallery/preview.png)

Most notes are read-only and forgotten by the next page. Scholia typesets the
*active* layer too: statements and intuition to take in, blanks and proof-skeletons
to work out. The source is your answer key.

> Start a new notebook with `typst init @preview/scholia`, or import the library:

## Use

```typ
#import "@preview/scholia:0.1.0": *

// options: theme: "light" | "dark" · prose: "notes" | "book" · fonts: (…)
#show: scholia

#cover("Title", subtitle: "subtitle", author: "Your Name", date: "2026")

= First course

#note[The big idea, in your own words. The passive layer.]

#definition[a name][State the object, leave a clause blank: #fillin(width: 3cm).] <def:thing>

#theorem[attribution][State the result, then refer back to @def:thing.] <thm:main>
#proof[Skeleton: (i) … (ii) … #TODO[the step that makes it work]]

#yourturn[Restage it as your own computation. #workspace(n: 3)]
```

Knots are built on [`@preview/frame-it`](https://typst.app/universe/package/frame-it):
each is a `figure`, so they get **native `@label` cross-references** and shared,
section-tied numbering for free. The title is the first argument, the body the
last: `#theorem[title][body]` (omit the title with `#theorem[body]`). An optional
middle slot is a small **tag** — a source or claim-ID, styled by `label-it`:
`#theorem[title][GSM 211, Thm 2.16][body]`.

Compile [`examples/example.typ`](examples/example.typ) to see every device in one
mini-chapter. It defaults to the light `notes` theme; uncomment the `theme: "dark"`
or `prose: "book"` line at the top to see the other modes.

## The toolkit

| you want… | you write… |
|---|---|
| the cover | `cover(title, subtitle: …, author: …, date: …, kicker: …)` |
| a result (blue) | `theorem[t][b]` · `lemma` · `proposition` · `corollary` · `conjecture` · `claim` |
| a definition (teal) | `definition[t][b]` · `axiom` · `assumption` · `notation` |
| an example / remark (amber / plain) | `example[t][b]` · `remark[b]` |
| cross-reference a knot | label with `<thm:x>`, refer with `@thm:x` (native) |
| a source / claim-ID tag | a middle slot `theorem[t][tag][b]`, or `label-it[…]` inline |
| the intuition voice | `note[…]` · `whisper[…]` · `keyword[…]` |
| a blank to fill | `fillin(width: 2.2cm)` |
| a proof gap | `TODO[the missing step]` |
| a do-it-yourself box | `yourturn[…]` + `workspace(n: 3)` |
| a margin note | `sidenote[…]` · `recall[…]` (the `?` preset) |

## Options (`#show: scholia.with(…)`)

| option | values | effect |
|---|---|---|
| `theme` | `"light"` (default) · `"dark"` | swaps the whole colour card — page, text, knots, accents (dark = slate) |
| `prose` | `"notes"` (default) · `"book"` | notes = no indent + paragraph spacing; book = first-line indent + tight |
| `paper` | `"a4"` (default) · `"us-letter"` · … | page size |
| `fonts` | dict overriding `default-fonts` | e.g. `(heading: ("Gill Sans",))` |

| Light | Dark |
|:---:|:---:|
| ![Cover, light theme](gallery/cover-light.png) | ![Cover, dark theme](gallery/cover-dark.png) |

## Fonts

Every role is a **fallback list — the nice face first, an embedded face last** — so
output never breaks and degrades gracefully when a font is missing:

| role | default list |
|---|---|
| body | `("STIX Two Text", "Libertinus Serif")` |
| math | `("STIX Two Math", "New Computer Modern Math")` |
| heading | `("Optima", "Libertinus Serif")` |
| sans (intuition voice) | `("Avenir Next", "Libertinus Serif")` |
| mono | `("Menlo", "DejaVu Sans Mono")` |

A Typst package cannot ship fonts, so the universal baseline is Typst's four
embedded faces (Libertinus Serif, New Computer Modern + Math, DejaVu Sans Mono).
For the intended look install **STIX Two** (Text + Math, free/OFL); on macOS, Optima
and Avenir Next are already present. Override any role via `fonts:`, or retune the
defaults in [`src/fonts.typ`](src/fonts.typ).

## Known limitations

- Subsection / subsubsection are styled blocks, not true run-in headings.
- A `fonts:` override reaches body / math / heading; the `note` voice and knot labels
still read `default-fonts` directly (full theming is a later pass).

## Internals

`src/` is modular: [`colors.typ`](src/colors.typ) (palettes + theme state) ·
[`fonts.typ`](src/fonts.typ) · [`visual.typ`](src/visual.typ) ·
[`theorems.typ`](src/theorems.typ) (frame-it knots) ·
[`lib.typ`](src/lib.typ) (pedagogy, margins, cover, wrapper).

## License & credits

MIT © Eric Yang. An independent Typst reimplementation, inspired by the design of the
[loom-notes](https://github.com/Polaris-Aeterna/loom-notes) XeLaTeX project (no source
code reused). Example notes restate cited results for educational use; the mathematics
belongs to its authors.
181 changes: 181 additions & 0 deletions packages/preview/scholia/0.1.0/examples/example.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// example.typ — the Scholia showcase. Compile THIS file to see every device.
// For dark or book mode, uncomment an option below (mirrors template/main.typ).
#import "@preview/scholia:0.1.0": *

#show: scholia.with(
// theme: "dark", // light (default) | dark (slate)
// prose: "book", // notes (default) | book (first-line indent, tight)
)

#cover(
"Descent",
subtitle: "A fill-in primer on convexity and first-order methods",
author: "Eric Yang",
date: datetime.today().display("[month repr:long] [day], [year]"),
kicker: "Optimization · Notebook 4",
)

// ===========================================================================
// NOTATION AND SETUP
// Shows: notation, axiom, assumption, keyword, sidenote, recall
// ===========================================================================

= Notation and Setup

Before the main arguments, we fix symbols and state the #keyword[standing hypotheses]
that hold throughout without being repeated.

#notation[Throughout this chapter][
$x, y, z in bb(R)^n$ are vectors; $f, g: bb(R)^n -> bb(R)$ are real-valued.
The Euclidean inner product is $x^top y$ and the induced norm $||x|| = sqrt(x^top x)$.
We write $f^* = inf_x f(x)$ for the global minimum value and $x^*$ for any minimiser.
]

#axiom[Finite-dimensional Hilbert space][
$bb(R)^n$ is equipped with the standard inner product $x^top y$, which is bilinear,
symmetric, and positive-definite. Every bounded sequence has a convergent subsequence
(Bolzano–Weierstrass).
]

#assumption[Regularity][
Unless stated otherwise, $f$ is #keyword[convex], continuously differentiable, and
bounded below: a minimiser $x^*$ exists so that $f^* > -infinity$.
]

#sidenote[Non-differentiable convex $f$ needs *subgradients* — the subject of a later notebook.]

#recall[Why must we assume $f^* > -infinity$? Give a convex $f$ for which it fails.]

// ===========================================================================
// CONVEX FUNCTIONS
// Shows: note + whisper, definition, lemma, cross-ref @, block-head, trigger
// ===========================================================================

= Convex Functions

A function is #keyword[convex] when its graph never climbs above any of its chords.
That single picture — a bowl with no false bottoms — is why some optimisation is easy
and the rest is a fight to make problems look like this one.

#note[
Read this layer for the shape of the idea; fill the blanks and close the proof gaps
to make it yours. #whisper[The margin is where you argue back with the text.]
]

#definition[convex function][
$f: bb(R)^n -> bb(R)$ is #keyword[convex] if, for every $x, y$ and $lambda in [0, 1]$,
$ f(lambda x + (1 - lambda) y) <= lambda f(x) + (1 - lambda) f(y). $
It is #keyword[strictly convex] when the inequality is strict for $x != y, lambda in (0,1)$.
] <def:convex>

#recall[Which way does the inequality face — and what becomes of it for concave $f$?]

== The first-order picture

Once $f$ is differentiable, convexity wears a cleaner face: every tangent plane is a
global _underestimate_ of the function.

#lemma[first-order condition][
$f$ is convex if and only if, for all $x, y$,
$ f(y) >= f(x) + nabla f(x)^top (y - x). $
] <lem:fo>

#block-head[Reading the gradient]
The gradient $nabla f(x)$ is the slope of the best linear underestimate at $x$.
A point where $nabla f(x) = 0$ is therefore not merely flat — by @lem:fo it is a
*global* minimum. Convexity turns a local check into a global guarantee.

#trigger[Reach for @lem:fo whenever you must turn "it's convex" into an inequality.]

// ===========================================================================
// GRADIENT DESCENT
// Shows: definition (L-smooth), theorem + tag badge, proof + claim + TODO,
// corollary, table + label-it, proposition, conjecture,
// example, yourturn + fillin + workspace, sidenote, remark
// ===========================================================================

= Gradient Descent

#definition[$L$-smoothness][
$f$ is #keyword[$L$-smooth] if its gradient is $L$-Lipschitz:
$ ||nabla f(x) - nabla f(y)|| <= L ||x - y|| quad "for all" x, y. $
] <def:smooth>

Two assumptions now sit on the table — convex (a global shape) and smooth (a local
speed limit on gradients). Together they are exactly what a first-order method needs
to make promises.

// The middle slot [Nesterov §2.1] is a visible source badge, independent of <thm:descent>.
#theorem[descent lemma][Nesterov §2.1][
If $f$ is $L$-smooth then, for all $x, y$,
$ f(y) <= f(x) + nabla f(x)^top (y - x) + L / 2 ||y - x||^2. $
] <thm:descent>

#proof[
Write the remainder $r(y) = f(y) - f(x) - nabla f(x)^top(y-x)$ as an integral.

#claim[
$r(y) = integral_0^1 [nabla f(x + t(y-x)) - nabla f(x)]^top (y-x) d t.$
]

Apply the triangle inequality, then bound the bracketed gradient difference
using @def:smooth.
#TODO[carry out the Lipschitz bound on $||nabla f(x + t(y-x)) - nabla f(x)||$]
]

#corollary[the $O(1 slash t)$ rate][
On a convex, $L$-smooth $f$, gradient descent $x_(t+1) = x_t - eta nabla f(x_t)$
with step $eta = 1 slash L$ satisfies
$ f(x_t) - f^* <= (||x_0 - x^*||^2) / (2 eta t). $
Bounded suboptimality, no luck required — it leans on @thm:descent and @lem:fo.
] <cor:rate>

The cost of each assumption is legible in the rate it buys:

#table(
columns: (1fr, auto),
stroke: none,
table.header[#label-it[assumptions]][#label-it[rate of $f(x_t) - f^*$]],
table.hline(stroke: 0.4pt),
[convex + $L$-smooth], [$O(1 slash t)$],
[$mu$-strongly convex + $L$-smooth], [$O((1 - mu slash L)^t)$ — linear],
)

#proposition[strong convexity buys speed][
If $f$ is additionally $mu$-strongly convex, the rate of @cor:rate sharpens to linear:
$f(x_t) - f^* <= (1 - mu slash L)^t (f(x_0) - f^*)$.
]

#conjecture[lower bound on first-order methods][
No deterministic first-order method on convex, $L$-smooth objectives can achieve
convergence faster than $O(1 slash t)$ per gradient query without momentum.
#whisper[Nesterov's accelerated gradient achieves $O(1 slash t^2)$ under the same
assumptions — suggesting the tight lower bound is below $O(1 slash t)$. The exact
constant is still an active research question.]
]

#example[one step on a quadratic][
For $f(x) = 1/2 x^top A x$ with $0 prec.eq A prec.eq L I$, the gradient step
$x_1 = x_0 - eta A x_0$ gives the error contraction
$ ||x_1 - x^*|| = ||I - eta A|| dot ||x_0 - x^*||. $
With $x^* = 0$: the worst-case shrink factor is $max(|1 - eta lambda_"min"|, |1 - eta lambda_"max"|)$.
]

#yourturn[
Derive the contraction yourself. Expand $||x_(t+1) - x^*||^2$ for the quadratic
$f(x) = 1/2 x^top A x$, then find the step $eta$ that minimises the contraction factor.
What is the optimal $eta$?
$ eta^* = #fillin(width: 2cm) $
#workspace(n: 3)
]

#sidenote[What fails when $f$ is convex but *not* smooth? Subgradients survive,
but the rate slows to $O(1 slash sqrt(t))$.]

=== Stochastic steps, in one line

#remark[
Replace $nabla f$ with any unbiased gradient estimate and the same convergence
machinery survives in expectation — the quiet engine inside essentially every modern
learning system.
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions packages/preview/scholia/0.1.0/src/colors.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Color palettes + active-theme state.
// Two cards switch seamlessly via `scholia.with(theme: "light" | "dark")`.
// Scheme: "cool modern STEM" — blue (theorem) / teal (definition) / amber (example).
//
// Roles: bg (page) · ink (body) · muted (secondary) · hairline (faint rules) ·
// thm/def/eg (knot accents) · rule (section rule) · tag · keyword.

#let palettes = (
light: (
dark: false,
bg: white,
ink: rgb("#16202E"),
muted: rgb("#6B7686"),
hairline: rgb("#D2D8E0"),
thm: rgb("#2F5EA8"),
def: rgb("#0F766E"),
eg: rgb("#B45309"),
rule: rgb("#2F5EA8"),
tag: rgb("#0F766E"),
keyword: rgb("#0F766E"),
),
dark: (
dark: true,
bg: rgb("#141B27"), // deep blue-grey slate
ink: rgb("#E3E8F0"),
muted: rgb("#8893A4"),
hairline: rgb("#2A3340"),
thm: rgb("#74A6FF"),
def: rgb("#54C8B6"),
eg: rgb("#E0A458"),
rule: rgb("#74A6FF"),
tag: rgb("#54C8B6"),
keyword: rgb("#54C8B6"),
),
)

// the active palette; the document wrapper updates it from the `theme` option.
#let active = state("scholia-palette", palettes.light)

// a soft box fill: blend an accent toward the page background.
#let tint(p, c, strength: auto) = {
let s = if strength != auto { strength } else if p.dark { 16% } else { 9% }
color.mix((c, s), (p.bg, 100% - s))
}
23 changes: 23 additions & 0 deletions packages/preview/scholia/0.1.0/src/fonts.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Font roles as fallback lists: the "nice" face first, an embedded face last.
// Typst embeds Libertinus Serif, New Computer Modern (+ Math), DejaVu Sans Mono —
// so every list degrades to something that exists everywhere, zero install.
// (A Typst package cannot ship fonts; downstream users only have embedded +
// their own system fonts. Missing first choices just warn and fall back.)

#let default-fonts = (
body: ("STIX Two Text", "Libertinus Serif"),
math: ("STIX Two Math", "New Computer Modern Math"),
heading: ("Optima", "Libertinus Serif"),
sans: ("Avenir Next", "Libertinus Serif"), // the informal "intuition voice"
mono: ("Menlo", "DejaVu Sans Mono"),
cjk: ("Songti SC",), // appended after body/sans for 中文 glyph fallback
)

// shallow-merge user overrides over the defaults
#let resolve-fonts(overrides) = {
let f = default-fonts
if overrides != none {
for (k, v) in overrides { f.insert(k, v) }
}
f
}
Loading
Loading