Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Engineering Calculator Platform

PMV/PPD and moist-air calculators deployed as a secure AWS serverless web application.
PMV/PPD, moist-air, and refrigerant Mollier-diagram tools deployed as a secure AWS serverless web application.

This repository is intended to grow into a collection of engineering calculation tools with a shared web interface and AWS platform.

Expand All @@ -12,6 +12,7 @@ This repository is intended to grow into a collection of engineering calculation
- Separate Python Lambda functions calculate PMV/PPD and moist-air properties.
- API requests are throttled and validated server-side.
- The moist-air page renders an h-x psychrometric chart as browser-native SVG.
- The Mollier page runs the official CoolProp 8.0.0 WebAssembly build locally in the browser and renders a refrigerant p-h diagram.

The stack deploys independently, so existing calculators can remain available during acceptance testing and migration.

Expand All @@ -36,6 +37,10 @@ The PMV tests include representative values captured from the legacy API. The mo
The browser chart ports the equations and oblique-coordinate transform from
[`iguchi-lab/psychrometric-chart`](https://github.com/iguchi-lab/psychrometric-chart).

The Mollier tool adapts [`iguchi-lab/mollier-diagram`](https://github.com/iguchi-lab/mollier-diagram)
and vendors the official MIT-licensed CoolProp 8.0.0 JavaScript/WASM binaries. The pinned
vendor files and license are stored under `web-release/mollier/vendor/coolprop-8.0.0/`.

## Deployment

Prerequisites:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"private": true,
"type": "module",
"scripts": {
"test:chart": "node --test tests_verified/psychrometric-chart.test.js"
"test:chart": "node --test tests_verified/*.test.js"
}
}
2 changes: 1 addition & 1 deletion template-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Resources:
Name: !Sub '${AWS::StackName}-security-headers'
SecurityHeadersConfig:
ContentSecurityPolicy:
ContentSecurityPolicy: "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
ContentSecurityPolicy: "default-src 'self'; style-src 'self'; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
Override: true
ContentTypeOptions:
Override: true
Expand Down
35 changes: 35 additions & 0 deletions tests_verified/mollier-diagram.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";

import CoolPropModule from "../web-release/mollier/vendor/coolprop-8.0.0/coolprop.js";
import { calculateMollierData } from "../web-release/mollier/mollier-chart.js";

const coolprop = await CoolPropModule();

test("CoolProp WASM provides the R32 critical temperature", () => {
const critical = coolprop.PropsSI("Tcrit", "", 0, "", 0, "R32");
assert.ok(Math.abs(critical - 351.255) < 0.05);
});

test("Mollier data preserves the ideal refrigeration cycle invariants", () => {
const data = calculateMollierData(coolprop, {
fluid: "R32",
evaporatingTemperature: 6.85,
condensingTemperature: 51.85,
superheat: 5,
subcooling: 5,
pressureMin: 0.3,
pressureMax: 5,
curvePoints: 30,
pressurePoints: 24,
});

assert.equal(data.states.length, 4);
const byNumber = Object.fromEntries(data.states.map((state) => [state.number, state]));
assert.ok(Math.abs(byNumber[1].p - byNumber[4].p) < 1e-9);
assert.ok(Math.abs(byNumber[2].p - byNumber[3].p) < 1e-9);
assert.ok(Math.abs(byNumber[3].h - byNumber[4].h) < 1e-9);
assert.ok(Math.abs(byNumber[1].s - byNumber[2].s) < 1e-7);
assert.equal(data.saturatedLiquid.length, 30);
assert.equal(data.saturatedVapor.length, 30);
});
9 changes: 8 additions & 1 deletion web-release/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ button:disabled { opacity:.55; cursor:wait; }
.chart-heading .lead { margin-bottom:0; }
.chart-heading button { flex:0 0 auto; }
.chart-scroll { width:100%; overflow-x:auto; border:1px solid var(--line); border-radius:10px; background:#fff; }
.psychrometric-chart { display:block; width:100%; min-width:700px; height:auto; }
.psychrometric-chart,.mollier-chart { display:block; width:100%; min-width:700px; height:auto; }
.mollier-controls { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:0 18px; }
.mollier-actions { display:flex; align-items:end; padding-bottom:22px; }
.table-scroll { width:100%; overflow-x:auto; }
.state-table { width:100%; border-collapse:collapse; white-space:nowrap; }
.state-table th,.state-table td { padding:10px 12px; border-bottom:1px solid var(--line); text-align:right; font-variant-numeric:tabular-nums; }
.state-table th:first-child,.state-table td:first-child,.state-table th:nth-child(2),.state-table td:nth-child(2) { text-align:left; }
.state-table thead { background:#edf6f6; }
.chart-source { margin:8px 0 0; color:var(--muted); font-size:.9rem; }
footer { border-top:1px solid var(--line); padding:24px 0; color:var(--muted); }
@media (max-width:620px) { main { padding-top:24px; } .card { padding:18px; } .chart-heading { display:block; } .chart-heading button { margin-top:12px; } }
1 change: 1 addition & 0 deletions web-release/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ <h1>環境計算ツール</h1><p class="lead">計算したい項目を選択し
<div class="grid">
<article class="card"><h2>PMV・PPD</h2><p>温熱環境の予測平均温冷感申告と予測不満足者率を計算します。</p><a class="button" href="/pmv/index.html">PMV計算を開く</a></article>
<article class="card"><h2>湿り空気</h2><p>乾球温度、相対湿度、露点温度、絶対湿度などを相互に計算します。</p><a class="button" href="/air/index.html">湿り空気計算を開く</a></article>
<article class="card"><h2>p-h線図(モリエル線図)</h2><p>冷媒物性と理想的な蒸気圧縮冷凍サイクルを表示します。</p><a class="button" href="/mollier/index.html">モリエル線図を開く</a></article>
</div>
</main>
<footer><div class="container">計算結果は設計・研究上の参考値として利用してください。</div></footer>
Expand Down
87 changes: 87 additions & 0 deletions web-release/mollier/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import CoolPropModule from "./vendor/coolprop-8.0.0/coolprop.js";
import {
calculateMollierData,
downloadMollierSvg,
renderMollierDiagram,
} from "./mollier-chart.js";

const form = document.querySelector("#mollier-form");
const button = document.querySelector("#calculate");
const status = document.querySelector("#status");
const svg = document.querySelector("#mollier-chart");
const stateTable = document.querySelector("#state-table");
const downloadButton = document.querySelector("#download-chart");
let latestFluid = "R32";

const coolPropPromise = CoolPropModule({
locateFile(file) {
return new URL(`./vendor/coolprop-8.0.0/${file}`, import.meta.url).href;
},
});

function formOptions() {
return {
fluid: form.elements.fluid.value,
evaporatingTemperature: Number(form.elements.evaporatingTemperature.value),
condensingTemperature: Number(form.elements.condensingTemperature.value),
superheat: Number(form.elements.superheat.value),
subcooling: Number(form.elements.subcooling.value),
pressureMin: Number(form.elements.pressureMin.value),
pressureMax: Number(form.elements.pressureMax.value),
};
}

function renderStates(states) {
stateTable.replaceChildren();
for (const state of states) {
const row = document.createElement("tr");
const values = [
state.number,
state.name,
state.t.toFixed(2),
state.p.toFixed(3),
state.h.toFixed(2),
state.s.toFixed(4),
];
for (const value of values) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
stateTable.append(row);
}
}

async function calculate() {
if (!form.reportValidity()) return;
button.disabled = true;
downloadButton.disabled = true;
status.className = "status";
status.textContent = "CoolPropで冷媒物性を計算しています…";
await new Promise((resolve) => requestAnimationFrame(resolve));
try {
const coolprop = await coolPropPromise;
const options = formOptions();
const data = calculateMollierData(coolprop, options);
renderMollierDiagram(svg, data);
renderStates(data.states);
latestFluid = data.fluid;
status.textContent = `${data.fluid}のp-h線図を作成しました。`;
downloadButton.disabled = false;
} catch (error) {
status.className = "status error";
status.textContent = error instanceof Error ? error.message : "線図を作成できませんでした。";
} finally {
button.disabled = false;
}
}

form.addEventListener("submit", (event) => {
event.preventDefault();
calculate();
});
downloadButton.addEventListener("click", () => {
downloadMollierSvg(svg, `mollier-diagram-${latestFluid}.svg`);
});

calculate();
60 changes: 60 additions & 0 deletions web-release/mollier/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>p-h線図(モリエル線図)</title>
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
<header><div class="container"><a href="/index.html">← 計算ツール一覧</a></div></header>
<main class="container">
<h1>p-h線図(モリエル線図)</h1>
<p class="lead">冷媒と運転条件を指定し、理想的な蒸気圧縮冷凍サイクルを表示します。</p>

<form id="mollier-form" class="card mollier-controls">
<div class="field">
<label for="fluid">冷媒</label>
<select id="fluid" name="fluid">
<option value="R32">R32</option>
<option value="R410A">R410A</option>
<option value="R134a">R134a</option>
<option value="R1234yf">R1234yf</option>
<option value="R290">R290(プロパン)</option>
</select>
</div>
<div class="field"><label for="evaporating-temperature">蒸発温度(℃)</label><input id="evaporating-temperature" name="evaporatingTemperature" type="number" step="0.1" value="6.85"></div>
<div class="field"><label for="condensing-temperature">凝縮温度(℃)</label><input id="condensing-temperature" name="condensingTemperature" type="number" step="0.1" value="51.85"></div>
<div class="field"><label for="superheat">過熱度(K)</label><input id="superheat" name="superheat" type="number" min="0" max="50" step="0.1" value="5"></div>
<div class="field"><label for="subcooling">過冷却度(K)</label><input id="subcooling" name="subcooling" type="number" min="0" max="50" step="0.1" value="5"></div>
<div class="field"><label for="pressure-min">最低圧力(MPa)</label><input id="pressure-min" name="pressureMin" type="number" min="0.05" max="10" step="0.05" value="0.3"></div>
<div class="field"><label for="pressure-max">最高圧力(MPa)</label><input id="pressure-max" name="pressureMax" type="number" min="0.1" max="20" step="0.1" value="5"></div>
<div class="mollier-actions"><button id="calculate" type="submit">線図を作成</button></div>
</form>

<section class="card chart-card">
<div class="chart-heading">
<div><h2>冷媒p-h線図</h2><p id="status" class="status" aria-live="polite">CoolPropを読み込んでいます…</p></div>
<button id="download-chart" type="button" disabled>SVGを保存</button>
</div>
<div class="chart-scroll"><svg id="mollier-chart" class="mollier-chart" role="img" aria-labelledby="mollier-title mollier-description"></svg></div>
</section>

<section class="card chart-card">
<h2>冷凍サイクル状態点</h2>
<div class="table-scroll">
<table class="state-table">
<thead><tr><th>状態</th><th>名称</th><th>温度(℃)</th><th>圧力(MPa)</th><th>比エンタルピー(kJ/kg)</th><th>比エントロピー(kJ/(kg·K))</th></tr></thead>
<tbody id="state-table"><tr><td colspan="6">計算後に表示します。</td></tr></tbody>
</table>
</div>
</section>

<p class="chart-source">
計算エンジン: <a href="https://coolprop.org/" target="_blank" rel="noopener noreferrer">CoolProp 8.0.0</a>(MIT License)。
描画ロジック: <a href="https://github.com/iguchi-lab/mollier-diagram" target="_blank" rel="noopener noreferrer">iguchi-lab/mollier-diagram</a>。
</p>
</main>
<script type="module" src="./app.js"></script>
</body>
</html>
Loading
Loading