Dulus
An AI agent for your desktop and terminal, on any model you choose. Hosted models run on Fuel, paid in $DULUS.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
Submission for Congressional App
翻訳待ち
Loading the full README.
Read the full README ↗School districts decide heat-related early dismissals from the outdoor forecast. Individual classrooms are never measured, and a second-floor southwest room with no AC can be far hotter than the outdoor reading the decision rests on.
Thermion predicts per-room conditions from building physics, sharpens those predictions with sparse human reports through a per-room Kalman filter, and recommends which classes to move.
A heat-wave scenario toggle (dashboard and plan) runs the same physics on a clearly labeled synthetic June heat wave, so the model can be explored outside heat season. It is never mixed with live data.
Requires Node 20 or newer.
npm install
npm run dev # http://localhost:3000
npm test # vitest: physics, filter, optimizer, NWS client, DB
npm run build # production build (type-checks)
The SQLite database is created at ./data/thermion.db on first request. Migrations run automatically, and the seed adds one school (Morris Hills High School), 16 rooms and 14 classes. No reports are seeded.
Environment variables (all optional):
| Variable | Purpose |
|---|---|
NWS_USER_AGENT | User-Agent sent to api.weather.gov. NWS asks for an app name plus contact info; set this in production. |
THERMION_DB | Path to the SQLite file (default ./data/thermion.db, or /tmp/thermion.db on Vercel). |
The only external network dependency is api.weather.gov. There is no auth.
npx vercel deploy --prod
Vercel's filesystem is read-only apart from /tmp, so the database lives in /tmp/thermion.db there. It is ephemeral per serverless instance: reports survive while an instance stays warm, but a cold start re-seeds an empty database. That is fine for a demo. A production deployment should mount a persistent volume (any Node host) or point THERMION_DB at durable storage.
api.weather.gov
│ /points → forecastHourly (30-min cache, Result<…>, never throws)
▼
┌──────────────────────── lib/nws.ts ─────────────────────────┐
│ hourly periods (temp, RH, sky cover from NWS sky wording) │
└───────────────┬──────────────────────────────────────────────┘
│ logged hourly into weather_hours (past hours stay available)
▼
lib/weather.ts ── live │ stale (logged) │ climatology fallback → banner
│
▼ PURE MODULES (no I/O, unit-tested)
┌────────────────────────────────────────────────────────────────────┐
│ solar.ts NOAA solar position → elevation, azimuth │
│ irradiance.ts Kasten-Young AM, Meinel beam, diffuse, cos θ on a │
│ vertical window, cloud factor → watts into room │
│ thermal.ts dT/dt = invRC·(T_drive − T) + alpha·Q (Euler, 5 min)│
│ kalman.ts EKF on [T, invRC, alpha], χ² gating, ±σ tracks │
│ heatindex.ts NWS Rothfusz + adjustments │
│ thresholds.ts 82 advisory / 88 unusable │
│ hungarian.ts Kuhn-Munkres O(n³) │
│ reassign.ts cost matrix, feasibility, padding, plan │
└────────────────────────────────────────────────────────────────────┘
│ ▲
▼ │
lib/roomModel.ts recordReport(): insert report → propagate → update
│ → room_state + filter_log (one transaction)
│ buildForecast(): per-room tracks with ±σ, re-anchored at
│ each accepted report
▼
app/dashboard app/report (server action) app/plan (+ /plan/export CSV)
│
▼
SQLite (better-sqlite3): schools, rooms, reports, room_state, classes,
weather_hours, filter_log (PRAGMA user_version migrations)
/report posts to a server action, which validates the input (temp 40–130 °F).room_state, and append a filter_log row (accepted or rejected, with NIS and the posterior).State x = [T, invRC, alpha]. The process model is one Euler step of the thermal model. invRC and alpha follow random walks, so the filter learns each room's parameters. The Jacobian is derived analytically in lib/kalman.ts:
⎡ 1 − Δt·k Δt·(T_d − T) Δt·Q ⎤
F = ⎢ 0 1 0 ⎥
⎣ 0 0 1 ⎦
A report observes T directly (H = [1, 0, 0]). Measurement noise is 1.0 °F (1σ) for teachers and staff and 2.0 °F for students. Reports with normalized innovation squared above χ²₁(0.99) = 6.635 are rejected and logged. A rejected reading is accepted later only if a second report within 12 hours agrees with it; that is treated as evidence the model, not the reporters, is wrong.
This is the beginning of the README. Read the rest at the original. Read the full README ↗