# eCalc-style sizing analysis

[eCalc](https://ecalc.ch/xcoptercalc.php) answers one question well: *given these parts, does the machine work?* It couples mass, battery, motor, controller, propeller and environment so one change ripples through the whole system. This document shows how Napkin encodes that kind of first-order design from a user's napkin sketch, and then goes past a single answer. It covers sensitivity, feasible intervals, optimisation under every requirement, tolerance stacks, catalog comparison and duty-cycle energy.

The evidence comes from [`studies/sizing-stress-test/`](../studies/sizing-stress-test/README.md). Six toy models span the first stress-test domains in [sizing-references.md](sizing-references.md): a cargo rover, a CNC ball-screw axis, a pump loop, a two-link arm, a solar field station and the shipped multicopter. All inputs are illustrative assumptions. Results are cross-checked against independent closed-form or iterative calculations (10/10 agree), **not** against eCalc, Grin, Thomson or Grundfos. Treat them as sizing evidence for the method, not as product data.

## The workflow

1. **Sketch the chain.** Write one line per quantity, from requirement through loads, mechanism, actuator and energy to checks (the engineering grammar in [sizing-references.md](sizing-references.md#the-engineering-grammar)). Send it with `napkin_sketch`.
2. **Read the verdict.** `napkin_run` evaluates every check. A failed requirement is a valid design with a red check, not an error.
3. **Find what matters.** `napkin_analyze` `sensitivity` ranks every input by elasticity, so effects in different units are directly comparable.
4. **Size it.** Use `napkin_manipulate` for one target (*ratio for 95% duty*), `feasible` for the allowed interval of one input, and `optimise` for the best ranged inputs under every check.
5. **Stress it.** Use `sample` for tolerance stacks, `grid` for maps over two inputs, scenarios and profiles for load cases and duty cycles, and `catalog` for real part choices.

Each step reads and writes the same revision-checked study that the workbench draws, so the user can watch the agent work in the drawing.

## Encoding a design

The full rover is 50 sketch lines and becomes 50 records ([rover.napkin](../studies/sizing-stress-test/sketches/rover.napkin)). The core of it:

```text
const g = 9.80665 m/s^2
payload = 40 kg in 0..500 "Rated cargo"
grade = 0.08 in 0..0.6 "Rise over run"
cda = 0.6 m^2 ± 15% "Drag area, boxy cargo body"
ratio = 20 in 4..60 "Gearbox reduction"
kv = 100 rpm/V ± 5%
ambient = 25 degC

roadLoad(v: m/s) := crr*weight*cos(theta) + weight*sin(theta) + 0.5*rho*cda*v^2 [N]
motorCurrentAt(v: m/s) := roadLoad(v)*wheelD/2/(motors*ratio*gearEff)/kt + noLoadI [A]
motorVoltageAt(v: m/s) := v/(wheelD/2)*ratio*kt + motorCurrentAt(v)*motorR [V]
range := voltage*capacity*usable/(elecPower/speed) [km]
windingTemp := ambient + motorCurrent^2*motorR*rth [degC]

vTop ?= 3 m/s in 0.01..30
relate topSpeed: motorVoltageAt(vTop) == maxDuty*voltage ± 0.0001 V

check speed feasible: duty <= maxDuty
check winding temperature: windingTemp <= windingLimit
check range: range >= 10 km
scenario Steep: grade = 0.15, payload = 80 kg
```

Each kind of design intent maps to one primitive:

| Design intent | Napkin primitive | Sketch form |
|---|---|---|
| A requirement or assumption | input with assumption, `range` (design interval), `tolerance` (uncertainty) | `payload = 40 kg in 0..500 ± 5% "note"` |
| A physical constant | shared input, excluded from sensitivity | `const g = 9.80665 m/s^2` |
| A load, power or loss | derived quantity | `Fload := roadLoad(speed) [N]` |
| A reusable physics relation | `function` quantity | `roadLoad(v: m/s) := … [N]` |
| A measured curve (pump, stepper pull-out, propeller) | `table` quantity, linear interpolation | `curve(Q: L/min) [m] := table 0: 25, 300: 12.5` |
| An operating point (component curve meets system curve) | `unknown` + one `relate` on function calls | `relate duty: pumpHead(Q) == systemHead(Q) ± 1e-5 m` |
| A mass or energy rollup | tagged quantities + `rollup` | `radio := … [W] #load`, `dailyLoad := sum of load [W]` |
| A requirement | check (observation with a limit) | `check range: range >= 10 km` |
| A load case | scenario overrides | `scenario Steep: grade = 0.15` |
| A duty cycle | scenario `profile`, time-weighted `mean` / `integral` | `profile Day:` + `night 15 h: sun = 0` |
| A catalog part | `component` with options, `select` | `component motor: kv, R, mass` + one line per part |

Physical units are checked throughout: `rpm`, `degC`, `L/min`, `W*h/kg` and unit literals such as `9.81 m/s^2` all work, and mismatches reject the edit.

## Results by domain

### Cargo rover

As sketched, the 2.5 m/s cruise on an 8% grade is infeasible: duty 1.32, and the top speed solved from the operating point is 1.77 m/s.

| Question | Command | Answer |
|---|---|---|
| Which inputs drive range? | `sensitivity` (51 evaluations, 3.7 ms) | capacity, usable fraction, voltage +1.0; gear efficiency +0.89; grade −0.72; chassis mass −0.52 |
| Which gear ratios work? | `feasible` on ratio | 8.0–13.8:1. Range limits the low end, duty the high end |
| Ratio for 95% duty | `manipulate` duty → 0.95, solveFor `[ratio]` | 13.8:1, with top speed re-solved automatically |
| Best range | `optimise` range over ratio | 11.1 km at 13.8:1, with duty active |
| Max payload in the steep case at 15 A | `manipulate` with `scenario: Steep` | 146 kg (at 20:1) |
| After committing 12.95:1 (90% duty) | `run` | duty 0.90, top speed 2.65 m/s, range 11.05 km, winding 53.7 °C |
| Where does it work? | `grid` grade × payload | 17 of 40 cells pass. At 80 kg: 6% passes (59.5 °C), 9% runs out of range (7.2 km), 15% reaches 174 °C |
| Tolerance stack | `sample`, 4,000 draws, 115 ms | every check passes; range p05/p50/p95 10.5/11.0/11.6 km, winding 47/53/61 °C |

### CNC ball-screw axis

The stepper is a measured-style pull-out table rather than a fitted line. At the baseline 10 mm lead the motor runs at 1800 rpm and the pull-out margin is 0.86, so the check fails.

| Question | Command | Answer |
|---|---|---|
| Which inputs drive torque margin? | `sensitivity` at 10 mm | v<sub>max</sub> −5.7, lead +5.4. The curve is steep there, so speed dominates |
| Which leads work? | `feasible` on lead | 11.1–26.4 mm. Pull-out torque limits the low end, inertia ratio ≤ 10 the high end |
| Every lead with margin 1.5 | `manipulate` with `explore` | 11.06 mm; no second root inside the declared 1–50 mm range |
| Fastest feasible move | `optimise` move time over lead, acceleration, v<sub>max</sub> | 1.26 s (from 2.15 s) at 26.4 mm, 2.7 m/s², 0.57 m/s; pull-out and inertia both active |

The lead sweep shows the trade-off directly:

| Lead (mm) | 6 | 10 | 14 | 18 | 22 | 26 | 30 |
|---|---|---|---|---|---|---|---|
| Motor rpm | 3000 | 1800 | 1286 | 1000 | 818 | 692 | 600 |
| Torque margin | 0 | 0.86 | 2.87 | 3.70 | 3.81 | 3.65 | 3.44 |
| Inertia ratio | 1.3 | 2.2 | 3.4 | 5.1 | 7.2 | 9.8 | 12.7 |

### Pump loop

A datasheet pump curve (table), affinity-law speed scaling (function) and a Churchill-friction system curve (function) meet at one unknown flow.

| Question | Command | Answer |
|---|---|---|
| Operating point | solved on every edit | 183.9 L/min, 1151 W (Colebrook cross-check: 184.4 L/min) |
| What drives flow? | `sensitivity` | pipe diameter +1.84, speed +1.52, pump curve tolerance +0.62, pipe length −0.32; roughness and fittings below 0.06 |
| What drives power? | `sensitivity` | speed +3.3 (affinity law), curve tolerance +1.4, diameter +1.1 |
| VFD speed range | `run` sweep 0.6–1.1 | fails at 0.6, 0.7 and 0.8 (42, 90, 125 L/min against 150 required) |
| Pipe for 200 L/min | `manipulate` Q → 200, solveFor `[pipeD]` | 41.9 mm |
| Cheapest compliant design | `optimise` annual cost over diameter and speed | 161/yr (from 432/yr) at 63.7 mm and 69% speed; flow and BEP checks active |
| Tolerance stack | `sample`, 2,000 draws, 55 ms | flow p05/p50/p95 174/184/195 L/min; every check passes |

At nominal speed, pipe diameter alone moves the design across the requirements:

| Pipe (mm) | 25 | 32 | 40 | 50 | 65 | 80 |
|---|---|---|---|---|---|---|
| Flow (L/min) | 64 | 116 | 184 | 257 | 314 | 334 |
| Electrical power (W) | 826 | 947 | 1151 | 1533 | 2439 | 3643 |
| All checks | fail | fail | **pass** | fail (past BEP) | fail | fail |

### Two-link arm

| Question | Command | Answer |
|---|---|---|
| Worst pose, 1 kg payload | `run` 37-pose sweep | passes; peak 83% of motor rating, at horizontal |
| Worst pose, 1.5 kg payload | same sweep | fails at 5 of 37 poses (worst 0.325 N·m against 0.32 rated) |
| Max payload | `manipulate` motor torque → rated | 1.46 kg |
| Usable reduction | `feasible` on ratio | 83–160:1 |
| Workspace map | `grid` q1 × q2, 63 poses | peak 0.265 N·m at horizontal, straight elbow |
| What drives motor torque? | `sensitivity` | ratio, gear efficiency −1.0; upper arm +0.83; payload +0.45; forearm +0.31 |

### Solar field station

Loads are tagged and rolled up, so adding one is one line. A four-segment winter day is a profile, integrated by the run.

| Question | Command | Answer |
|---|---|---|
| Daily average load | rollup | 9.07 W |
| Net battery energy over the day | `run` profile, `integral` | −128 W·h: the battery drains |
| PV needed | `feasible` on PV, `byCheck` | daily balance needs ≥ 174 W; PV alone cannot meet autonomy |
| Battery needed | `feasible` on battery, `byCheck` | autonomy needs ≥ 64 Ah; the battery alone cannot meet balance |
| Uncertainty in sun hours and derate | `sample` | energy balance p05/p50/p95 0.35/0.43/0.51 at 60 W |

### Multicopter

The shipped example now couples battery mass to capacity through specific energy, and keeps motors and propellers as catalog components.

| Question | Command | Answer |
|---|---|---|
| Best motor × propeller | `catalog`, 9 combinations | 2216 920Kv + 12×4.5: 20.7 min, T/W 3.85. Worst: 2814 700Kv + 8×4.5, 12.8 min |
| Best capacity | `optimise` hover time over capacity | 16.5 Ah → 23.9 min. Hover time falls beyond it (21.7 min at 30 Ah) because the pack outweighs its energy |
| What drives hover time? | `sensitivity` | CT +1.48, CP −1.05, usable fraction +1.0, diameter +0.67, cells +0.54 |

Before coupling, a capacity sweep claimed 104 min of hover at 30 Ah.

## What the stress test changed

The first pass built the same models on the 0.6.1 core with helper code outside Napkin. It found 16 shortcomings. Each one now has a fix and a regression test in [`tests/sizing.test.ts`](../tests/sizing.test.ts).

| Shortcoming | Before | Now |
|---|---|---|
| Offset temperatures | `25 degC + 13 K` evaluated to 1.14 degC | Affine conversion: 38 degC |
| Angular rates | `100 rad/s` → `6000 min^-1`; `1000 cycle/min` → `104.7 Hz`, silently | `rpm`/`rps`/`percent` units; ambiguous rate conversions rejected |
| Sweep checks | Judged only the last sample, so a failing sweep passed | Every sample must pass; worst value and `failedSamples` reported |
| Unknowns in inverse edits | Dropped unless listed in `solveFor`; feasible requests failed | Always re-solved |
| Flat regions (`max(0, …)`) | Zero gradient → `inconsistent_constraints` | Multi-start within declared ranges |
| Multiple roots | One answer, no warning | `explore` returns alternatives; `feasible` returns intervals |
| Scenario sizing | Baseline only | `manipulate` with `scenario` |
| Inequalities, best designs | Hand-written search | `optimise` and `feasible` with checks as constraints |
| Operating points | Load chain restated for the unknown | `function` quantities, one relation |
| Measured curves | Fitted formulas only | `table` quantities |
| Catalogs | 9 scenarios × 8 overrides; values could mix between parts | `component` records, `select`, `catalog` analysis |
| Collections | Hand-edited sums | Tags and `rollup` |
| Coupling | Battery mass independent of capacity in the example | Mass follows capacity / specific energy |
| Uncertainty | Ranges doubled as uncertainty | Separate `tolerance`; `sample` analysis |
| Time basis | Integral rejected | Duty-cycle `profile`, time-weighted `mean`/`integral` |
| Expression syntax | No unit literals, conditionals, `log10`, `floor` | All supported |
| Authoring cost | 264 hand-written bindings across five models; helper copied three times | Sketch text: bindings inferred, 26–50 lines per model |
| Evaluation speed | 1.0 ms per multicopter evaluation (re-parse + mathjs units) | Compiled cache; analyses run on a float plan at 0.013 ms (52×) |

## Limits

- **First-order physics only.** Results are static or quasi-static: there are no transients inside a profile segment, no thermal time constants, and no control loops. Profiles are piecewise constant.
- **Local searches.** `optimise`, `feasible` and the solver are local or sampled. A feasible island narrower than the scan spacing, or a disconnected optimum, can be missed. Each result states its method and limitations.
- **Expression checks only.** Analyses evaluate expression checks. Geometric checks (port distance, AABB clearance) run only through `run`.
- **Untaken branches.** A conditional branch the baseline never takes is unit-checked only when an evaluation reaches it.
- **Not yet benchmarked.** No result has been compared with eCalc, Grin, Thomson, Oriental Motor or Grundfos on a shared configuration. That is the next step for each domain, using the tools listed in [sizing-references.md](sizing-references.md#first-stress-tests).

## Reproduce

```sh
npx tsx studies/sizing-stress-test/analyze.ts     # all six domains, writes results.json
npx tsx --test tests/sizing.test.ts               # the fixes above as regression tests
```
