. Equations: IEC 60534-2-1 style, SI units (bar, m3/h, kg/h, K). */ (function () { 'use strict'; function init() { var root = document.getElementById('cvs-app'); if (!root || root.getAttribute('data-ready')) return; root.setAttribute('data-ready', '1'); var $ = function (id) { return root.querySelector('#cvs_' + id); }; var N1 = 0.865, N8 = 94.8, KV = 1.156099, RG = 8314.462618; var CASES = [['Minimum', 5, 10, 8], ['Normal', 25, 10, 7], ['Maximum', 40, 10, 6]]; var ids = ['project','plant','tag','service','pid','datasheet','engineer','phase','flow','pin','pout','temp','pipe','density','visc','pv','pc','mw','k','z','vtype','characteristic','ratedCv','FL','xT','FP','rangeability']; var last = null, lastCases = [], marks = { main: null, cases: [] }; /* ---------- helpers ---------- */ function n(id) { return parseFloat($(id).value); } function fmt(x, d) { return isFinite(x) ? Number(x).toLocaleString('en-US', { maximumFractionDigits: d == null ? 3 : d }) : '—'; } function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function txt(id, s) { $(id).textContent = s; } function lvlOf(w) { return w.some(function (x) { return x[0] === 'bad'; }) ? 'bad' : (w.length ? 'warn' : 'ok'); } function badge(l) { return '' + (l === 'ok' ? 'OK' : l === 'bad' ? 'Review' : 'Caution') + ''; } /* ---------- operating case rows ---------- */ $('caseRows').innerHTML = CASES.map(function (c, i) { ids.push('c' + i + 'q', 'c' + i + 'a', 'c' + i + 'b'); var inp = function (k, v) { return ''; }; var out = function (k) { return '—'; }; return '' + c[0] + '' + inp('q', c[1]) + inp('a', c[2]) + inp('b', c[3]) + out('cv') + out('r') + out('t') + out('s') + ''; }).join(''); var defaults = {}; ids.forEach(function (id) { defaults[id] = $(id).value; }); /* ---------- tabs and phase ---------- */ function tab(id) { root.querySelectorAll('.cvs-tab').forEach(function (b) { b.classList.toggle('cvs-active', b.getAttribute('data-tab') === id); }); root.querySelectorAll('.cvs-pane').forEach(function (p) { p.classList.toggle('cvs-active', p.id === 'cvs_pane_' + id); }); } root.querySelectorAll('.cvs-tab').forEach(function (b) { b.addEventListener('click', function () { tab(b.getAttribute('data-tab')); }); }); function phaseUI() { var ph = $('phase').value; root.querySelectorAll('[data-ph]').forEach(function (el) { el.classList.toggle('cvs-hide', el.getAttribute('data-ph') !== ph); }); var u = ph === 'liquid' ? 'm³/h' : 'kg/h'; txt('flowLbl', 'Required flow, ' + u); txt('fu', u); } /* ---------- validation ---------- */ function check(q, p1, p2) { var e = [], ph = $('phase').value; if (!(q > 0)) e.push('Flow must be greater than zero.'); if (!(p1 > 0 && p2 > 0)) e.push('Absolute pressures must be greater than zero.'); else if (p1 <= p2) e.push('Upstream pressure must exceed downstream pressure.'); if (!(n('ratedCv') > 0)) e.push('Rated Cv must be greater than zero.'); if (!(n('FP') > 0 && n('FP') <= 1.5)) e.push('FP must be between 0 and 1.5.'); if (!(n('rangeability') >= 2)) e.push('Rangeability must be at least 2.'); if (!(n('temp') > -273.15)) e.push('Temperature must be above absolute zero.'); if (ph === 'liquid') { if (!(n('FL') > 0 && n('FL') <= 1)) e.push('FL must be between 0 and 1.'); if (!(n('density') > 0)) e.push('Density must be greater than zero.'); if (!(n('pv') >= 0)) e.push('Vapor pressure must be zero or more.'); if (!(n('pc') > 0)) e.push('Critical pressure must be greater than zero.'); if (p1 <= n('pv')) e.push('Upstream pressure is at or below vapor pressure: fluid is not a liquid at the inlet.'); } else { if (!(n('xT') > 0 && n('xT') <= 1)) e.push('xT must be between 0 and 1.'); if (!(n('mw') > 0)) e.push('Molecular weight must be greater than zero.'); if (!(n('k') > 1)) e.push('k must be greater than 1.'); if (!(n('z') > 0)) e.push('Z must be greater than zero.'); } return e; } /* ---------- sizing ---------- */ function size(q, p1, p2) { var ph = $('phase').value, FP = n('FP'), d = n('pipe'), A = d > 0 ? Math.PI * d * d / 4e6 : 0; var r = { ph: ph, q: q, p1: p1, p2: p2, dp: p1 - p2, FP: FP }; if (ph === 'liquid') { var sg = n('density') / 999.016, pv = n('pv'), pc = n('pc'), FL = n('FL'); var FF = 0.96 - 0.28 * Math.sqrt(pv / pc); var dpMax = FL * FL * (p1 - FF * pv), dpE = Math.min(r.dp, dpMax); r.sg = sg; r.FF = FF; r.dpMax = dpMax; r.choked = r.dp >= dpMax; r.cv = q / (N1 * FP) * Math.sqrt(sg / dpE); /* Cv = Q / (N1 Fp) * sqrt(SG / dP) */ r.flash = p2 <= pv; r.kc = r.dp / (p1 - pv); r.cav = r.flash ? 'Flashing likely' : r.choked ? 'Choked / cavitating' : (r.kc >= 0.7 * FL * FL ? 'Incipient cavitation possible' : 'Not indicated'); r.regime = r.choked ? 'Choked flow' : 'Non-choked'; if (A > 0) r.v = q / 3600 / A; } else { var M = n('mw'), k = n('k'), Z = n('z'), T = n('temp') + 273.15, xT = n('xT'); var xc = (k / 1.4) * xT, x = r.dp / p1, xE = Math.min(x, xc), Y = 1 - xE / (3 * xc); r.x = x; r.xc = xc; r.Y = Y; r.choked = x >= xc; r.cv = q / (N8 * FP * p1 * Y) * Math.sqrt(T * Z / (xE * M)); /* Cv = W/(N8 Fp P1 Y) * sqrt(T Z / (x M)) */ r.rho1 = p1 * 1e5 * M / (RG * Z * T); r.cav = 'Not applicable'; r.regime = r.choked ? 'Choked flow' : 'Non-choked'; if (A > 0) { var rho2 = p2 * 1e5 * M / (RG * Z * T); r.v = q / 3600 / rho2 / A; r.mach = r.v / Math.sqrt(k * Z * RG * T / M); } } r.kv = r.cv * KV; r.rated = n('ratedCv'); r.ratio = r.rated / r.cv; r.t = travel(r.cv / r.rated); return r; } /* ---------- characteristic (t = travel fraction 0..1, f = capacity fraction) ---------- */ function travel(f) { var R = Math.max(2, n('rangeability') || 50), c = $('characteristic').value; if (!(f > 0)) return 0; var t = c === 'linear' ? f : c === 'quick' ? f * f : 1 + Math.log(Math.max(f, 1 / R)) / Math.log(R); return Math.max(0, Math.min(1, t)) * 100; } function cap(t) { var R = Math.max(2, n('rangeability') || 50), c = $('characteristic').value; return c === 'linear' ? t : c === 'quick' ? Math.sqrt(t) : Math.pow(R, t - 1); } /* ---------- assessment ---------- */ function assess(r) { var w = []; if (r.ratio < 1) w.push(['bad', 'Rated Cv is below the required Cv.']); else if (r.ratio > 3) w.push(['warn', 'Rated Cv is more than 3× required: check for oversizing.']); if (r.ph === 'liquid') { if (r.flash) w.push(['bad', 'P2 is at or below vapor pressure: flashing likely.']); else if (r.choked) w.push(['bad', 'ΔP is at or above the allowable ΔP: choked flow / cavitation.']); else if (r.cav.indexOf('Incipient') === 0) w.push(['warn', 'Incipient cavitation possible (screening, Kc ≈ 0.7·FL²).']); if (r.v > 5) w.push(['warn', 'Downstream liquid velocity above 5 m/s.']); if (n('visc') > 20) w.push(['warn', 'Viscosity above 20 cP: Reynolds correction (FR) is not applied.']); } else { if (r.choked) w.push(['warn', 'Choked gas flow: flow no longer rises with ΔP. Check noise and vibration.']); if (r.mach > 0.5) w.push(['bad', 'Downstream Mach number above 0.5.']); else if (r.mach > 0.3) w.push(['warn', 'Downstream Mach number above 0.3.']); } if (r.ratio >= 1 && r.t < 10) w.push(['warn', 'Travel below 10%: poor control resolution.']); if (r.ratio >= 1 && r.t > 90) w.push(['warn', 'Travel above 90%: little control margin.']); return w; } /* ---------- plot ---------- */ function draw() { var c = $('curve'), x = c.getContext('2d'), w = c.width, h = c.height, L = 62, R = 24, T = 22, B = 48, pw = w - L - R, ph = h - T - B, i; var px = function (t) { return L + pw * t / 100; }, py = function (f) { return h - B - ph * f; }; x.clearRect(0, 0, w, h); x.fillStyle = '#0e171e'; x.fillRect(0, 0, w, h); x.font = '14px system-ui,sans-serif'; x.lineWidth = 1; for (i = 0; i <= 5; i++) { x.strokeStyle = '#344655'; x.fillStyle = '#a5b5c1'; x.beginPath(); x.moveTo(px(i * 20), T); x.lineTo(px(i * 20), h - B); x.stroke(); x.beginPath(); x.moveTo(L, py(i / 5)); x.lineTo(w - R, py(i / 5)); x.stroke(); x.fillText(String(i * 20), px(i * 20) - 10, h - B + 22); x.fillText(String(i * 20), 26, py(i / 5) + 5); } x.fillStyle = '#edf3f7'; x.fillText('Valve travel (%)', w / 2 - 45, h - 8); x.save(); x.translate(16, h / 2 + 60); x.rotate(-Math.PI / 2); x.fillText('Relative capacity (%)', 0, 0); x.restore(); x.strokeStyle = '#53d6a0'; x.lineWidth = 3; x.beginPath(); for (i = 0; i <= 100; i++) { if (i === 0) x.moveTo(px(i), py(cap(i / 100))); else x.lineTo(px(i), py(cap(i / 100))); } x.stroke(); var pts = []; if (marks.main) pts.push([marks.main, '#72b7ff', 'Design']); marks.cases.forEach(function (m, j) { if (m) pts.push([m, ['#ffd166', '#ffffff', '#ff7373'][j], ['Min', 'Nor', 'Max'][j]]); }); pts.forEach(function (p) { var X = px(p[0].t), Y = py(Math.min(1, p[0].f)); x.fillStyle = p[1]; x.beginPath(); x.arc(X, Y, 6, 0, 6.2832); x.fill(); x.fillText(p[2], Math.min(X + 9, w - 60), Math.max(Y - 9, T + 12)); }); } /* ---------- design point ---------- */ function calcMain() { var q = n('flow'), p1 = n('pin'), p2 = n('pout'), e = check(q, p1, p2); if (e.length) { $('validation').innerHTML = 'Input errors ' + esc(e.join(' ')); return false; } var r = size(q, p1, p2), w = assess(r); r.warns = w; last = r; $('validation').innerHTML = badge(lvlOf(w)) + ' Calculation completed. Review the checks before use.'; txt('reqCv', fmt(r.cv, 3)); txt('reqKv', fmt(r.kv, 3)); txt('ratio', fmt(r.ratio, 2) + '×'); txt('trv', fmt(r.t, 1) + '%'); txt('dp', fmt(r.dp, 3) + ' bar'); txt('regime', r.regime); txt('cav', r.cav); txt('vel', r.v ? (r.ph === 'liquid' ? fmt(r.v, 2) + ' m/s' : fmt(r.v, 1) + ' m/s (Ma ' + fmt(r.mach, 2) + ')') : '—'); var notes = w.concat([['warn', 'Valve factors, full IEC/ISA method, noise, actuator and mechanical checks must be verified separately.']]); $('warns').innerHTML = notes.map(function (x) { return '
' + badge(x[0]) + '
' + esc(x[1]) + '
'; }).join(''); var rows = [['Phase', r.ph], ['Flow', fmt(q, 4) + (r.ph === 'liquid' ? ' m³/h' : ' kg/h')], ['P1 / P2', fmt(p1, 4) + ' / ' + fmt(p2, 4) + ' bara'], ['ΔP', fmt(r.dp, 4) + ' bar'], ['FP', fmt(r.FP, 4)], ['Required Cv', fmt(r.cv, 5)], ['Required Kv', fmt(r.kv, 5)], ['Rated Cv', fmt(r.rated, 4)], ['Rated / required', fmt(r.ratio, 4)], ['Travel', fmt(r.t, 2) + '%']]; if (r.ph === 'liquid') rows.push(['Specific gravity', fmt(r.sg, 5)], ['FF', fmt(r.FF, 5)], ['Allowable ΔP (choked)', fmt(r.dpMax, 4) + ' bar'], ['ΔP / (P1 − Pv)', fmt(r.kc, 4)]); else rows.push(['x = ΔP / P1', fmt(r.x, 5)], ['Choking limit Fk·xT', fmt(r.xc, 5)], ['Expansion factor Y', fmt(r.Y, 5)], ['Inlet density', fmt(r.rho1, 4) + ' kg/m³']); $('inter').innerHTML = rows.map(function (a) { return '' + esc(a[0]) + '' + esc(a[1]) + ''; }).join(''); marks.main = { t: r.t, f: r.cv / r.rated }; draw(); renderReport(); return true; } /* ---------- operating cases ---------- */ function calcCases() { lastCases = []; marks.cases = []; CASES.forEach(function (c, i) { var q = n('c' + i + 'q'), p1 = n('c' + i + 'a'), p2 = n('c' + i + 'b'), e = check(q, p1, p2); var cell = function (k) { return $('c' + i + k); }; if (e.length) { ['cv', 'r', 't'].forEach(function (k) { cell(k).textContent = '—'; }); cell('s').innerHTML = badge('bad') + ' ' + esc(e[0]); lastCases.push(null); marks.cases.push(null); return; } var r = size(q, p1, p2), w = assess(r), l = lvlOf(w); r.name = c[0]; r.warns = w; lastCases.push(r); cell('cv').textContent = fmt(r.cv, 3); cell('r').textContent = fmt(r.ratio, 2) + '×'; cell('t').textContent = fmt(r.t, 1); cell('s').innerHTML = badge(l) + (w.length ? ' ' + esc(w[0][1]) : ''); marks.cases.push({ t: r.t, f: r.cv / r.rated }); }); draw(); renderReport(); } /* ---------- report / export ---------- */ function renderReport() { if (!last) return; var rows = [['Project', $('project').value], ['Plant / unit', $('plant').value], ['Valve tag', $('tag').value], ['Service', $('service').value], ['Phase', last.ph], ['Flow', fmt(last.q, 4) + (last.ph === 'liquid' ? ' m³/h' : ' kg/h')], ['P1 / P2', fmt(last.p1, 4) + ' / ' + fmt(last.p2, 4) + ' bara'], ['Required Cv', fmt(last.cv, 4)], ['Rated Cv', fmt(last.rated, 4)], ['Rated / required', fmt(last.ratio, 3)], ['Travel', fmt(last.t, 1) + '%'], ['Flow regime', last.regime], ['Cavitation / flashing', last.cav]]; var cs = lastCases.filter(Boolean).map(function (r) { return '' + esc(r.name) + '' + fmt(r.q, 3) + '' + fmt(r.p1, 3) + '/' + fmt(r.p2, 3) + '' + fmt(r.cv, 3) + '' + fmt(r.t, 1) + '%'; }).join(''); $('reportText').innerHTML = '

Control valve sizing report

' + rows.map(function (a) { return ''; }).join('') + '
' + esc(a[0]) + '' + esc(a[1]) + '
' + (cs ? '

Operating cases

' + cs + '
CaseFlowP1/P2 baraReq. CvTravel
' : '') + '
Preliminary calculation. Not equivalent to CONVAL or a validated IEC/ISA package. Verify all inputs, factors and results independently.
'; } function collect() { var o = { application: 'Control Valve Sizing Simulator', version: '1.0', exportedAt: new Date().toISOString(), inputs: {} }; ids.forEach(function (id) { o.inputs[id] = $(id).value; }); o.lastCalculation = last; o.operatingCases = lastCases; return o; } function apply(o) { var s = (o && o.inputs) || o || {}; ids.forEach(function (id) { if (s[id] !== undefined && s[id] !== null && typeof s[id] !== 'object') $(id).value = String(s[id]); }); phaseUI(); } function download(name, content, type) { var url = URL.createObjectURL(new Blob([content], { type: type })), a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function csvCell(v) { v = String(v == null ? '' : v); if (/^[=+\-@]/.test(v)) v = "'" + v; return '"' + v.replace(/"/g, '""') + '"'; } function base() { return ($('tag').value || 'control-valve').replace(/[^\w\-]+/g, '_'); } function resetAll() { ids.forEach(function (id) { $(id).value = defaults[id]; }); last = null; lastCases = []; marks = { main: null, cases: [] }; ['reqCv', 'reqKv', 'ratio', 'trv', 'dp', 'regime', 'cav', 'vel'].forEach(function (id) { txt(id, '—'); }); CASES.forEach(function (c, i) { ['cv', 'r', 't', 's'].forEach(function (k) { $('c' + i + k).textContent = '—'; }); }); $('validation').textContent = 'Enter data and select Calculate sizing.'; $('warns').innerHTML = '
Run a calculation to populate checks.
'; $('inter').innerHTML = 'Not yet calculated'; $('reportText').innerHTML = '

Report preview

Run a calculation to generate the report.

'; phaseUI(); draw(); } /* ---------- events ---------- */ $('toValve').addEventListener('click', function () { tab('valve'); }); $('toResults').addEventListener('click', function () { tab('results'); }); $('calc').addEventListener('click', function () { if (calcMain()) tab('results'); }); $('calcCases').addEventListener('click', calcCases); $('json').addEventListener('click', function () { download(base() + '-project.json', JSON.stringify(collect(), null, 2), 'application/json'); }); $('csv').addEventListener('click', function () { if (!last) { alert('Run sizing first.'); return; } var L = [['Parameter', 'Value'], ['Project', $('project').value], ['Tag', $('tag').value], ['Phase', last.ph], ['Flow', last.q], ['P1 bara', last.p1], ['P2 bara', last.p2], ['dP bar', last.dp], ['Required Cv', last.cv], ['Required Kv', last.kv], ['Rated Cv', last.rated], ['Rated/required', last.ratio], ['Travel %', last.t], ['Regime', last.regime], ['Cavitation/flashing', last.cav]]; lastCases.forEach(function (r) { if (r) L.push(['Case ' + r.name + ' flow / P1 / P2 / Cv / travel%', [r.q, r.p1, r.p2, r.cv, r.t].join(' / ')]); }); L.push(['Warning', 'Preliminary model; verify independently']); download(base() + '-results.csv', L.map(function (row) { return row.map(csvCell).join(','); }).join('\r\n'), 'text/csv'); }); $('print').addEventListener('click', function () { if (!last) calcMain(); tab('report'); window.print(); }); $('save').addEventListener('click', function () { try { localStorage.setItem('cvs-valve-save', JSON.stringify(collect())); alert('Saved in this browser.'); } catch (e) { alert('Browser storage unavailable. Use JSON export.'); } }); $('load').addEventListener('click', function () { try { var s = localStorage.getItem('cvs-valve-save'); if (!s) { alert('No browser save found.'); return; } apply(JSON.parse(s)); calcMain(); } catch (e) { alert('Could not load the saved project.'); } }); $('reset').addEventListener('click', function () { if (confirm('Reset all inputs to the example values?')) resetAll(); }); $('file').addEventListener('change', function (ev) { var f = ev.target.files[0]; if (!f) return; var rd = new FileReader(); rd.onload = function () { try { apply(JSON.parse(rd.result)); calcMain(); alert('Project imported. Review the inputs and warnings.'); } catch (e) { alert('Invalid project JSON.'); } }; rd.readAsText(f); }); root.querySelectorAll('input,select').forEach(function (el) { if (el.type === 'file') return; el.addEventListener('change', function () { phaseUI(); if (last) calcMain(); if (lastCases.length) calcCases(); }); }); phaseUI(); draw(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();