Skip to content

Commit 209b79d

Browse files
akwiatkowskiclaude
andcommitted
feat: KaTeX math, post previews, table styling, wind turbine rewrite, Miasteczko.jl project
- Add self-hosted KaTeX (vendor/katex/) with client-side rendering, opt-in via math: true - Configure Goldmark passthrough extension for $$ and \(\) delimiters - Fix post previews: use frontmatter description instead of .Summary (prevents heading leak) - Add description subtitle to single post and project page layouts - Add taxonomy page description with plainify fallback - Add table CSS: subtle borders, uppercase headers, zebra striping - Rewrite wind turbine post: restructured narrative, physics calculations with source - Add Miasteczko.jl project page with full writeup, Wikipedia reference links - Add CSS lint-css Makefile target Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent be06a24 commit 209b79d

74 files changed

Lines changed: 239 additions & 18 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: dev build new clean help
1+
.PHONY: dev build new clean lint-css help
22

33
HUGO := mise exec -- hugo
44

@@ -18,6 +18,19 @@ new:
1818
clean:
1919
rm -rf public/ resources/_gen/
2020

21+
## lint-css: validate CSS syntax
22+
lint-css:
23+
@python3 -c "\
24+
css = open('themes/homeio/static/css/style.css').read(); \
25+
depth = 0; errors = []; \
26+
for i, c in enumerate(css): \
27+
if c == '{': depth += 1; \
28+
elif c == '}': depth -= 1; \
29+
if depth < 0: errors.append(f'Unmatched }} at line {css[:i].count(chr(10))+1}'); break; \
30+
if depth > 0: errors.append(f'Unclosed {{ — depth {depth} at end'); \
31+
[print(f'ERROR: {e}') for e in errors] or print('CSS braces OK'); \
32+
exit(1 if errors else 0)"
33+
2134
## help: show this help
2235
help:
2336
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'

content/posts/2007/2007-setting-up-wind-turbine.md

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,75 @@ date: 2007-01-01
44
draft: true
55
topics: ['energy']
66
tags: ['wind', 'renewable', 'diy']
7-
description: 'Setting up a small wind turbine — planning, installation, and first results.'
7+
description: 'A cheap wind turbine, a homemade braking system, and a November storm that changed how I think about control systems.'
8+
math: true
89
---
910

10-
## Why?
11+
## The setup
1112

12-
This is the start of one of my longest-running projects. We found wind turbines being sold on Allegro.pl. To be honest, I don't remember **why** we decided to buy one. I do remember the initial hardware was **not ready**.
13+
This was 2007. Photovoltaic panels were exotic and expensive. If you wanted to generate your own electricity in Poland, wind was about the only option. We found small wind turbines being sold on Allegro.pl — Chinese-made kits, 1kW rated, 48V output. To be honest, I don't remember **why** we decided to buy one. Curiosity, probably.
1314

14-
We bought 4x gel batteries which cost about >1000 PLN at the time. Quite expensive compared to current (2026) battery prices and capacity.
15+
The kit came with blades, a three-phase permanent magnet generator, and a cheap inverter (48V → 230V AC — I'm not sure it even produced a pure sine wave). We added 4× gel batteries, which cost over 1,000 PLN at the time expensive compared to what you'd pay for lithium cells today.
1516

16-
### Braking system
17+
The whole package looked like the design team had only thought about the happy path.
1718

18-
In theory the wind turbine should brake mechanically by turning 90 degrees to the wind at high speed. It didn't. So I had to build a braking device. It used a high-power resistor to first slow down the turbine by putting resistance on all coils. After a second, other switches short-circuited all coils to make resistance as low as possible. The turbine was nearly not rotating after that.
19+
## The braking system
1920

20-
## Accident
21+
In theory, the turbine should brake mechanically by turning its tail 90° to the wind at high speed. It didn't.
2122

22-
One late November night, high winds were predicted. We had an old APC UPS converting 48V from the batteries, because the inverter included in the kit was not good enough. I turned on a vacuum cleaner to use up the energy being generated. The current swung between 3–20A from second to second. A very big swing.
23+
So I built a braking device. First stage: high-power resistors connected across all three coils to slow the rotor down through electrical resistance. After about a second, relays short-circuited all coils — dropping resistance as low as possible. The turbine was nearly stopped after that.
2324

24-
A few hours later I noticed I lost connection to the turbine. The blades were spinning like a helicopter. Not a good thing. A Chernobyl-series kind of accident. Next day winds dropped and we lowered the whole turbine.
25+
Braking took a few seconds. All the kinetic energy stored in the spinning blades was converted to heat, dissipated in the resistors.
2526

26-
From that moment I knew we needed a control system. Something that could monitor all vital parameters and allow remote control. Keep in mind this was long before Home Assistant, Zigbee, and similar technologies.
27+
How much energy is that? I don't have specs from the original turbine, but a [comparable Chinese 1kW unit](https://bessent.en.made-in-china.com/product/bfZryBRxgmcP/China-Small-Size-3-5-Blades-1kw-24V-48V-Horizontal-Axis-Wind-Turbine-Generator.html) (Bessent NE-1000M5, 3 blades, 48V, 3-phase permanent magnet AC) weighs 19 kg total. Subtracting the generator, tail, and mount, the rotor is probably 6–9 kg — roughly 2–3 kg per blade at ~1.5m length.
2728

28-
And that's where a lot of my ideas started.
29+
Approximating each blade as a uniform rod rotating about one end, the [moment of inertia][moment-of-inertia] is:
30+
31+
$$I_{blade} = \frac{1}{3} m L^2$$
32+
33+
For three blades (hub contribution negligible):
34+
35+
$$I_{total} = 3 \times \frac{1}{3} m L^2 = m L^2$$
36+
37+
The rotor speed depends on wind — the [tip-speed ratio][tsr] (TSR) for cheap 3-blade turbines is typically 4–6. At high wind speeds when braking matters most, the blades spin fast. Taking rated wind (~12 m/s) as an example:
38+
39+
$$\omega = \frac{\text{TSR} \times v_{wind}}{R}$$
40+
41+
The kinetic energy stored in the spinning rotor:
42+
43+
$$E_k = \frac{1}{2} I_{total} \; \omega^2$$
44+
45+
These are rough estimates — I don't have the exact blade mass or RPM data from our turbine. But plugging in reasonable assumptions gives an idea of the scale:
46+
47+
| Assumption | Blade mass | TSR | ω (rad/s) | RPM | E_k |
48+
|---|---|---|---|---|---|
49+
| Conservative | 2 kg | 5 | 40 | ~380 | ~3.6 kJ |
50+
| Upper estimate | 3 kg | 6 | 48 | ~460 | ~7.8 kJ |
51+
52+
During braking, all of this kinetic energy gets converted to heat in the three 5Ω resistors over a few seconds. That's roughly **400–870W average per resistor** — and the peak power at the start is even higher, since power drops with ω² as the rotor decelerates. These numbers apply at rated wind speed; in storm conditions with the rotor spinning even faster, the energy scales with the square of RPM.
53+
54+
That's why the resistors had to be serious, high-power units bolted to a heat sink.
55+
56+
## The accident
57+
58+
One late November night, high winds were predicted. We had an old APC UPS converting 48V from the batteries, because the inverter from the kit wasn't good enough. I turned on a vacuum cleaner to burn off the energy being generated. The current was swinging between 3 and 20 amps from second to second. A wild ride.
59+
60+
A few hours later I noticed I'd lost connection to the turbine. The blades were spinning like a helicopter. A Chernobyl-series kind of moment. The next day winds dropped and we lowered the whole assembly.
61+
62+
## What came next
63+
64+
From that night I knew we needed a proper control system — something that could monitor all vital parameters and allow remote braking. Keep in mind, this was 2007. No Home Assistant, no Zigbee, no cheap microcontrollers with WiFi. Building that system meant designing the hardware and software from scratch.
65+
66+
That led to building the first iteration of wind turbine firmware — a microcontroller monitoring voltage, current, and RPM, with a simple backend and a web frontend for remote control. A few years later, that system grew beyond the turbine. It became a general-purpose home monitoring platform — the first version of what I eventually called HomeIO, which is also the name of this blog.
67+
68+
But the seed was that November night and a turbine spinning out of control.
2969

3070
## Lessons learned
3171

32-
- Don't trust cheap niche products to work out of the box.
33-
- Wind is stochastic and complicated by nature.
34-
- Gel batteries are decent, but not good enough.
72+
- Don't trust cheap products to work outside the happy path. The mechanical braking, the inverter, the overall system design — all failed under real conditions.
73+
- Wind is stochastic by nature. Equipment needs safe margins for the worst case, not the average case.
74+
- Gel batteries were decent for 2007, but the energy density and cycle life were poor compared to what's available now.
75+
- If you can't monitor it, you don't know what's happening. The turbine had no telemetry — we only found out it failed because the connection dropped.
76+
77+
[moment-of-inertia]: https://en.wikipedia.org/wiki/Moment_of_inertia
78+
[tsr]: https://en.wikipedia.org/wiki/Tip-speed_ratio

content/projects/miasteczko-jl.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
title: 'Miasteczko.jl'
3+
date: 2026-04-18
4+
draft: true
5+
topics: ['urban transport']
6+
tags: ['simulation', 'optimization', 'gravity-model', 'public-transport', 'osm']
7+
tech: ['Julia', 'JuMP', 'React', 'TypeScript', 'MapLibre', 'Electron', 'Luxor.jl', 'Cairo']
8+
description: 'Bottom-up transport planning for Polish towns — simulate demand first, design supply second. 266 corridors discovered automatically from a gravity model over real OSM data.'
9+
status: 'in-progress'
10+
---
11+
12+
## Goal
13+
14+
Design public transport networks from the ground up: simulate how people actually move, then build routes that serve that demand. The opposite of the usual top-down approach where planners draw lines on a map and hope ridership follows.
15+
16+
The library works at two scales — town-level (MicroSim) and regional (MacroSim) — both built on real [OpenStreetMap][osm] data for Polish settlements.
17+
18+
## Approach
19+
20+
### MicroSim — how does this town move?
21+
22+
Per-person, per-minute simulation of a single town:
23+
24+
1. **OSM extraction** — buildings, roads, points of interest, rail stations from a Poland [PBF file][pbf] (a compact binary format for OpenStreetMap data)
25+
2. **Population synthesis** — households placed in real residential buildings, workers assigned to workplaces, students to schools ([GUS][gus] census demographics)
26+
3. **Activity simulation** — agents follow daily schedules driven by a needs system (hunger, energy, social). 60-second tick resolution over multiple days.
27+
4. **Demand aggregation** — individual trips become an [origin-destination (OD) matrix][od-matrix] by zone and time-of-day
28+
5. **Bus network generation** — greedy stop placement at intersections, demand-driven route building, overlap detection, express routes for long corridors
29+
6. **Timetable generation** — per-route demand-proportional departures, bidirectional schedules, timed transfer hubs ([Taktfahrplan][taktfahrplan]-style pulse — a Swiss method where all lines meet at hubs simultaneously)
30+
7. **Feedback loop** — simulate with transit → measure ridership → adjust timetable → repeat until stable
31+
32+
Mode choice is probabilistic ([logit model][logit]) across walk, bike, e-bike, bus, and car — using actual road network distances, weather conditions, and trip chaining (if you bused to work, you bus back).
33+
34+
### MacroSim — how does this region connect?
35+
36+
Regional-scale simulation where vehicles (trains) are the agents, not people:
37+
38+
1. **Settlement graph** — 2,987 settlements extracted from Poland PBF, three-tier hierarchy (cities >50k, towns 5–50k, villages <5k)
39+
2. **Corridor discovery** — 109 backbone + 157 feeder = 266 corridors found automatically from rail connectivity (see examples below)
40+
3. **[Gravity demand model][gravity-model]** — OD matrix with population-based attraction, distance decay, and employment scoring from OSM amenity density
41+
4. **10-pulse system** — composable demand generators (morning commute, school, university weekly, shopping, nightlife, healthcare, leisure, family visits). Each pulse fires on specific day types — the simulation distinguishes 4: regular weekday (Mon–Thu), Friday (university exodus + nightlife), Saturday (shopping peak), and Sunday (leisure + family). Averaging into just "weekday" and "weekend" would hide real phenomena like the Friday afternoon wave of students heading home.
42+
5. **Vehicle simulation** — deterministic train traversal with capacity constraints and automatic relief train insertion
43+
6. **Junction transfers** — detect stations on 2+ corridors, route transfer demand through them with 5-minute penalty, align timetables at junctions
44+
45+
Calibrated against Poland PBF to produce 3.8M daily passengers — intentionally optimistic for a green transport scenario.
46+
47+
### Corridor discovery
48+
49+
The system discovers rail corridors automatically from OpenStreetMap track connectivity — no manual line definitions, no [PKP][pkp] data. It finds city-to-city paths through the station graph, then generates feeder corridors from uncovered towns to their nearest city.
50+
51+
Top 5 discovered corridors by length:
52+
53+
| Corridor | Distance | Stations |
54+
|----------|----------|----------|
55+
| Szczecin — Gdańsk | 356 km | 52 |
56+
| Łódź — Gdańsk | 352 km | 45 |
57+
| Elbląg — Łódź | 334 km | 43 |
58+
| Gliwice — Warszawa | 333 km | 51 |
59+
| Ełk — Gdańsk | 327 km | 52 |
60+
61+
These roughly match real PKP lines — without being told they exist. The gravity model then assigns demand to each corridor based on population and distance.
62+
63+
<small>Validated on Poland PBF, commit `4fc5d0f` (2026-04-18).</small>
64+
65+
### Economics module
66+
67+
Full financial modeling per route: distance-based fares (power-law), fuel/energy costs (diesel and electric), CO₂ accounting with car-counterfactual savings, and four subsidy types (deficiency grant, per-passenger-km, CO₂ bonus, accessibility bonus for underserved areas).
68+
69+
### UI — Desktop preview app
70+
71+
The presentation layer went through several iterations. First attempt was [CImGui][cimgui] (immediate-mode GUI with GLFW/ModernGL) — functional but limited for map-heavy workflows. A quick [Bonito.jl][bonito] proof-of-concept validated that Electron + web rendering was the right direction. The production app landed on React + TypeScript + [MapLibre][maplibre] + [Oxygen.jl][oxygen] + Electron.
72+
73+
The app is a submodule (`supapp/lib`) primarily used for presenting simulation results: [MapLibre][maplibre] maps, floating detail windows, sortable data tables, route/stop/vehicle detail panels, time-distance diagrams, passenger flow charts, and a system-wide economics dashboard. The Julia backend serves a JSON API via Oxygen.jl, so generated results can also be integrated with other frontends through the web server.
74+
75+
![MicroSim — route detail, passenger flow, and stop timetable for Szamocin](/img/projects/miasteczko-jl/microsim.png)
76+
77+
### UI — Isometric city renderer (in progress)
78+
79+
IsoRender module — a proof-of-concept to render towns in a [Simutrans][simutrans]/[OpenTTD][openttd]-style isometric view using real OSM geometry. Unlike those games, it doesn't snap to a tile grid — buildings and roads keep their actual footprints, projected into 2:1 [dimetric perspective][dimetric]. Built with [Luxor.jl][luxor] (Cairo), using building extrusion for 3D appearance and [painter's algorithm][painters-algo] depth sorting. Outputs high-resolution PNG images.
80+
81+
## Outcome
82+
83+
- **17 modules**, 3,900+ tests, all passing
84+
- Validated on three towns: Szamocin (17k), Chodzież (64k, 3 rail stations), Margonin (22k)
85+
- MacroSim pipeline runs on full Poland PBF — 266 corridors, 92 semi-fast + 23 express service patterns
86+
- MicroSim feedback converges in 2–3 cycles to stable timetables
87+
- Three train models with physics-based travel times (EN57, Elf 2, SA134)
88+
89+
## What I learned
90+
91+
- Bottom-up transport planning produces surprisingly reasonable networks — the gravity model discovers corridors that roughly match real PKP lines, without being told they exist
92+
- Mode choice modeling is extremely sensitive to small parameter changes — [distance decay][distance-decay] β controls how fast demand drops with distance (higher β = people less willing to travel far). A 0.1 shift can flip 20% of trips between bus and car
93+
- The pulse system for demand generation is more expressive than expected — composing 10 independent travel patterns across 4 day types captures real phenomena that a simple weekday/weekend split would miss
94+
- Julia's type system and multiple dispatch made the module architecture natural — each module produces typed structs, the next module consumes them, no serialization layer needed
95+
96+
[osm]: https://en.wikipedia.org/wiki/OpenStreetMap
97+
[pbf]: https://wiki.openstreetmap.org/wiki/PBF_Format
98+
[gus]: https://en.wikipedia.org/wiki/Statistics_Poland
99+
[od-matrix]: https://en.wikipedia.org/wiki/Origin%E2%80%93destination_matrix
100+
[taktfahrplan]: https://en.wikipedia.org/wiki/Taktfahrplan
101+
[logit]: https://en.wikipedia.org/wiki/Discrete_choice#Logit
102+
[gravity-model]: https://en.wikipedia.org/wiki/Gravity_model_of_trade
103+
[pkp]: https://en.wikipedia.org/wiki/Polish_State_Railways
104+
[maplibre]: https://en.wikipedia.org/wiki/MapLibre
105+
[simutrans]: https://en.wikipedia.org/wiki/Simutrans
106+
[painters-algo]: https://en.wikipedia.org/wiki/Painter%27s_algorithm
107+
[distance-decay]: https://en.wikipedia.org/wiki/Distance_decay
108+
[cimgui]: https://github.com/ocornut/imgui
109+
[bonito]: https://github.com/MakieOrg/Bonito.jl
110+
[oxygen]: https://github.com/ndortega/Oxygen.jl
111+
[openttd]: https://en.wikipedia.org/wiki/OpenTTD
112+
[dimetric]: https://en.wikipedia.org/wiki/Axonometric_projection#Three_types
113+
[luxor]: https://github.com/JuliaGraphics/Luxor.jl

hugo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ enableRobotsTXT = true
2323
[markup.goldmark]
2424
[markup.goldmark.renderer]
2525
unsafe = true # allow raw HTML in markdown (for embeds, charts)
26+
[markup.goldmark.extensions]
27+
[markup.goldmark.extensions.passthrough]
28+
enable = true
29+
[markup.goldmark.extensions.passthrough.delimiters]
30+
block = [['\[', '\]'], ['$$', '$$']]
31+
inline = [['\(', '\)']]
2632

2733
# Main navigation
2834
[menus]

0 commit comments

Comments
 (0)