Skip to content

psychro-chart: mix OA and RA through Psychro.mixAir, by mass (#228 step 1) - #652

Open
EWH2000 wants to merge 1 commit into
feat/psychro-mixairfrom
feat/psychro-chart-mixair
Open

EWH2000 wants to merge 1 commit into
feat/psychro-mixairfrom
feat/psychro-chart-mixair

Conversation

@EWH2000

@EWH2000 EWH2000 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Stacked on #651 (feat/psychro-mixair, #228 step 0). Base is that branch. After #651 merges, rebase this branch onto origin/main and force-push so CI runs against main before anyone merges it (a retargeted stacked child otherwise shows no test check — CLAUDE.md Workflow).

Summary

The psychrometric chart's MA step stops mixing OA and RA inline and calls Psychro.mixAir({ basis: 'mass' }), which is #228 step 1 in docs/engine-standardization.md. Away from saturation every displayed number stays the same. When the mix fogs, the page now shows the #236 re-solved state on the saturation curve instead of the cold clamped one. The "% OA" label now says it is by mass, which matches the basis argument in the same diff.

Changes

  • html/tools/psychrometric-chart.html
    • solveChain, MA step: the inline blend is gone. It weighted W and h, backed out the dry-bulb, then called buildState, which clamped W at the cold dry-bulb when the mix fogged. The step now calls Psychro.mixAir({ streams: [{state: oa, share: pct}, {state: ra, share: 100 − pct}], basis: 'mass', P }) and uses exact. If the result is ok:false, the MA readouts go muted and psy-msg shows the error, the same way the other stages handle errors. In practice this path can't be reached from the page, because the 0–100 guard runs first and invalid OA/RA states never get to this step. The 0–100 page guard is unchanged, and no other chain step changed.
    • MA editor label Outdoor air (%) → Outdoor air (% by mass).
    • MA ref-note now says the % is a share of the dry-air mass flow, not CFM. It also says that a mixed point landing past the saturation curve condenses as fog and settles onto the curve. That last part is new behaviour, so the help text now covers it.

Before / after

I read these off the built page (origin/feat/psychro-mixair before, this branch after) with a small Playwright script. It sets OA/RA through the page's own inputs (Define-by mode + values), sets ma-pct, selects the MA pill, and reads ma-tdb-out / ma-w-out / ma-rh-out, the MA stage-table row and every ro-* detail readout. Fixture: the seven §1 rows, sea level. Both unit systems were captured. US is shown below. Metric gave the same result: the six ordinary rows were identical and only fog changed.

row inputs MA DB °F W gr/lb RH % h Btu/lb WB / DP °F v ft³/lb change
bench OA 35/80 %, RA 75/50 %, 50 % 55.1 → 55.1 44.2 → 44.2 68.8 → 68.8 20.09 → 20.09 49.7/45.0 13.109 none
design-day OA 0/60 %, RA 70/30 %, 30 % 49.1 → 49.1 23.8 → 23.8 46.3 → 46.3 15.46 → 15.46 40.6/29.7 12.896 none
friction OA 0/40 %, RA 75/50 %, 20 % 60.2 → 60.2 52.2 → 52.2 67.4 → 67.4 22.55 → 22.55 54.0/49.4 13.263 none
air-handlers OA 35/80 %, RA 75/50 %, 20 % 67.1 → 67.1 56.5 → 56.5 57.3 → 57.3 24.90 → 24.90 57.8/51.5 13.451 none
summer OA 95/75 °F WB, RA 75/50 %, 25 % 80.0 → 80.0 73.1 → 73.1 47.8 → 47.8 30.66 → 30.66 66.0/58.5 13.834 none
hot-dry OA 105/10 %, RA 75/50 %, 30 % 83.9 → 83.9 55.1 → 55.1 31.9 → 31.9 28.80 → 28.80 63.6/50.8 13.878 none
fog OA −20/40 %, RA 80/50 %, 70 % 10.4 → 17.7 9.4 → 13.4 100.0 → 100.0 3.93 → 6.29 10.4 → 17.7 (both) 11.876 → 12.071 intended #236 correction

All six ordinary rows match exactly in every captured field: editor readouts, stage-table row, all nine ro-* detail values and the status message. The fog row is the design note §1's 10.42 → 17.67 °F printed at the page's one decimal. Vapor pressure on that row moves 0.0315 → 0.0451 psia. In metric the fog row moves −12.0 → −8.0 °C, 1.3 → 1.9 g/kg, h −8.7 → −3.3 kJ/kg. The old h of 3.93 Btu/lb came from the clamp dropping moisture at the cold dry-bulb, so the old readout lost enthalpy the mix actually has. The re-solve fixes that too.

Fixture JSON (US; metric captured too, same verdict)

Before:

{"bench":{"editor":{"tdb":"55.1","w":"44.2","rh":"68.8"},"table":["55.1","44.2","68.8","20.09"],"detail":{"ro-db":"55.1","ro-wb":"49.7","ro-dp":"45.0","ro-rh":"68.8","ro-w":"44.2","ro-h":"20.09","ro-v":"13.109","ro-pw":"0.1478","ro-p":"14.696"}},
 "design-day":{"editor":{"tdb":"49.1","w":"23.8","rh":"46.3"},"table":["49.1","23.8","46.3","15.46"],"detail":{"ro-db":"49.1","ro-wb":"40.6","ro-dp":"29.7","ro-rh":"46.3","ro-w":"23.8","ro-h":"15.46","ro-v":"12.896","ro-pw":"0.0798","ro-p":"14.696"}},
 "friction":{"editor":{"tdb":"60.2","w":"52.2","rh":"67.4"},"table":["60.2","52.2","67.4","22.55"],"detail":{"ro-db":"60.2","ro-wb":"54.0","ro-dp":"49.4","ro-rh":"67.4","ro-w":"52.2","ro-h":"22.55","ro-v":"13.263","ro-pw":"0.1740","ro-p":"14.696"}},
 "air-handlers":{"editor":{"tdb":"67.1","w":"56.5","rh":"57.3"},"table":["67.1","56.5","57.3","24.90"],"detail":{"ro-db":"67.1","ro-wb":"57.8","ro-dp":"51.5","ro-rh":"57.3","ro-w":"56.5","ro-h":"24.90","ro-v":"13.451","ro-pw":"0.1882","ro-p":"14.696"}},
 "summer":{"editor":{"tdb":"80.0","w":"73.1","rh":"47.8"},"table":["80.0","73.1","47.8","30.66"],"detail":{"ro-db":"80.0","ro-wb":"66.0","ro-dp":"58.5","ro-rh":"47.8","ro-w":"73.1","ro-h":"30.66","ro-v":"13.834","ro-pw":"0.2427","ro-p":"14.696"}},
 "hot-dry":{"editor":{"tdb":"83.9","w":"55.1","rh":"31.9"},"table":["83.9","55.1","31.9","28.80"],"detail":{"ro-db":"83.9","ro-wb":"63.6","ro-dp":"50.8","ro-rh":"31.9","ro-w":"55.1","ro-h":"28.80","ro-v":"13.878","ro-pw":"0.1838","ro-p":"14.696"}},
 "fog":{"editor":{"tdb":"10.4","w":"9.4","rh":"100.0"},"table":["10.4","9.4","100.0","3.93"],"detail":{"ro-db":"10.4","ro-wb":"10.4","ro-dp":"10.4","ro-rh":"100.0","ro-w":"9.4","ro-h":"3.93","ro-v":"11.876","ro-pw":"0.0315","ro-p":"14.696"}}}

After: rows bench … hot-dry byte-identical to the above; fog:

{"editor":{"tdb":"17.7","w":"13.4","rh":"100.0"},"table":["17.7","13.4","100.0","6.29"],"detail":{"ro-db":"17.7","ro-wb":"17.7","ro-dp":"17.7","ro-rh":"100.0","ro-w":"13.4","ro-h":"6.29","ro-v":"12.071","ro-pw":"0.0451","ro-p":"14.696"}}

Test plan

  • Before/after fixture above. Seven §1 rows in US and metric: six ordinary rows unchanged in every field, fog row changed as intended.
  • tests/smoke.spec.js + tests/responsive.spec.js filtered -g psychrometric: 10 passed (chart loads cleanly, computes the AHU chain on load, above-boiling HC errors cleanly pid: fast scene becomes supply-fan-VFD loop; wiring: DC loop-power note #238, Cold range preset, 375 px fit, plus the two psychrometrics-basics pages the filter also matches)
  • tests/psychro-mixair.spec.js (17), tests/psychro-engine.spec.js (12), tests/psychro-mixstreams.spec.js (19), tests/metric-spans.spec.js (3), tests/machine-sweep.spec.js (7), tests/a11y-bundle.spec.js (9), tests/fullscreen-toggle.spec.js (7): 74 passed, 0 failed
  • Contrast sweep not run: no new ink (same label/ref-note classes, text only)
  • CI test check green on this branch after the post-psychro: add mixAir and mixFraction page-facing combinators (#228 step 0) #651 rebase-push

Merge authority

Live shared script, nine live consumers → owner merges. (This diff touches only the live page tools/psychrometric-chart.html. It sits on #651, which changes the shared psychro-engine.js.)

🤖 Generated with Claude Code

…ep 1)

The chart blended OA and RA inline: W and h weighted by the % OA, the
dry-bulb backed out of h, then buildState — which clamps W to
saturation and keeps the cold, pre-#236 dry-bulb when the mix fogs.
The engine's mixAir (step 0, #651) does the same blend through the
mixStreams kernel and re-solves a fogged mix onto the curve, so the
page now calls it and the #236 ruling reaches this surface.

Clear of saturation the displayed MA state is unchanged: six of the
seven engine-standardization §1 rows read identically off the built
page before and after, in both unit systems. The fog-corner row moves
10.4 -> 17.7 °F, 9.4 -> 13.4 gr/lb, h 3.93 -> 6.29 Btu/lb — the
intended correction (design note §1: 10.42 -> 17.67 °F).

- html/tools/psychrometric-chart.html: solveChain's MA step calls
  Psychro.mixAir({ basis: 'mass' }) and takes exact; an ok:false
  result mutes the MA readouts and sets the status message like the
  other stages. The % OA label reads "Outdoor air (% by mass)" and
  the MA ref-note says the % is of dry-air mass flow, not CFM, and
  what happens when the mixed point lands past the curve.

Refs: codebase-issues#228

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
controlsfreak 529c957 Commit Preview URL

Branch Preview URL
Oct 07 2026, 01:58 AM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant