Build, score, optimize, and view lighting designs by describing them. For AIs (Claude / ChatGPT / agents) and developers.
Base URL https://www.innerscene.com · JSON version of this doc: /api/eml-calc/ai/spec
You never need POST. /score, /optimize, /import and /render all accept a plain GET with the room in a ?room= query param (URL-encoded JSON) — so if you can fetch any URL you can compute real metrics and get a share link. POST (a JSON body) is just an equivalent option, not a requirement. You just fetched this page over GET from this host, so the compute endpoints are reachable the same way — build your room, GET /api/eml-calc/ai/score?room=…, and read the numbers before you answer. "I can't POST" / "the host isn't whitelisted" is not a reason to skip this: GET is enough, and you already reached the host.
score_room / optimize_room / create_design / render_scene) instead of raw HTTP; the schemas and rules travel with the tools./score or /optimize with ?room=<url-encoded JSON> (or a POST body), read the returned per-occupant fc / mel-EDI / UGR, and return the share url./api/eml-calc/ai/import?room=<url-encoded full JSON> link that opens the complete model (room, fixtures, occupants, targets) and computes it in the user's browser. Give that. It is strictly better than the bare ?preset=…&count=… quick-link, which drops the occupants and targets and so produces no per-seat numbers. See GET-only links & compute.json code block. Always include it. Never reply with prose alone, and never ask the user to run HTTP calls themselves. Then tell them to paste it into the Import window and click Open./score or /optimize response, or the browser after the user opens the link). If you could not compute, say so plainly and hand over the link + JSON — never present the worked example's layout or numbers (4× CS24, ~43 fc, etc.) as if they were this room's answer, and never quote a figure you did not compute.
This page is written for AI assistants, so you don’t have to read it. Open Claude or ChatGPT, paste one of the prompts below, and edit the [bracketed] parts. The assistant reads this guide and does the rest.
A. Describe your space in words
B. From an annotated drawing — attach a floor plan or reflected ceiling plan (image or PDF) with dimensions and fixtures. Use a vision-capable model: Claude Opus 4.8 (or Sonnet 4.6), or ChatGPT (GPT-4o or later).
What you get back: an agent that can browse or call APIs (e.g. Claude Code) returns a link you open directly. A chat-only assistant returns a small block of room JSON — open the Import window in CircadianLab, paste it, and click Open (or use the standalone Import page). The most you ever do is paste text and click a link — no terminal, code, or installs.
A real exchange in an agentic assistant (e.g. Claude Code). The assistant makes the HTTP calls itself; the user just talks.
Here's the horizontal footcandle plan with iso-lines. Each panel sits directly over a workstation, so all four desks read ~43 fc — comfortably above the 30 fc target (the green marker on the scale below is the 30 fc office line). Want the mel-EDI view instead, or a warmer 5000 K look?The figures in this exchange (~43 fc, the pass/fail marks) are illustrative, not a computed result for any specific room — reproduce them only by calling /score or /optimize, or by letting the browser compute after the user opens the link. Below: the full reference behind those calls — schema, presets, the fixture library, endpoints, and more examples.
The user describes a space and lighting goals in words or from a drawing — e.g. “a 20×10 ft room with 3 Circadian Sky 2×4s” or “an office that hits 30 fc on desks, 250 mel-EDI at each workstation, glare under 25 UGR.” You turn that into a room, validate or optimize it, and return a link they can open.
createscore / validateoptimize to targetsview (share link + PNG)
Origin is the south-west floor corner. X = east (width), Y = north (depth), Z = up (height). Set "units":"ft" (default) or "m"; it applies to every length and to fixture / occupant x/y/z.
circadian-lab/room@1[ {…}, {…} ]); /import, /score and /optimize take one room object and reject an array. For a multi-room floor, emit a separate JSON (and link) per room.partitions[], or split an open area into enclosed rooms, that are not in the plan. On a reflected ceiling plan a 2×2 / 2×4 ceiling grid or a fixture layout is not a wall — only solid wall linework (or a dimensioned enclosure) is. If a space reads as open plan, model it as one open room with no partitions. If it's genuinely unclear whether a space is enclosed, ask, or model it open and say so.One JSON object. Top-level fields:
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
schema | string | no | "circadian-lab/room@1" | Version tag. Unknown values warn but still parse. |
units | "ft" | "m" | no | "ft" | Applies to all lengths and coordinates. |
room | object | yes | — | Box dimensions + surface reflectances. See below. |
fixtures | array | object | yes* | — | Explicit list, or a {preset,count} auto-grid. *≥1 needed to score. |
customFixtures | array | no | [] | Fixtures not in the preset library (supply IES + CCT). |
occupants | array | no | [] | Measurement points. Needed for per-occupant scoring / optimize. |
furniture | array | no | [] | Desks (cast light onto a surface, add mild shadowing). |
partitions | array | no | [] | Interior wall segments. |
targets | object | no | null | Pass/fail thresholds. Required by /optimize. |
optimize | object | no | — | Search knobs for /optimize (fixture, ladders, grids). |
view | object | no | 2D / eml | Initial view of the share. |
windows | array | no | [] | Real daylight apertures — windows & skylights. See Daylight. |
daylight | object | no | — | Sun + sky scene (lat/lon + date/time). Adds daylight through the windows. See Daylight. |
maintenanceFactor | number | no | 0.8 | Light-loss factor (dirt/depreciation), 0–1. |
room| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
width | number | yes* | — | X extent. *Optional if you give a polygon or walls (derived from their bounding box). |
depth | number | yes* | — | Y extent. *Same as width. |
height | number | no | 9 ft / 2.7 m | Ceiling height. |
polygon | [[x,y], …] | no | — | Non-rectangular room: perimeter as a 2D polygon (≥3 points). Each edge becomes a full-height wall; the bounding box is the room. |
walls | [{x1,y1,x2,y2,height?,baseHeight?,reflectance?}] | no | — | Alternative to polygon: an explicit list of wall segments. |
reflectance.walls | number 0–1 | no | 0.5 | Wall reflectance (also used for polygon/wall segments). |
reflectance.floor | number 0–1 | no | 0.3 | Floor reflectance. |
reflectance.ceiling | number 0–1 | no | 0.8 | Ceiling reflectance. |
Non-rectangular rooms: the engine is rectangular, so a polygon (or walls) is modeled as the bounding-box room with each edge as an interior wall that blocks + reflects light — carving out L-shapes, alcoves, etc. Coordinates can use any origin (everything is shifted to the box corner), and if you omit width/depth they’re taken from the polygon’s extent. Place fixtures/occupants explicitly for shaped rooms (auto-grid fills the whole box, including any notch).
fixtures[] — explicit placement| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
preset | string | yes | — | A preset id from §4 (e.g. "CS24"). |
x, y | number | no | room center | Position on the ceiling plane. |
z | number | no | ceiling − 0.05 m | Mounting height. |
cct | number (K) | no | preset default | Only for tunable presets; clamped to the preset range. |
dim | number 0–1 | no | 1.0 | Dimming level. |
rotation | number (deg) | no | 0 | Spins the fixture about its own aim axis (e.g. 90 = landscape→portrait, or turns an asymmetric IES pattern). Does not tilt the beam — use tilt for that. |
tilt | number (deg 0–180) | no | 0 | Aim off vertical: 0 = straight down, 90 = horizontal, 180 = straight up (uplight / cove — washes the ceiling for soft indirect light). Swings the whole beam so you can point a fixture at a wall (wall-wash) or the ceiling (cove). For an uplight, drop z below the ceiling so its upward light is not clipped. |
aimAzimuth | number (deg) | no | 0 | Compass bearing the tilt points toward: 0 = north (+Y), 90 = east (+X), 180 = south, 270 = west (same sense as occupant facing). Only used when tilt > 0. |
count / rows / cols | number | no | — | If present, auto-places a grid of this fixture instead of one. |
regress | number (units) | no | — | Switches to a lay-in (drywall deep regress) install — see the Lay-in section below for all parameters. |
Or pass fixtures as a single object { "preset":"CS24", "count":6, "cct":5000, "dim":1 } to auto-place an even ceiling grid of N (add tilt/aimAzimuth/rotation to aim the whole grid).
Don't overlap footprints. Each ceiling fixture is recessed and cuts a hole its full size (the Size m (w×d) in §4 — e.g. CS24 = 0.63 × 1.24 m). When you place fixtures explicitly, keep centers at least one footprint apart — a full width in x and a full depth in y (accounting for rotation) — so the cut-outs don't collide. Overlapping troffers can't be installed and render as one oversized dark hole; /score and /import return an overlap warning naming the offending pairs, so read the warnings and nudge fixtures apart if any appear.
Aiming a fixture: leave tilt off for a normal downlight. To graze a wall, set tilt (e.g. 30–60) and aimAzimuth toward that wall. Note a fixture at the default ceiling height is treated as flush-recessed, so its upward light is clipped by the ceiling — that's fine for washing down a wall; to throw light up onto the upper wall, drop z a little (e.g. z = ceiling − 0.3 m) so it hangs below the ceiling like an adjustable/pendant. Counter-intuitively, aiming light sideways often raises UGR (more luminance in the viewing zone, dimmer overall room), so verify with /score rather than assuming it lowers glare.
Give a fixture a wall instead of x/y and it mounts flush on that wall, emitting horizontally into the room. In this form a Circadian Sky panel reads as a backlit sky window — most often a standing-height window (center ≈ 1.4 m / 4.5 ft) or a clerestory band set high on the wall.
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
wall | "south|east|north|west" (or 0–3) | yes | — | Which wall — its presence switches the fixture to wall-mount. south = Y 0, north = Y depth, west = X 0, east = X width. Aim is automatically the wall's inward normal (into the room). |
along | number 0–1 | no | 0.5 | Position along the wall (0 = one corner, 1 = the other). 0.5 = centered. |
height | number | no | ≈1.4 m | Center height above the floor (a standing window). In the room's units. |
heightFromCeiling | number | no | — | Alternative to height: center this far below the ceiling — use it for a clerestory. (Ignored if height is also given.) |
orientation | "landscape|portrait" | no | landscape | Long axis horizontal (landscape) or vertical (portrait = a tall, narrow window). |
count | number | no | 1 | Places a row of N evenly along the wall (a window band / clerestory row); overrides along. |
⚠ Watch UGR with wall windows. A wall panel sits in the occupants' field of view, so it delivers strong vertical mel-EDI (excellent circadian dose) but can drive glare (UGR) high — a bright standing-height window directly in front of someone can read UGR > 30 (intolerable). Mitigate by mounting it high (clerestory) rather than at eye level, placing it on a wall occupants don't face head-on, dimming it, and always checking UGR with /score. Never assume a window is comfortable — verify.
Give a Circadian Sky fixture a regress depth and it becomes a lay-in install: the panel lays into a drywall coffer from above (like a ceiling tile), so its luminous window sits regress above the finished ceiling and the room sees it through an opening — reading as a true architectural skylight rather than a flush troffer. The engine models the coffer with real geometry (a ceiling aperture plus white bounce walls), so the physics is faithful: the coffer walls progressively cut off high-angle light, and what they catch bounces back out of the cavity via radiosity.
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
regress | number (units) | yes | — | Coffer depth: how far the luminous window sits above the finished ceiling. Its presence switches the fixture to lay-in. Typical 4″ (0.33 ft / 0.1 m) = 2×4 flat framing + finish; clamped to 0.5–24″ (0.013–0.6 m). Deeper = softer, more skylight-like, stronger high-angle cutoff. |
regressOpening | "glass" | "trim" | no | "glass" | glass (default): the drywall opening is flush with the luminous window, concealing the trim — the install-manual detail. trim: the opening sits at the trim/housing edge (trim visible, slightly softer cutoff). |
regressWall | number (units) | no | 2″ nominal | Coffer wall (framing + finish) thickness. Visual only — the calc always uses the opening line, so this never changes the numbers. |
count | number | no | 1 | Lays a butted row of N forming one continuous slot (fixtures end-to-end; trims within 1/8″ merge into a shared coffer) — the six-fixture CS12 array install. Centered at x/y. |
axis | "x" | "y" | no | "x" | Run direction for the butted row. Use rotation (90° steps) to orient each panel within the run. |
x, y, cct, dim | number | no | center / preset | As for ceiling fixtures. z and tilt are ignored — the window is placed at ceiling + regress automatically, aimed straight down. |
rotation | number (deg) | no | 0 | Snapped to 90° steps — drywall coffers are framed square to the room. |
Photometric consequences — always verify with /score. The coffer vignettes high-angle light (full output at nadir, tapering to a hard cutoff at ≈ atan(window ÷ regress) off vertical). That trades the two circadian metrics against each other: UGR drops for oblique observers (the window disappears from view at shallow angles — often the point of a lay-in), but vertical eye-level mel-EDI also drops versus the same panel flush-mounted, because near-horizontal light is exactly what reaches a standing occupant's eyes. Directly under the fixture, illuminance is nearly unchanged. Rules of thumb: a 4″ regress is mild; 8–12″ is a strong architectural effect with a real dose penalty — compensate with output, count, or CCT and re-score.
Placement rules. Butt lay-ins exactly (use count, or place centers one footprint apart — within 1/8″ reads as continuous, so no coffer wall is built between them) or keep them clearly separate; anything in between builds an unbuildably thin drywall strip between the slots. The overlap warning from /score / /import applies to lay-ins too. Lay-in adds real surfaces to the scene, so calcs run somewhat slower than flush fixtures — normal.
Give a fixture a cove (a wall name) instead of x/y and it becomes a linear LED strip run along that wall, near the ceiling, aimed up. The engine bounces the light off the ceiling (radiosity), so the whole ceiling glows as a soft, low-glare "sky." This is true indirect / cove lighting, the most comfortable way to deliver circadian light to someone lying down and looking up (a patient), because there is no bright source anywhere in the upward view. The bright ceiling is counted as a glare source, so the reported UGR is real (low, not zero). Note a cove is a genuine LED strip, which tops out near 6500 K — so a cove alone delivers only a modest dose (~117 mel-EDI for a two-wall 400 lm/ft cove). To reach a 250 circadian target, pair the cove with a dimmed Circadian Sky panel (the hybrid in the hint below); do not fake a high-CCT "circadian sky strip," no such product exists.
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
cove | "south|east|north|west" (or 0–3) | yes | — | Which wall the strip runs along. Its presence switches the fixture to a cove; no preset needed. |
lmPerFt | number (lm/ft) | no | 250 | Strip output per foot. Indirect light needs more output than direct for the same dose (one ceiling bounce), so 300–500 lm/ft is typical. |
cct | number (K) | no | 6500 | Strip colour, capped at 6500 K — no real LED strip goes higher. Run a tunable strip at 6500 for the strongest daytime spectrum. It cannot reach Circadian Sky CCTs; for more dose, add a separate Circadian Sky panel. |
sourceType | standard_led | no | standard_led | A cove is a genuine LED strip (Innerscene makes sky panels, not strips), so leave this standard_led. Do not set circadian_sky on a cove — that would model a product that does not exist. |
coveDepth | number | no | ≈0.25 m | How far the strip drops below the ceiling. Deeper spreads the ceiling wash, lowers glare, and avoids a bright scallop with roughly flat output; must be > 0 or the strip clips against the slab. |
shelfWidth | number | no | ≈0.15 m | Width of the ledge the strip sits on. ~0.15 m is optimal; wider does not raise output (and can hurt uniformity). |
fasciaHeight | number | no | ≈0.10 m | Height of the front lip that hides the strip. Keep it just tall enough to block the direct view of the strip — taller clips the upward throw and cuts output. |
along | length | number | no | full wall | along = 0–1 start; length = run length (units). Default runs the full wall. |
iesContent | string | no | bundled strip | Override the strip distribution with your own linear/batwing IES. |
A cove expands into a line of upward-aimed point sources carrying a real linear-strip IES. Because it is priced per foot (a strip), not per panel, it is a different cost basis from the Circadian Sky panels. Put a cove on two opposite walls for an even ceiling wash.
Design hints for someone lying down (patient rooms), some counter-intuitive. A supine occupant looks straight up, so any bright fixture overhead sits dead-center in their gaze — the worst place. These were verified in CircadianLab; always confirm with /score in the direction:"horizontal" (looking-up) view.
coveDepth) cuts glare with roughly flat output; a wider shelf does not help (optimum ~0.15 m); keep the lip just tall enough to hide the strip — taller only steals output.occupants[]| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
x, y | number | yes | — | Position (a person / measurement point). |
facing | north|east|south|west | no | "north" | Matters: mel-EDI is read vertically in this direction (what the eye receives); UGR is evaluated for this view. For a laying occupant this is the direction they look when they sit up (toward the foot of the bed). |
posture | sitting|standing|laying | no | "standing" | Sets eye height: sitting 1.2 m, standing 1.5 m, laying 0.4 m (looks up). |
recline | number (deg 0–90) | no | — | Bed / Fowler angle for a laying occupant: 0 = flat on the back, gaze straight up; 45 = semi-Fowler; 90 = sitting upright, gaze horizontal toward facing. The eye rises off the mattress and shifts toward the feet as it raises. mel-EDI and glare are read at that reclined eye + gaze. Set 0/45/90 to compare bed positions. |
name | string | no | "Point N" | Label in results. |
Bed position changes everything. Evaluate a patient at recline 0, 45 and 90. Two things move with the bed: glare (an overhead source is worst flat and vanishes as they sit up) and circadian dose (highest lying flat facing the source, and it falls as they sit up and face away — most designs drop well under 250 mel-EDI sitting upright). Report the worst case across all three.
customFixtures[]| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
iesContent | iesHash | string | yes | — | Raw IES/LDT text, or a hash from /ies/upload. |
cct | number (K) | yes* | — | *IES has no CCT — you must supply it. |
sourceType | standard_led|fluorescent|circadian_sky | yes* | standard_led | Drives melanopic ratio; or give customDER. |
customDER | number | no | — | Measured melanopic DER; overrides sourceType/CCT lookup. |
x, y, z | number | no | center / ceiling | Position. |
lumens, dim, width, depth, rotation | number | no | from file / 1 | Optional overrides. |
tilt, aimAzimuth | number (deg) | no | 0 | Aim the fixture (see §3 fixtures). Point a real wall-wash / adjustable downlight at a wall. |
furniture[] & partitions[]| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
furniture: x, y | number | yes | — | Desk position. |
furniture: width, depth | number | no | 1.2 / 0.6 m | Desk size. |
furniture: height | number | no | 2.5 ft | Surface height. |
partition: x1,y1,x2,y2 | number | yes | — | Wall segment endpoints. |
partition: height | number | no | ceiling | Top height. |
partition: baseHeight | number | no | 0 | Bottom above floor. |
Add windows (apertures) plus a daylight scene (a place, date and time) and the engine solves real daylight in the same radiosity pass as the electric fixtures: direct sun, diffuse sky, and inter-reflection are added to every workplane point. Daylight is spectrally rich, so it contributes strongly to mel-EDI (treated as ≈ 6500 K) — by day, windows can carry much of the circadian dose. Daylight only enters through apertures, so a daylight scene does nothing without at least one window or skylight.
windows[] — windows & skylights| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
wall | "south|east|north|west|ceiling" | yes | — | Which wall the window sits in. "ceiling" = a skylight. |
width, height | number | no | 4 × 4 | Aperture size (units). For a skylight, width = X-extent, height = Y (depth) extent. |
sill | number | no | ~0.9 m | Height of the window's bottom edge above the floor. (Skylight: its Y position from the south edge.) |
along | xLeft | number | no | centered | along = 0–1 fraction along the wall (0 = left corner viewed from inside). Or xLeft = distance in units. Default centers it. |
glazing | "clear|double|low_e|frosted" | no | clear | Glass type: clear (τ≈0.78), double (0.62), low_e (0.45), frosted (0.55, diffusing). Or an explicit {transmittance,reflectance,diffusion}. |
daylight — the sun & sky scene| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
lat, lon | number | yes | — | Site latitude / longitude (decimal degrees). Drives the sun position. |
date | "YYYY-MM-DD" | yes* | — | Local calendar date. Change it to compare seasons (e.g. 2026-06-21 vs 2026-12-21). *Or give utc. |
time | "HH:MM" | no | 12:00 | Local wall-clock (24 h). Change it to compare times of day. |
tz | IANA string | no | from lon | e.g. "America/Chicago" — needed for exact civil time (DST-aware). If omitted, approximated from longitude (with a warning). |
utc | ISO 8601 | no | — | Alternative to date/time/tz: an exact UTC instant, e.g. "2026-06-21T17:00:00Z". |
sky | "clear|intermediate|overcast" | no | clear | Sky condition. |
orientation | number (deg) | no | 0 | Building rotation: 0 = the room's south wall faces geographic south; +CCW from above. Set it to match the real site. |
Evaluating different times of day and year: keep the room and windows fixed and re-score with different daylight.date / daylight.time. Since /score is GET-able, fire one call per time — e.g. a matrix of 9:00 / 12:00 / 15:00 × Jun 21 / Dec 21, plus an overcast case, to see the full swing. Report the daylight-inclusive fc / mel-EDI / UGR at each, and note where electric light must fill in (early/late, overcast, deep-plan points far from glazing).
Orientation matters a lot. A south window takes strong direct sun — and the low winter sun drives far more vertical light (and glare) deep into the room than the high summer sun; north gives steady soft diffuse light; east/west spike at sunrise/sunset; a skylight gives even overhead daylight with the least glare. Direct-sun cases can read tens of thousands of lux / mel-EDI right at the glazing — great circadian dose, but check UGR and consider shading.
targets, optimize, view| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
targets.deskFc | number | no | — | Min horizontal footcandles at each occupant. |
targets.melEDI | number | no | — | Min vertical mel-EDI (facing direction) at each occupant. |
targets.ugr | number | no | — | Max UGR (glare) at each occupant. Pass if strictly under. |
optimize.fixture | preset id | no | first tunable | Which fixture /optimize varies. |
optimize.cctLadder / dimLadder / grids / maxCount | array / number | no | sensible | Override the search space. |
view.mode / metric / direction / units | string | no | 2d / eml / max / metric | Initial share view. |
view.heatmap / contours / values | boolean | no | on / on / off | Overlays (contours = iso-lines). Do not add values — the per-cell number grid is dense and noisy, and it is off by default. Only include values:true if the user explicitly wants the exact number printed on every cell; otherwise omit the field. |
| Preset | Name | Lumens | Source | CCT (K) | Size m (w×d) |
|---|---|---|---|---|---|
CS12 | Circadian Sky 1x2 | 1862 | circadian_sky | 2200–200000 (dflt 40000) | 0.324×0.629 |
CS22 | Circadian Sky 2x2 | 2763 | circadian_sky | 2200–200000 (dflt 40000) | 0.629×0.629 |
CS14 | Circadian Sky 1x4 | 2966 | circadian_sky | 2200–200000 (dflt 40000) | 0.324×1.238 |
CS154 | Circadian Sky 1.5x4 | 2732 | circadian_sky | 2200–200000 (dflt 40000) | 0.476×1.238 |
CS24 | Circadian Sky 2x4 | 6833 | circadian_sky | 2200–200000 (dflt 40000) | 0.629×1.238 |
FLUO24 | Fluorescent 2x4 | 5700 | fluorescent | 3500 fixed | 0.610×1.219 |
LED24 | LED Panel 2x4 | 4800 | standard_led | 3500 fixed | 0.610×1.219 |
LED22 | LED Panel 2x2 | 3600 | standard_led | 3500 fixed | 0.610×0.610 |
Circadian Sky presets are CCT-tunable; higher CCT delivers more mel-EDI per fixture (melanopic ratio rises with CCT, peaking near 40000 K = the default), so raising CCT is the cheapest way to hit a circadian target before adding hardware. LED / fluorescent presets are fixed at 3500 K.
Five sizes, all in the same recessed troffer housing: 89 mm (3.5″) deep, tuning 2200–200000 K (default 40000 K). Pick the size that fits the ceiling grid and the lumen budget; use CCT to dial circadian dose.
| Preset | Nominal | Housing mm (in) | Emission mm | Max lm | W | lm/W |
|---|---|---|---|---|---|---|
CS12 | 1×2 ft | 324×629 (12.8×24.8") | 212×512 | 1862 | 35 | 53 |
CS22 | 2×2 ft | 629×629 (24.8×24.8") | 512×512 | 2763 | 45 | 61 |
CS14 | 1×4 ft | 324×1238 (12.8×48.7") | 212×1112 | 2966 | 45 | 66 |
CS154 | 1.5×4 ft | 476×1238 (18.7×48.7") | 312×1112 | 2732 | 45 | 61 |
CS24 | 2×4 ft | 629×1238 (24.8×48.7") | 512×1112 | 6833 | 75 | 91 |
All CS sizes are 89 mm (3.5″) deep. Emission = the lit aperture (drives glare + soft-shadow behavior); housing = the cut-out footprint.
The Circadian Sky spectrum shifts with CCT, so two things change at once (measured values, from the ATMOS 4-chip SPD):
| CCT (K) | Melanopic ratio (M/P) | Relative light output | Relative mel-EDI / fixture | CRI (Ra) | R9 |
|---|---|---|---|---|---|
| 2,200 | 0.365 | 0.53× | 0.25× | 92.1 | 89 |
| 2,700 | 0.497 | 0.70× | 0.45× | 92.3 | 95 |
| 3,000 | 0.544 | 0.82× | 0.58× | 92.1 | 91 |
| 3,500 | 0.610 | 1.03× | 0.82× | 92.8 | 88 |
| 4,000 | 0.688 | 1.12× | 1.00× | 94.8 | 90 |
| 4,600 ← peak light output | 0.766 | 1.21× | 1.20× | 97.1 | 98 |
| 5,000 ← peak colour (Ra 97.5) | 0.806 | 1.16× | 1.22× | 97.5 | 98 |
| 6,500 | 0.944 | 1.00× | 1.23× | 96.5 | 88 |
| 9,000 | 1.084 | 0.94× | 1.33× | 94.6 | 86 |
| 15,000 | 1.243 | 0.92× | 1.49× | 92.9 | 85 |
| 40,000 ← peak mel-EDI; no valid CRI | 1.389 | 0.96× | 1.73× | n/a | n/a |
| 200,000 | 1.716 | 0.76× | 1.70× | n/a | n/a |
How to use this — circadian dose vs colour: to hit a mel-EDI target with the fewest fixtures, push CCT toward 40000 K (≈1.9× the mel-EDI of the same fixture at 4000 K); to maximize desk footcandles, stay near 4600–5000 K where light output peaks. /optimize applies that ordering (raise CCT before adding hardware), and the 40000 K default — a cool, blue-sky-like white — is a good daytime choice for offices, circulation, and wellness spaces valued for circadian dose and a daylight feel. Where accurate colour judgement matters, prefer a warmer setting — but only for colour judged under the room's own light: medical exam & dermatology (skin/tissue), retail, print / paint / art, food, and the macroscopic bench reads in a lab (colony colour & morphology, haemolysis on blood agar, chromogenic media, reagent/indicator colour). Keep CRI high there — ~5000 K peaks (Ra 97.5 / R9 98), ≤6500 K stays strong; set optimize.cctLadder to end there or fix cct. A task with its own dedicated light source is not governed by the ceiling — a microscope's illuminator (stained slides), a colour-viewing booth, or a task lamp set the colour there, so those don't require capping the overhead CCT. So in a microbiology lab the overhead serves the people and the by-eye bench reads (cap it warm), while slide microscopy rides the scope's own light. The Circadian Sky still delivers a solid circadian dose at 5000 K, so capping costs only modest mel-EDI. Mention the CCT you chose, and why, when you report a colour-critical room.
Dimming is constant-CCT and linear: the melanopic ratio does not change with the dim level, and both mel-EDI and illuminance scale directly with it. So a CS24 at 40000 K and dim: 0.5 delivers half the mel-EDI and half the lux of the same fixture at dim: 1.0 — the M/P ratio stays 1.39 either way.
dim | Relative mel-EDI & lux |
|---|---|
| 1.00 | 1.00× |
| 0.75 | 0.75× |
| 0.50 | 0.50× |
| 0.25 | 0.25× |
Circadian Sky is not a fixed-CCT luminaire you set once. Every fixture runs a built-in automated 24-hour program: through the day it drives high CCT and high melanopic output (a bright blue-white sky) to support daytime alertness and circadian entrainment, and in the evening and night it automatically lowers CCT and dims to a warm, low-melanopic scene that protects melatonin onset and sleep. This modulation is part of the product; it needs no external controls, astronomical clock, or extra wiring.
What this means when you design: CircadianLab models one static instant, so you design for the daytime peak, which is the hardest case for a mel-EDI target. A layout that reaches 250 mel-EDI at 40000 K represents that daytime high point. The same fixtures deliver a warm, dim, low-mel-EDI scene automatically after hours, so you do not need a second night-time model or a warmer alternate layout to be circadian-friendly in the evening. When you present a recommendation, state that the CCT and output are the daytime setting and note that the fixtures ramp themselves down in the evening. Do not suggest the user must hold a high CCT all day, or that a warmer look requires more hardware; warmth arrives on its own at night.
Beyond the presets above, CircadianLab ships a large third-party IES library you can search and pull real photometry from — use it whenever a design specifies a particular downlight, linear, troffer, etc. (a real IES gives trustworthy illuminance and glare; a placeholder does not).
GET /api/eml-calc/fixtures/searchTwo modes:
?q=… — multi-term match on name / manufacturer / brand / catalog / type. Returns one result per product family.variantId (feed to /lookup) and its exact watts / cct / lumens. Use this to resolve e.g. “10W linear 4000K” deterministically; plain text can’t (watts isn’t a text field, so q=10W finds nothing).| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
q | string | no | — | Free-text terms (name/brand/catalog/type). |
type | string | no | — | Form-factor, e.g. Linear/Strip, Downlight, Troffer (see table below). Substring match. |
wattsMin / wattsMax | number | no | — | Wattage range. |
cct | number (K) | no | — | Target CCT; combine with cctTol. |
cctTol | number | no | 0 | CCT tolerance (±K) around cct. |
lumensMin / lumensMax | number | no | — | Delivered-lumens range. |
beamMin / beamMax | number (deg) | no | — | Beam-angle range (spot vs wide). |
perManufacturer | number 1–10 | no | — | At most N (best) results per manufacturer. Use for "options across manufacturers / from top manufacturers" — without it one brand's product lines can flood the ranking and bury the rest. |
limit | number | no | 20 | Max results (≤50). |
Structured results: [{ variantId, name, manufacturer, type, watts, cct, lumens, beamAngle, url }], ranked by closeness to the requested watts/CCT.
243,546 product families across 177 manufacturers (433,003 IES variants). By form-factor:
| type= | Fixtures |
|---|---|
| Generic Profile | 126,450 |
| Downlight | 49,048 |
| Linear/Strip | 19,916 |
| Other | 17,873 |
| Troffer | 11,835 |
| Area/Flood | 6,478 |
| High Bay | 5,382 |
| Wall Wash | 3,535 |
| Spot/Track | 1,764 |
| Tube/Strip | 747 |
| Flat Panel | 320 |
— 10W linear @ 4000K, general-bright (a "C1" downlight-strip spec)
GET /fixtures/search?type=Linear&wattsMin=8&wattsMax=12
&cct=4000&lumensMin=1000
— 2×4 troffers near 3500K, 3000+ lm
GET /fixtures/search?type=Troffer&cct=3500&lumensMin=3000
— narrow-beam accent downlights, ≤15W
GET /fixtures/search?type=Downlight&wattsMax=15&beamMax=40
— 3500K spot downlights, best 2 per manufacturer
GET /fixtures/search?type=Downlight&cct=3500&beamMax=30
&perManufacturer=2
— free-text by brand / product line
GET /fixtures/search?q=lithonia+wall+wash
— then fetch the IES for a chosen result
GET /fixtures/lookup?id=<variantId>
→ { fixture:{watts,cct,lumens,…}, iesContent }
customFixturesDrop the fetched IES into a customFixtures entry, or upload a file with POST /api/eml-calc/ies/upload and reference the returned iesHash. IES files carry no CCT, so you must supply cct plus sourceType (standard_led | fluorescent | circadian_sky) or a measured customDER — otherwise the mel-EDI will be wrong.
"customFixtures": [
{ "iesContent": "<from /lookup>", "cct": 4000, "sourceType": "standard_led", "x": 4, "y": 5 }
]
| Endpoint | Does |
|---|---|
POST · GET /api/eml-calc/ai/score | Room (+targets) → room stats + per-occupant fc / mel-EDI / UGR + pass-fail. Validate a design. GET form: ?room= / ?room64= (see below). |
POST · GET /api/eml-calc/ai/optimize | Room + targets → escalates CCT → dimming → fixture count until targets pass. Returns design, score, trace, url. GET form: ?room= / ?room64=. |
POST · GET /api/eml-calc/ai/import | Room → stores it. POST returns a share url; GET ?room=/?room64= 302-redirects straight to the share — hand the user this link, no POST needed. |
GET /api/eml-calc/ai/render?share=<id> · ?room=/?room64= &w=&h= | PNG of a scene, to show the user what a design looks like. Renders a stored share, or a room passed inline. Resolution-capped; renders are queued one-at-a-time server-side, so a busy server returns 429 — wait briefly and retry. |
GET /api/eml-calc/fixtures/search · /lookup | Find real fixtures + fetch their IES (for custom fixtures). |
Build JSON → /score or /optimize → open/return the url → optionally /render to inspect the image and iterate.
Always output the complete room JSON in a fenced json code block — that block is your deliverable, so include it every time and never answer with prose only. Then tell the user to open the Import window in CircadianLab (or the standalone Import page), paste the JSON (or drop the .json file plus any .ies files), and click Open; their browser sends it and computes the model. No terminal or HTTP calls on their end. Do not tell the user to POST to an endpoint themselves. Better yet, if you can build a URL hand them a one-click GET link that opens the design — see GET-only links & compute just below.
If your client speaks the Model Context Protocol (Claude Code, Claude Desktop, Cursor, and other MCP-capable agents), you can skip hand-building HTTP calls entirely. The @innerscene/circadian-lab-mcp server wraps this exact API as typed, validated tools: same engine, same share links.
# Claude Code
claude mcp add circadian-lab -- npx -y @innerscene/circadian-lab-mcp
# Claude Desktop and other MCP clients (claude_desktop_config.json)
{ "mcpServers": { "circadian-lab": {
"command": "npx", "args": ["-y", "@innerscene/circadian-lab-mcp"] } } }
Tools: score_room, optimize_room, create_design, render_scene (returns the scene PNG as an image the assistant can actually see), search_fixtures, lookup_fixture, get_design_reference; plus this spec and the presets as MCP resources, and two starter prompts. One extra convenience over raw HTTP: inside customFixtures you can reference a library fixture as {"iesId": "<variantId from search_fixtures>"} and the server fetches and injects the IES text for you, so the assistant never carries raw photometry. Requires Node 18+. Set CIRCADIAN_LAB_BASE_URL to point it at a different host.
Many chat assistants can follow or fetch a URL (GET) but cannot POST. Every write in this API is reachable over GET too, so a POST is never required.
GET /api/eml-calc/ai/import takes the room in the query string and 302-redirects to the finished share. You do not have to call it yourself — just give the user the link. Their browser runs the GET when they click and lands on the model. Two encodings:
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
?room=<JSON> | URL-encoded | preferred | — | The room JSON, percent-encoded (encodeURIComponent). Robust + inspectable — prefer this whenever it fits (up to ~8 KB; covers most preset rooms — e.g. a 32-point oval office with 12 fixtures is ≈ 3.8 KB). A tool-less chat model can build it by hand. |
?room64=<blob> | gzip+base64url | large only | — | gzip the JSON, then base64url it (+/→-_, drop =). Compact (~400 chars) but opaque and fragile — one altered or dropped character (URL wrapping, a bad copy) silently corrupts the whole blob and it won't decompress. Use only when ?room= would exceed ~8 KB, and always also give the user the room JSON as a paste-in fallback. |
ChatGPT / Python — build either link:
import json, gzip, base64, urllib.parse
room = { "schema":"circadian-lab/room@1", "units":"ft", "room":{"width":24,"depth":18,"height":9},
"fixtures":[{"preset":"CS24","count":6,"cct":40000}], "occupants":[...], "targets":{...} }
raw = json.dumps(room, separators=(",",":")).encode()
# PREFERRED — robust + inspectable, use whenever it fits (up to ~8 KB):
link = "https://www.innerscene.com/api/eml-calc/ai/import?room=" + urllib.parse.quote(json.dumps(room))
# compact FALLBACK for very large rooms only — opaque; reproduce byte-exact or it won't decompress:
link = "https://www.innerscene.com/api/eml-calc/ai/import?room64=" + base64.urlsafe_b64encode(gzip.compress(raw)).decode().rstrip("=")
The same works from any language: percent-encode the JSON for room, or gzip → base64url for room64.
⚠ If a link doesn't open: a long room64 blob is easily corrupted when a chat UI wraps the URL or the user re-copies it — a single changed character means it can't be decompressed, and the page shows an import error. Don't try to repair the blob. Regenerate the link as plain ?room= if it fits, or simply give the user the room JSON in a code block to paste into the Import window — that path can't be broken by URL handling.
The identical ?room= / ?room64= query also drives /score and /optimize, which return JSON. If you can make GET requests, call these to report computed results (real fc / mel-EDI / UGR and a share url) instead of estimates — never quote numbers you didn't compute.
GET /api/eml-calc/ai/optimize?room64=… → runs the solver, returns { ok, design, score, trace, url }.GET /api/eml-calc/ai/score?room64=… → returns room stats + per-occupant fc / mel-EDI / UGR + pass/fail.GET /api/eml-calc/ai/render?room64=… (or ?share=<id>) → a PNG of the scene you can show the user. Add &metric=… via the room's view (e.g. footcandle plan vs mel-EDI). Renders are queued server-side; on a 429, wait a moment and retry.GET /api/eml-calc/fixtures/search?… → already GET; find real fixtures and their IES (see §5).A safe URL is about 2,000 characters; ~8,000 is the practical server ceiling. Preset rooms fit with room to spare (a six-panel office is ~420 chars as room64). What does not fit is customFixtures with embedded IES text (tens of KB each). For those, don't force it into a link — hand the user files for the Import window:
customFixtures, each naming its file via iesFile) plus the .ies/.ldt files dropped alongside — the Import window embeds them by filename..zip containing the room .json and the referenced .ies files. The Import window unpacks it and wires each iesFile to its fixture. (CircadianLab's own Export → Scene bundle writes this same layout, so an exported bundle re-imports cleanly.)Rule of thumb: preset-only design → a GET link; custom photometry → paste JSON or a ZIP.
| Field | Type | Req? | Default | Notes |
|---|---|---|---|---|
mel-EDI | lux | — | — | Melanopic EDI — circadian light at the eye. WELL v2 daytime tiers 136 / 250; ANSI/IES RP-46 daytime min 250 (vertical at eye). |
Footcandles (fc) | fc | — | — | Task/desk illuminance. Offices ~30–50 fc. ×10.764 = lux. |
UGR | — | — | — | Glare: <16 imperceptible · 19 acceptable · 22 tolerable · 25 common office limit. Lower is better; indirect light (a cove) lowers it. |
POST to /ai/import. “20×10 ft room with 3 Circadian Sky 2×4s.”
{
"schema": "circadian-lab/room@1",
"units": "ft",
"room": {
"width": 20,
"depth": 10,
"height": 9
},
"fixtures": [
{
"preset": "CS24",
"count": 3
}
],
"occupants": [
{
"x": 10,
"y": 5,
"facing": "north",
"posture": "standing"
}
]
}Response: { shareId, url, fixtures: 3, warnings: [] } — open url.
POST to /ai/score. Two seated workstations, check fc / mel-EDI / UGR.
{
"units": "ft",
"room": {
"width": 20,
"depth": 12,
"height": 9
},
"fixtures": [
{
"preset": "CS24",
"count": 6
}
],
"occupants": [
{
"x": 6,
"y": 4,
"facing": "north",
"posture": "sitting"
},
{
"x": 14,
"y": 8,
"facing": "north",
"posture": "sitting"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
}
}Response: { room:{avgFc,minFc,...}, occupants:[{deskFc,melEDI,ugr,pass}], pass:false }
POST to /ai/optimize. Give targets, not a layout — it escalates CCT → dimming → count.
{
"units": "ft",
"room": {
"width": 24,
"depth": 18,
"height": 9
},
"occupants": [
{
"x": 6,
"y": 5
},
{
"x": 18,
"y": 5
},
{
"x": 6,
"y": 13
},
{
"x": 18,
"y": 13
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
},
"optimize": {
"fixture": "CS24"
}
}Response: { ok:true, design:{preset,count,cct,dim,watts}, score, trace:[…], url }
Fetch a real IES via search+lookup, then score it. Note the required cct + sourceType.
{
"units": "ft",
"room": {
"width": 12,
"depth": 10,
"height": 9
},
"customFixtures": [
{
"iesContent": "<paste raw IES text here>",
"cct": 4000,
"sourceType": "standard_led",
"x": 4,
"y": 5
},
{
"iesContent": "<same>",
"cct": 4000,
"sourceType": "standard_led",
"x": 8,
"y": 5
}
],
"occupants": [
{
"x": 6,
"y": 5,
"facing": "north"
}
]
}A room split by an interior wall, with desks; occupants face their desks.
{
"units": "ft",
"room": {
"width": 24,
"depth": 12,
"height": 9
},
"fixtures": [
{
"preset": "CS22",
"count": 8
}
],
"partitions": [
{
"x1": 12,
"y1": 0,
"x2": 12,
"y2": 8,
"height": 6
}
],
"furniture": [
{
"x": 6,
"y": 3,
"width": 4,
"depth": 2
},
{
"x": 18,
"y": 3,
"width": 4,
"depth": 2
}
],
"occupants": [
{
"x": 6,
"y": 4,
"facing": "south",
"posture": "sitting"
},
{
"x": 18,
"y": 4,
"facing": "south",
"posture": "sitting"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 22
}
}The view field sets what the share shows when opened / rendered. contours (iso-lines) are on; values is omitted — leave it out so the dense per-cell number overlay stays off (add values:true only if the user asks for exact numbers on every cell).
{
"units": "ft",
"room": {
"width": 20,
"depth": 10,
"height": 9
},
"fixtures": [
{
"preset": "CS24",
"count": 3
}
],
"view": {
"mode": "2d",
"metric": "illuminance",
"units": "imperial",
"contours": true
}
}Response: Then GET /ai/render?share=<id>&w=1400&h=900 for a PNG.
Give the perimeter as a polygon — width/depth are derived from its bounding box and each edge becomes a wall. Coordinates can start anywhere.
{
"units": "ft",
"room": {
"height": 9,
"polygon": [
[
0,
0
],
[
30,
0
],
[
30,
20
],
[
10,
20
],
[
10,
10
],
[
0,
10
]
]
},
"fixtures": [
{
"preset": "CS24",
"count": 6
}
],
"occupants": [
{
"x": 5,
"y": 5
},
{
"x": 20,
"y": 15
}
]
}A central grid plus two accent fixtures tilted to graze the north and east walls. tilt = degrees off straight-down; aimAzimuth = the bearing it points toward (0 = north, 90 = east).
{
"units": "ft",
"room": {
"width": 20,
"depth": 14,
"height": 9
},
"fixtures": [
{
"preset": "CS24",
"count": 4
},
{
"preset": "CS22",
"x": 10,
"y": 12.5,
"tilt": 50,
"aimAzimuth": 0
},
{
"preset": "CS22",
"x": 18.5,
"y": 7,
"tilt": 50,
"aimAzimuth": 90
}
],
"occupants": [
{
"x": 10,
"y": 6,
"facing": "north"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
}
}Wall-mounted CS panels read as backlit sky windows. A row of 4 sits high on the south wall (clerestory), landscape — high mel-EDI, and mounting high keeps glare in check (still confirm UGR).
{
"units": "ft",
"room": {
"width": 24,
"depth": 16,
"height": 10
},
"fixtures": [
{
"preset": "CS24",
"wall": "south",
"count": 4,
"heightFromCeiling": 1.5,
"orientation": "landscape"
}
],
"occupants": [
{
"x": 12,
"y": 9,
"facing": "south"
}
],
"targets": {
"deskFc": 20,
"melEDI": 250,
"ugr": 25
}
}A classic CS pattern: a tight 3×2 overhead "skylight" of six panels (rotated 90° so they read as one luminous rectangle) for downward dose, plus a clerestory band on the wall the desks face for a strong vertical mel-EDI lift. The band is mounted flush under the ceiling and dimmed to 0.6 so the added glare stays under target. Scores ≈ 78 fc, ≈ 750 mel-EDI, UGR ≈ 24 at both desks — passes; drop the band's dim further if you want more UGR headroom.
{
"units": "ft",
"room": {
"width": 24,
"depth": 18,
"height": 10
},
"fixtures": [
{
"preset": "CS24",
"x": 8,
"y": 8,
"rotation": 90
},
{
"preset": "CS24",
"x": 12,
"y": 8,
"rotation": 90
},
{
"preset": "CS24",
"x": 16,
"y": 8,
"rotation": 90
},
{
"preset": "CS24",
"x": 8,
"y": 10,
"rotation": 90
},
{
"preset": "CS24",
"x": 12,
"y": 10,
"rotation": 90
},
{
"preset": "CS24",
"x": 16,
"y": 10,
"rotation": 90
},
{
"preset": "CS22",
"wall": "south",
"count": 4,
"heightFromCeiling": 1,
"orientation": "landscape",
"dim": 0.6
}
],
"occupants": [
{
"x": 8,
"y": 12,
"facing": "south",
"posture": "sitting"
},
{
"x": 16,
"y": 12,
"facing": "south",
"posture": "sitting"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
}
}Six CS12 panels butted end-to-end in one continuous drywall coffer (the lay-in array install), regressed 4″ so the band reads as an architectural skylight. The coffer cuts oblique glare; check the vertical mel-EDI cost vs flush with /score and compensate with CCT/output if needed.
{
"units": "ft",
"room": {
"width": 24,
"depth": 16,
"height": 9
},
"fixtures": [
{
"preset": "CS12",
"regress": 0.33,
"count": 6,
"axis": "x",
"x": 12,
"y": 8,
"cct": 6500
}
],
"occupants": [
{
"x": 12,
"y": 11,
"facing": "north",
"posture": "sitting"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 19
}
}Response: Compare the same row with regress: 0.67 (8″ deep) and with no regress (flush) to see the UGR / mel-EDI trade.
A window plus a sun/sky scene. POST/GET this to /score, then resend with different daylight.time (9:00 / 12:00 / 15:00) and daylight.date (Jun 21 / Dec 21) to see the daylight swing. Daylight is added on top of the electric fixtures.
{
"units": "ft",
"room": {
"width": 20,
"depth": 15,
"height": 9
},
"fixtures": [
{
"preset": "CS24",
"count": 4,
"dim": 0.5
}
],
"windows": [
{
"wall": "south",
"width": 10,
"height": 5,
"sill": 2.5,
"glazing": "clear"
}
],
"daylight": {
"lat": 40.7,
"lon": -74,
"tz": "America/New_York",
"date": "2026-06-21",
"time": "12:00",
"sky": "clear",
"orientation": 0
},
"occupants": [
{
"x": 10,
"y": 6,
"facing": "south",
"posture": "sitting"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
}
}Response: Change only daylight.time / daylight.date and re-score to compare times.
A 6×6 ft skylight centered in the ceiling — steady overhead daylight with the least glare. "ceiling" wall = skylight; width/height are its X/Y extents.
{
"units": "ft",
"room": {
"width": 18,
"depth": 18,
"height": 11
},
"windows": [
{
"wall": "ceiling",
"width": 6,
"height": 6,
"glazing": "double"
}
],
"daylight": {
"lat": 34,
"lon": -118.2,
"tz": "America/Los_Angeles",
"date": "2026-09-21",
"time": "10:00",
"sky": "clear"
},
"occupants": [
{
"x": 9,
"y": 9,
"facing": "north"
}
],
"targets": {
"deskFc": 30,
"melEDI": 250,
"ugr": 25
}
}Server-compute caps apply only to /score and /optimize, which run the engine on a shared box: room ≤ 400 m², ≤ 500 fixtures, ≤ 50 occupants. Oversized or malformed input returns HTTP 400/413 with an error message — fix and resend. A warnings array in a success response is non-fatal (e.g. a CCT clamped to a preset’s range, or an unknown preset skipped).
CircadianLab’s photometric engine runs client-side, so those caps exist to protect the server — not the calculation. For a design bigger than the caps (a warehouse, hundreds of fixtures, a very fine grid, high bounce counts), /import still accepts it — up to generous storage ceilings (50,000 m², 2000 fixtures) — and returns a share url. When the user, or an agent driving a local / headless browser, opens that url, the full-resolution calculation runs on their machine with no limits. So: use /score and /optimize to validate or solve within the server envelope, and open the share to compute and view arbitrarily large scenes locally.
CircadianLab · Innerscene. Machine-readable spec: /api/eml-calc/ai/spec