Press fleet calculator: target output and specials share to fleet mix and needs
Deploy test site / deploy (push) Successful in 37s
Deploy test site / deploy (push) Successful in 37s
User, October 2026: calculations for the later business plan, starting from 2 Duplex + 1 Classic at 3 records a minute per Duplex and 2 per Classic, as an interactive tool on the website: pick the yearly output and the share of splatter/specials, get the machine mix and all the requirements (people, hydraulics, power, ...). docs/fleet-calculator.html (single static page, no dependencies): - target mode sizes the fleet (specials only on Duplex with its EX 35; grow with Classics or with Duplex systems; start fleet as the floor); fleet mode takes a fleet and gives its output at the specials share; - output, capacity, utilisation and the mix bar per machine type; - needs: people per shift and in total, galvanics operators, machines, connected kVA and average kW, MWh/yr, central hydraulics (flow, motor, oil, accumulator), steam, cooling and cooling water, compressed air, press floor, room climate, compound, trim, titles, electroformed parts and plating units; checks against the 400 kg/h boiler, 250 kW chiller and the 870 m2 Payap press hall; - ladder from the start fleet to a full Payap press hall, click to load; - every assumption editable with its source (Newbilt Duplex site requirements V14 and offer 240620-04, scale.md, galvanics.md, revenue model, Payap model; estimates labelled), shareable link via the hash. The Classic counts as one HRP 12 press plus one extruder until Newbilt states figures (Toolex Alpha leaflet as a cross-check). Site: DEMOS entry (/demos/fleet-calculator/) and landing-page entry. README bullet and layout line; backlinks from scale.md (capacity) and pressing-line.md (current plan).
This commit is contained in:
1 parent
68539d563c
commit
c6bec71442
6 files changed
+631
-1
No files matched your search
@@ -0,0 +1,596 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>CNX Vinyl · press fleet calculator</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0f1a; --panel: #0f1626; --panel2: #131c30; --stroke: #2a3a56;
|
||||
--line: #1c2840; --fg: #d6deeb; --mute: #8a97b0; --accent: #3ddc84;
|
||||
--warn: #ffb547; --bad: #ff6b6b; --dspec: #c58bff; --dstd: #6aa8ff;
|
||||
--classic: #3ddc84;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--fg);
|
||||
font: 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
a { color: var(--accent); }
|
||||
header, .wrap { max-width: 1320px; margin: 0 auto; padding: 0 20px; }
|
||||
header { padding-top: 28px; }
|
||||
h1 { margin: 0; font-size: 26px; letter-spacing: 0.02em; }
|
||||
.tag {
|
||||
display: inline-block; margin-top: 6px; padding: 2px 8px;
|
||||
border: 1px solid var(--stroke); border-radius: 4px; color: var(--mute); font-size: 13px;
|
||||
}
|
||||
.lead { color: var(--mute); max-width: 920px; margin: 14px 0 22px; }
|
||||
.grid { display: grid; grid-template-columns: 370px 1fr; gap: 18px; align-items: start; }
|
||||
@media (max-width: 960px) { .grid { grid-template-columns: 1fr; } }
|
||||
.card {
|
||||
background: var(--panel); border: 1px solid var(--stroke); border-radius: 8px;
|
||||
padding: 16px 18px; margin-bottom: 18px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--mute); margin: 0 0 12px;
|
||||
}
|
||||
.row { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin: 9px 0; }
|
||||
.row > span { flex: 1; }
|
||||
.row small, .hint { color: var(--mute); font-size: 12.5px; }
|
||||
.hint { margin: 6px 0 0; }
|
||||
input[type=number] {
|
||||
width: 118px; background: #0a1120; color: var(--fg); border: 1px solid var(--stroke);
|
||||
border-radius: 4px; padding: 5px 7px; font: inherit; text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
input[type=number]:focus { outline: none; border-color: var(--accent); }
|
||||
input[type=range] { width: 100%; accent-color: var(--accent); }
|
||||
.seg { display: flex; border: 1px solid var(--stroke); border-radius: 6px; overflow: hidden; margin: 6px 0 10px; }
|
||||
.seg button {
|
||||
flex: 1; background: transparent; color: var(--mute); border: 0; padding: 7px 8px;
|
||||
font: inherit; font-size: 14px; cursor: pointer;
|
||||
}
|
||||
.seg button + button { border-left: 1px solid var(--stroke); }
|
||||
.seg button.on { background: var(--panel2); color: var(--accent); }
|
||||
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.pair .row { margin: 4px 0; }
|
||||
.pair input[type=number] { width: 70px; }
|
||||
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; }
|
||||
.kpi { background: var(--panel2); border: 1px solid var(--stroke); border-radius: 6px; padding: 10px 12px; }
|
||||
.kpi b { display: block; font-size: 24px; font-variant-numeric: tabular-nums; }
|
||||
.kpi span { color: var(--mute); font-size: 13px; }
|
||||
.kpi.d b { color: var(--dstd); } .kpi.c b { color: var(--classic); }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
th, td { padding: 6px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
||||
th { color: var(--mute); font-weight: 500; font-size: 12.5px; }
|
||||
td.n, th.n { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
tr.grp td {
|
||||
color: var(--accent); font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em;
|
||||
padding-top: 14px; border-bottom-color: var(--stroke);
|
||||
}
|
||||
td.src { color: var(--mute); font-size: 12.5px; }
|
||||
.flag { border-left: 3px solid var(--warn); background: #1b1a12; padding: 7px 10px; margin: 8px 0 0; font-size: 14px; }
|
||||
.flag.ok { border-color: var(--accent); background: #0f1a14; }
|
||||
.flag.bad { border-color: var(--bad); background: #1d1214; }
|
||||
.bar { display: flex; height: 24px; border-radius: 4px; overflow: hidden; margin: 14px 0 8px; background: var(--panel2); }
|
||||
.bar div { height: 100%; }
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13.5px; color: var(--mute); }
|
||||
.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; }
|
||||
#ladT tbody tr { cursor: pointer; }
|
||||
#ladT tbody tr:hover td { background: var(--panel2); }
|
||||
#ladT tbody tr.cur td { background: #12261c; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 11px; padding: 0 6px; border-radius: 3px;
|
||||
border: 1px solid var(--stroke); color: var(--mute); white-space: nowrap;
|
||||
}
|
||||
.badge.vendor { color: #9fd0ff; border-color: #2c4c73; }
|
||||
.badge.doc { color: #b5e8c7; border-color: #2b5a3d; }
|
||||
.badge.est { color: #ffd59a; border-color: #6b5426; }
|
||||
.badge.user { color: #e2c4ff; border-color: #563a73; }
|
||||
#asT input[type=number] { width: 92px; }
|
||||
#asT tr.chg input { border-color: var(--warn); }
|
||||
.btns { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
|
||||
.btns button {
|
||||
background: var(--panel2); color: var(--fg); border: 1px solid var(--stroke); border-radius: 5px;
|
||||
padding: 7px 12px; font: inherit; font-size: 14px; cursor: pointer;
|
||||
}
|
||||
.btns button:hover { border-color: var(--accent); }
|
||||
.note { color: var(--mute); font-size: 13.5px; }
|
||||
.note p { margin: 6px 0; }
|
||||
footer { color: var(--mute); font-size: 13px; padding: 10px 20px 40px; max-width: 1320px; margin: 0 auto; }
|
||||
[hidden] { display: none !important; }
|
||||
.grid > div { min-width: 0; }
|
||||
.tscroll { overflow-x: auto; }
|
||||
.tscroll table { min-width: 560px; }
|
||||
#ladT { min-width: 860px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Press fleet calculator</h1>
|
||||
<span class="tag">planning estimates · not a quote</span>
|
||||
<p class="lead">
|
||||
Set the records you want a year and the share of splatter and other
|
||||
specials. The calculator sizes the press fleet from Newbilt Duplex systems
|
||||
(two HRP 12 presses, an EX 50 and an EX 35 colour extruder each) and Newbilt
|
||||
Classic automatic presses, starting from the planned two Duplex systems and
|
||||
one Classic. It then lists what that fleet needs: people, power, central
|
||||
hydraulics, steam, cooling, compressed air, floor, compound and stampers.
|
||||
Vendor figures are from Newbilt's Duplex site requirements and offer; the
|
||||
rest are planning figures and estimates from the CNX Vinyl documents. Every
|
||||
figure can be changed under Assumptions.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="wrap grid">
|
||||
<div>
|
||||
<section class="card">
|
||||
<h2>What you want</h2>
|
||||
<div class="seg" id="mode">
|
||||
<button data-v="target">From a target output</button>
|
||||
<button data-v="fleet">From a fleet</button>
|
||||
</div>
|
||||
<div id="targetBox">
|
||||
<label class="row"><span>Records per year</span><input type="number" id="target" min="0" step="50000"></label>
|
||||
<input type="range" id="targetR" min="0" max="10000000" step="50000" aria-label="Records per year">
|
||||
<div class="row" style="margin-top:12px"><span>Grow the fleet with</span></div>
|
||||
<div class="seg" id="grow">
|
||||
<button data-v="classic">Classics</button>
|
||||
<button data-v="duplex">Duplex systems</button>
|
||||
</div>
|
||||
<div class="hint" style="margin-bottom:6px">Start fleet, never fewer than:</div>
|
||||
<div class="pair">
|
||||
<label class="row"><span>Duplex</span><input type="number" id="dMin" min="0" step="1"></label>
|
||||
<label class="row"><span>Classic</span><input type="number" id="cMin" min="0" step="1"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fleetBox" hidden>
|
||||
<div class="pair">
|
||||
<label class="row"><span>Duplex systems</span><input type="number" id="dFleet" min="0" step="1"></label>
|
||||
<label class="row"><span>Classics</span><input type="number" id="cFleet" min="0" step="1"></label>
|
||||
</div>
|
||||
<p class="hint">The output is what this fleet makes at the set effectiveness and specials share.</p>
|
||||
</div>
|
||||
<label class="row" style="margin-top:14px"><span>Splatter and specials, share of output</span><b id="specV"></b></label>
|
||||
<input type="range" id="spec" min="0" max="100" step="1" aria-label="Specials share">
|
||||
<p class="hint">Splatter, multi-colour and effect records run only on the Duplex, with
|
||||
its EX 35 colour extruder (Newbilt recommends the Duplex for splatter). Black and
|
||||
single-colour records run on either press.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Operation</h2>
|
||||
<label class="row"><span>Shifts per day</span><input type="number" id="shifts" min="1" max="3" step="1"></label>
|
||||
<label class="row"><span>Hours per shift</span><input type="number" id="hrs" min="1" max="12" step="0.5"></label>
|
||||
<label class="row"><span>Working days per year</span><input type="number" id="days" min="1" max="365" step="1"></label>
|
||||
<label class="row"><span>Overall effectiveness, %<br><small>scale.md: 60 to 70 % (changeovers, stamper changes, QC, breaks)</small></span><input type="number" id="eff" min="1" max="100" step="1"></label>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Press rates</h2>
|
||||
<label class="row"><span>Duplex system, records per minute<br><small>both presses together; 3 = 40 s per press cycle. Newbilt quotes about 30 s per press</small></span><input type="number" id="rateD" min="0" step="0.1"></label>
|
||||
<label class="row"><span>Classic, records per minute<br><small>2 = 30 s cycle; Newbilt flyer: 28 s at 180 g</small></span><input type="number" id="rateC" min="0" step="0.1"></label>
|
||||
<label class="row"><span>Specials run at, % of the standard rate<br><small>no vendor figure; lower it to test slower splatter runs</small></span><input type="number" id="specF" min="10" max="100" step="5"></label>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<section class="card">
|
||||
<h2>Fleet</h2>
|
||||
<div class="kpis" id="fleetK"></div>
|
||||
<div id="flags"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Output and machine mix</h2>
|
||||
<div class="kpis" id="outK"></div>
|
||||
<div class="bar" id="bar"></div>
|
||||
<div class="legend" id="legend"></div>
|
||||
<div class="tscroll" style="margin-top:12px"><table id="mixT"></table></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>What the fleet needs</h2>
|
||||
<div class="tscroll"><table id="reqT"></table></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<section class="card">
|
||||
<h2>From small to fully equipped</h2>
|
||||
<p class="note" style="margin-top:0">Fleets from the start fleet to a full Payap press hall (about 870 m²:
|
||||
the two Duplex bays plus the Classic and ten more Classic positions, or the same floor
|
||||
with more Duplex systems), at the current settings. Click a row to load it.</p>
|
||||
<div class="tscroll"><table id="ladT"></table></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Assumptions</h2>
|
||||
<p class="note" style="margin-top:0">Every result above comes from these figures. Changed values are
|
||||
kept in the link, so a scenario can be shared with "Copy link".</p>
|
||||
<div class="tscroll"><table id="asT"></table></div>
|
||||
<div class="btns">
|
||||
<button id="copy">Copy link to this scenario</button>
|
||||
<button id="reset">Reset everything</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card note">
|
||||
<h2>Basis and limits</h2>
|
||||
<p><b>Classic utilities.</b> Newbilt has published no power, steam, cooling or weight figures for
|
||||
the Classic, so it counts here as one HRP 12 press plus one extruder on the central hydraulics.
|
||||
Cross-check from the Toolex Alpha leaflet the Classic is based on: 1.2 kg steam, 15 l cooling
|
||||
water, 18 l air and 3.15 l hydraulic oil per cycle (about 144 kg/h of steam at 2 a minute),
|
||||
2.18 × 1.30 m, 1,850 kg. These go into the re-quote questions in pressing-line.md.</p>
|
||||
<p><b>EX 35.</b> Counted at the EX 50's electrical, air and water figures; it is the smaller
|
||||
extruder, so this errs high.</p>
|
||||
<p><b>Central hydraulics.</b> Newbilt's aggregate serves two presses; the per-press figures are
|
||||
half of it, summed without any diversity. Whether the Classic can run from the shared plant
|
||||
is an open question for Newbilt.</p>
|
||||
<p><b>Peaks add up.</b> Steam, cooling and air are summed at each machine's peak, as Newbilt
|
||||
states them. A real plant staggers cycles, so the boiler and chiller can be sized lower once
|
||||
a utility engineer has the cycle data.</p>
|
||||
<p><b>Not scaled here:</b> mastering (1 to 2), QC (1 to 2), maintenance (2) and admin, sales and
|
||||
logistics (3 to 5), from scale.md; cover for leave and sickness; the boiler fuel and the PV
|
||||
share of the power.</p>
|
||||
<p>Sources in the CNX Vinyl planning repository: equipment/pressing-line.md,
|
||||
equipment/newbilt/ (Duplex site requirements V14, offer 240620-04, 2026 flyers),
|
||||
equipment/galvanics.md, scale.md, finance/revenue-model.md, site-candidates.md (Payap scenario model).</p>
|
||||
</section>
|
||||
</div>
|
||||
<footer>CNX Vinyl planning repository · docs/fleet-calculator.html · figures are planning estimates, October 2026.</footer>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
// ---------------------------------------------------------------- inputs
|
||||
const S0 = {
|
||||
mode: "target", target: 1000000, spec: 20, grow: "classic", dMin: 2, cMin: 1,
|
||||
dFleet: 2, cFleet: 1, shifts: 2, hrs: 8, days: 300, eff: 65, rateD: 3, rateC: 2, specF: 100,
|
||||
};
|
||||
// [group, id, label, value, unit, kind, source]
|
||||
const AS = [];
|
||||
function grp(g, rows) { rows.forEach(r => AS.push([g].concat(r))); }
|
||||
grp("Personnel", [
|
||||
["opD", "Operators per Duplex system, per shift", 1, "", "doc", "Duplex cell layout: one operator in the middle of each cell (pressing-line.md)"],
|
||||
["cPerAtt", "Classics one attendant looks after", 4, "", "est", "Assumption: Newbilt has not stated it"],
|
||||
["finRate", "Finishing output per person", 200, "records/h", "est", "Inspection, sleeving, packing; in line with scale.md (4 to 6 press and finishing people per shift for the start fleet)"],
|
||||
["galvRate", "Electroformed parts per galvanics operator", 20, "a day", "vendor", "OMS rule of thumb: one person runs about 20 stampers a day (galvanics.md)"],
|
||||
]);
|
||||
grp("Per press (HRP 12; a Classic counts as one)", [
|
||||
["pressKVA", "Connected load", 1, "kVA", "vendor", "Newbilt Duplex site requirements 3.2.1"],
|
||||
["pressKW", "Average power", 0.5, "kW", "vendor", "Site requirements 3.2.1"],
|
||||
["steamLo", "Steam, low", 150, "kg/h", "vendor", "Site requirements 3.2.3: 150 to 180 kg/h, 10 bar, 180 °C"],
|
||||
["steamHi", "Steam, high", 180, "kg/h", "vendor", "Site requirements 3.2.3"],
|
||||
["coolPress", "Cooling capacity, peak", 125, "kW", "vendor", "Site requirements 3.2.2: up to 125 kW depending on cycle time"],
|
||||
["waterTyp", "Cooling water, typical", 53, "l/min", "vendor", "Site requirements 3.2.2: about 53 l/min used"],
|
||||
["waterPeak", "Cooling water, peak", 100, "l/min", "vendor", "Site requirements 3.2.2: up to 100 l/min"],
|
||||
["airPress", "Compressed air", 6, "m³/h", "vendor", "Site requirements 3.2.3: 6 to 10 bar"],
|
||||
]);
|
||||
grp("Per extruder (EX 50; EX 35 and Classic extruder at the same figures)", [
|
||||
["extKVA", "Connected load", 15, "kVA", "vendor", "Site requirements 3.3.1 (EX 35 and Classic: estimate)"],
|
||||
["extKW", "Average power", 8, "kW", "vendor", "Site requirements 3.3.1"],
|
||||
["extAir", "Compressed air", 2, "m³/h", "vendor", "Site requirements 3.3.2"],
|
||||
["extWater", "Cooling water", 4, "l/min", "vendor", "Site requirements 3.3.3"],
|
||||
["extCool", "Cooling capacity", 3, "kW", "vendor", "Site requirements 3.3.3: about 2 to 3 kW"],
|
||||
]);
|
||||
grp("Central hydraulics, per press (Newbilt's aggregate serves two)", [
|
||||
["hydFlow", "Pump flow", 16, "l/min", "vendor", "Offer 240620-04: 32 l/min pump per aggregate"],
|
||||
["hydMotor", "Installed motor power", 7.5, "kW", "vendor", "Offer 240620-04: 11/15 kW motor per aggregate"],
|
||||
["hydKVA", "Connected load", 7.5, "kVA", "vendor", "Site requirements 3.4.1: 15 kVA per aggregate"],
|
||||
["hydKW", "Average power", 6, "kW", "vendor", "Site requirements 3.4.1: 12 kW per aggregate"],
|
||||
["hydOil", "Oil fill (HLP 46)", 100, "l", "vendor", "Site requirements 3.4.3: 200 l per aggregate, 250 l tank"],
|
||||
["hydAcc", "Accumulator volume", 25, "l", "vendor", "Offer 240620-04: 50 l per aggregate, up to 200 bar"],
|
||||
["hydWater", "Oil cooler water", 4, "l/min", "vendor", "Site requirements 3.4.2: about 8 l/min per aggregate"],
|
||||
["hydCool", "Oil cooler capacity", 2.5, "kW", "vendor", "Site requirements 3.4.2: up to 5 kW per aggregate"],
|
||||
]);
|
||||
grp("Utilities to supply it", [
|
||||
["cop", "Chiller COP", 4, "", "est", "Estimate for a water-cooled chiller"],
|
||||
["compSpec", "Compressor specific power", 0.11, "kW per m³/h", "est", "Estimate: screw compressor at about 6.5 kW per m³/min"],
|
||||
]);
|
||||
grp("Floor", [
|
||||
["floorD", "Press floor per Duplex system", 110, "m²", "doc", "Payap model bay, 9.5 × 11.4 m; Newbilt recommends about 200 m² including storage and side rooms"],
|
||||
["floorC", "Press floor per Classic", 50, "m²", "est", "Payap model position, 5.5 × 9 m; pressing-line.md estimates 50 to 75 m²"],
|
||||
]);
|
||||
grp("Material", [
|
||||
["weight", "Average record weight", 160, "g", "est", "Estimate across the 140 and 180 g formats"],
|
||||
["trim", "Edge trim per record", 30, "g", "vendor", "Newbilt granulator flyer: 20 to 40 g per 12-inch record, back as regrind"],
|
||||
["reject", "Rejects", 5, "%", "est", "Estimate; Newbilt claims at least 95 % yield for the Classic"],
|
||||
]);
|
||||
grp("Stampers", [
|
||||
["runLen", "Records per title", 3000, "", "doc", "Revenue model: 2,500 to 3,300 records per title"],
|
||||
["stLife", "Pressings per stamper", 1500, "", "doc", "galvanics.md: 1,000 to 2,000"],
|
||||
["pdpuLo", "Parts per plating unit a day, low", 20, "", "doc", "galvanics.md: 20 to 32 per Pheenix PDPU on two shifts"],
|
||||
["pdpuHi", "Parts per plating unit a day, high", 32, "", "doc", "galvanics.md"],
|
||||
]);
|
||||
grp("Planned plant, for the checks", [
|
||||
["boiler", "Steam boiler", 400, "kg/h", "doc", "scale.md: sized for one Duplex system"],
|
||||
["chiller", "Chiller", 250, "kW", "doc", "scale.md: sized for one Duplex system"],
|
||||
["hall", "Payap press hall", 870, "m²", "doc", "site-candidates.md, Payap scenario model"],
|
||||
]);
|
||||
const A0 = Object.fromEntries(AS.map(r => [r[1], r[3]]));
|
||||
const KIND = { vendor: "vendor", doc: "doc", est: "estimate", user: "your figure" };
|
||||
const LADDER = [
|
||||
["Start fleet (current plan)", 2, 1], ["Step 1", 2, 3], ["Step 2", 2, 5], ["Step 3", 2, 8],
|
||||
["Full hall, Classic-led", 2, 11], ["Full hall, mixed", 4, 7], ["Full hall, specials-led", 6, 3],
|
||||
];
|
||||
|
||||
let S = { ...S0 }, A = { ...A0 };
|
||||
|
||||
// ---------------------------------------------------------------- model
|
||||
function basis(S) {
|
||||
const hoursYr = S.shifts * S.hrs * S.days, eff = S.eff / 100;
|
||||
return {
|
||||
hoursYr,
|
||||
capD: Math.max(0, S.rateD) * 60 * hoursYr * eff,
|
||||
capC: Math.max(0, S.rateC) * 60 * hoursYr * eff,
|
||||
s: Math.min(1, Math.max(0, S.spec / 100)),
|
||||
f: Math.min(1, Math.max(0.1, S.specF / 100)),
|
||||
};
|
||||
}
|
||||
// Largest yearly output a fleet makes at specials share s: specials only on
|
||||
// Duplex (at rate factor f), standard on Classics first, then on spare Duplex time.
|
||||
function capacityOf(D, C, b) {
|
||||
const x1 = (D * b.capD + C * b.capC) / (b.s / b.f + 1 - b.s);
|
||||
const x2 = b.s > 0 ? D * b.capD * b.f / b.s : Infinity;
|
||||
return Math.max(0, Math.min(x1, x2));
|
||||
}
|
||||
function sizeFleet(S, b) {
|
||||
const T = Math.max(0, S.target), sp = T * b.s, st = T - sp;
|
||||
const dEq = b.capD > 0 ? sp / (b.capD * b.f) : 0;
|
||||
const dMin = Math.max(0, Math.round(S.dMin)), cMin = Math.max(0, Math.round(S.cMin));
|
||||
let D, C;
|
||||
if (b.capD <= 0) return { D: dMin, C: b.capC > 0 ? Math.max(cMin, Math.ceil(st / b.capC - 1e-9)) : cMin };
|
||||
if (S.grow === "classic" && b.capC > 0) {
|
||||
D = Math.max(dMin, Math.ceil(dEq - 1e-9));
|
||||
const spare = (D - dEq) * b.capD;
|
||||
C = Math.max(cMin, Math.ceil(Math.max(0, st - spare) / b.capC - 1e-9));
|
||||
} else {
|
||||
C = cMin;
|
||||
const stC = Math.min(st, C * b.capC);
|
||||
D = Math.max(dMin, Math.ceil(dEq + (st - stC) / b.capD - 1e-9));
|
||||
}
|
||||
return { D, C };
|
||||
}
|
||||
function compute(D, C, X, b) {
|
||||
const sp = X * b.s, st = X - sp;
|
||||
const stC = Math.min(st, C * b.capC), stD = st - stC;
|
||||
const dUse = D > 0 && b.capD > 0 ? (sp / (b.capD * b.f) + stD / b.capD) / D : 0;
|
||||
const cUse = C > 0 && b.capC > 0 ? stC / (C * b.capC) : 0;
|
||||
const P = 2 * D + C, E = 2 * D + C; // presses; extruders (EX 50 + EX 35, Classic built in)
|
||||
const perHour = b.hoursYr > 0 ? X / b.hoursYr : 0;
|
||||
const att = C > 0 ? Math.ceil(C / Math.max(1, A.cPerAtt)) : 0;
|
||||
const fin = X > 0 ? Math.ceil(perHour / Math.max(1, A.finRate)) : 0;
|
||||
const perShift = D * A.opD + att + fin;
|
||||
const perSide = Math.ceil(A.runLen / Math.max(1, A.stLife));
|
||||
const partsTitle = 2 * (2 + perSide);
|
||||
const titles = X / Math.max(1, A.runLen);
|
||||
const partsYr = titles * partsTitle, partsDay = partsYr / Math.max(1, S.days);
|
||||
const coolPeak = P * A.coolPress + E * A.extCool + P * A.hydCool;
|
||||
const air = P * A.airPress + E * A.extAir;
|
||||
const kvaMach = P * A.pressKVA + E * A.extKVA, kwMach = P * A.pressKW + E * A.extKW;
|
||||
const kvaHyd = P * A.hydKVA, kwHyd = P * A.hydKW;
|
||||
return {
|
||||
D, C, X, P, E, sp, st, stC, stD, dUse, cUse, perHour, att, fin, perShift,
|
||||
staff: perShift * S.shifts, galv: partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.galvRate)) : 0,
|
||||
perSide, partsTitle, titles, partsYr, partsDay,
|
||||
pdpu: [partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.pdpuHi)) : 0, partsDay > 0 ? Math.ceil(partsDay / Math.max(1, A.pdpuLo)) : 0],
|
||||
kvaMach, kwMach, kvaHyd, kwHyd, kva: kvaMach + kvaHyd, kw: kwMach + kwHyd,
|
||||
mwh: (kwMach + kwHyd) * b.hoursYr / 1000,
|
||||
chillerKW: coolPeak / Math.max(0.5, A.cop), compKW: air * A.compSpec,
|
||||
steam: [P * A.steamLo, P * A.steamHi], coolPeak,
|
||||
water: [P * A.waterTyp + E * A.extWater + P * A.hydWater, P * A.waterPeak + E * A.extWater + P * A.hydWater],
|
||||
air, hyd: { flow: P * A.hydFlow, motor: P * A.hydMotor, oil: P * A.hydOil, acc: P * A.hydAcc },
|
||||
floor: D * A.floorD + C * A.floorC,
|
||||
compound: X * A.weight * (1 + A.reject / 100) / 1e6,
|
||||
extKgH: perHour * (A.weight + A.trim) * (1 + A.reject / 100) / 1000,
|
||||
trimT: X * (1 + A.reject / 100) * A.trim / 1e6,
|
||||
};
|
||||
}
|
||||
function scenario() {
|
||||
const b = basis(S);
|
||||
let D, C, X, cap;
|
||||
if (S.mode === "fleet") {
|
||||
D = Math.max(0, Math.round(S.dFleet)); C = Math.max(0, Math.round(S.cFleet));
|
||||
if (D === 0 && b.s > 0) { b.s = 0; b.noSpecials = true; }
|
||||
cap = capacityOf(D, C, b); X = cap;
|
||||
} else {
|
||||
({ D, C } = sizeFleet(S, b));
|
||||
cap = capacityOf(D, C, b); X = Math.min(Math.max(0, S.target), cap);
|
||||
}
|
||||
return { b, cap, r: compute(D, C, X, b) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- format
|
||||
const nf = d => new Intl.NumberFormat("en-US", { maximumFractionDigits: d, minimumFractionDigits: 0 });
|
||||
const f0 = x => nf(0).format(Math.round(x));
|
||||
const f1 = x => nf(1).format(x);
|
||||
const big = x => x >= 1e6 ? nf(2).format(x / 1e6) + " M" : f0(x);
|
||||
const pct = x => Math.round(x * 100) + " %";
|
||||
const esc = s => String(s).replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
// ---------------------------------------------------------------- render
|
||||
function kpi(v, l, cls) { return `<div class="kpi ${cls || ""}"><b>${v}</b><span>${l}</span></div>`; }
|
||||
function render() {
|
||||
const { b, cap, r } = scenario();
|
||||
$("specV").textContent = S.spec + " %";
|
||||
$("targetBox").hidden = S.mode !== "target";
|
||||
$("fleetBox").hidden = S.mode !== "fleet";
|
||||
for (const g of ["mode", "grow"]) for (const bt of $(g).children) bt.classList.toggle("on", bt.dataset.v === S[g]);
|
||||
|
||||
$("fleetK").innerHTML =
|
||||
kpi(r.D, `Duplex systems<br>${2 * r.D} HRP 12 presses, ${r.D} EX 50, ${r.D} EX 35, ${r.D} trimmers`, "d") +
|
||||
kpi(r.C, "Newbilt Classic<br>automatic presses", "c") +
|
||||
kpi(r.P, "presses in total") +
|
||||
kpi(r.perShift, `people at the presses and in finishing, per shift (${r.staff} on ${S.shifts} shift${S.shifts > 1 ? "s" : ""})`);
|
||||
|
||||
const flags = [];
|
||||
if (b.noSpecials)
|
||||
flags.push(["bad", "No Duplex system: this fleet cannot press splatter or specials, so the output shown is black and single colour only."]);
|
||||
if (S.mode === "target" && S.target > cap + 1)
|
||||
flags.push(["bad", `The fleet cannot reach ${big(S.target)} records: check the press rates.`]);
|
||||
if (S.mode === "target" && b.s > 0 && r.D > Math.max(0, Math.round(S.dMin)) && S.grow === "classic")
|
||||
flags.push(["warn", `Specials alone need ${r.D} Duplex systems at this share.`]);
|
||||
if (r.steam[1] > A.boiler)
|
||||
flags.push(["warn", `Steam up to ${f0(r.steam[1])} kg/h: the planned ${f0(A.boiler)} kg/h boiler covers ${pct(A.boiler / r.steam[1])} of the peak.`]);
|
||||
if (r.coolPeak > A.chiller)
|
||||
flags.push(["warn", `Cooling up to ${f0(r.coolPeak)} kW: the planned ${f0(A.chiller)} kW chiller covers ${pct(A.chiller / r.coolPeak)} of the peak.`]);
|
||||
if (r.floor > A.hall)
|
||||
flags.push(["warn", `About ${f0(r.floor)} m² of press floor: more than the ${f0(A.hall)} m² Payap press hall.`]);
|
||||
else flags.push(["ok", `About ${f0(r.floor)} m² of press floor: fits the ${f0(A.hall)} m² Payap press hall (${pct(r.floor / A.hall)}).`]);
|
||||
$("flags").innerHTML = flags.map(([c, t]) => `<div class="flag ${c}">${esc(t)}</div>`).join("");
|
||||
|
||||
const use = cap > 0 ? r.X / cap : 0;
|
||||
$("outK").innerHTML =
|
||||
kpi(big(r.X), S.mode === "target" ? "records a year (target)" : "records a year at this mix") +
|
||||
kpi(big(cap), "fleet capacity a year") +
|
||||
kpi(pct(use), "of capacity used") +
|
||||
kpi(f0(r.X / Math.max(1, S.days)), "records per working day") +
|
||||
kpi(f0(r.perHour), "records per operating hour");
|
||||
|
||||
const W = r.X > 0 ? r.X : 1;
|
||||
$("bar").innerHTML =
|
||||
`<div style="width:${100 * r.sp / W}%;background:var(--dspec)" title="Duplex, specials"></div>` +
|
||||
`<div style="width:${100 * r.stD / W}%;background:var(--dstd)" title="Duplex, standard"></div>` +
|
||||
`<div style="width:${100 * r.stC / W}%;background:var(--classic)" title="Classic, standard"></div>`;
|
||||
$("legend").innerHTML =
|
||||
`<span><i style="background:var(--dspec)"></i>Duplex, specials ${big(r.sp)} (${pct(r.sp / W)})</span>` +
|
||||
`<span><i style="background:var(--dstd)"></i>Duplex, black and single colour ${big(r.stD)} (${pct(r.stD / W)})</span>` +
|
||||
`<span><i style="background:var(--classic)"></i>Classic, black and single colour ${big(r.stC)} (${pct(r.stC / W)})</span>`;
|
||||
$("mixT").innerHTML =
|
||||
`<thead><tr><th>Machine</th><th class="n">Units</th><th class="n">Capacity a year</th><th class="n">Makes a year</th><th class="n">Utilisation</th><th class="n">Share of output</th></tr></thead><tbody>` +
|
||||
`<tr><td>Duplex system (2 presses)</td><td class="n">${r.D}</td><td class="n">${big(r.D * b.capD)}</td><td class="n">${big(r.sp + r.stD)}</td><td class="n">${pct(r.dUse)}</td><td class="n">${pct((r.sp + r.stD) / W)}</td></tr>` +
|
||||
`<tr><td>Classic</td><td class="n">${r.C}</td><td class="n">${big(r.C * b.capC)}</td><td class="n">${big(r.stC)}</td><td class="n">${pct(r.cUse)}</td><td class="n">${pct(r.stC / W)}</td></tr></tbody>`;
|
||||
|
||||
const rows = [];
|
||||
const G = t => rows.push(`<tr class="grp"><td colspan="3">${t}</td></tr>`);
|
||||
const R = (l, v, s) => rows.push(`<tr><td>${l}</td><td class="n">${v}</td><td class="src">${s || ""}</td></tr>`);
|
||||
G("People");
|
||||
R("Duplex operators, per shift", r.D * A.opD, "one operator per Duplex cell");
|
||||
R("Classic attendants, per shift", r.att, `one per ${f0(A.cPerAtt)} Classics (assumption)`);
|
||||
R("Finishing: inspection, sleeving, packing, per shift", r.fin, `at ${f0(A.finRate)} records an hour per person (estimate)`);
|
||||
R("Production staff on all shifts", r.staff, `${r.perShift} per shift × ${S.shifts}; add cover for leave`);
|
||||
R("Galvanics operators (day work)", r.galv, `${f1(r.partsDay)} parts a day at about ${f0(A.galvRate)} per person (OMS)`);
|
||||
R("Not scaled here", "7 to 11", "mastering, QC, maintenance, admin and logistics (scale.md)");
|
||||
G("Machines");
|
||||
R("HRP 12 presses (Duplex)", 2 * r.D, "");
|
||||
R("Classic automatic presses", r.C, "extruder built in; trimming not stated by Newbilt");
|
||||
R("Extruders", `${r.D} EX 50 + ${r.D} EX 35`, "one main and one colour extruder per Duplex system");
|
||||
R("Trimmers", r.D, "one per Duplex cell");
|
||||
R("Heaviest lift", r.C > 0 ? "about 1.85 t" : "1.35 t", r.C > 0 ? "Classic, if it weighs like the Toolex Alpha (unconfirmed)" : "HRP 12 press (Newbilt)");
|
||||
G("Electrical");
|
||||
R("Connected load, presses and extruders", f0(r.kvaMach) + " kVA", `${r.P} presses at ${f1(A.pressKVA)} kVA, ${r.E} extruders at ${f1(A.extKVA)} kVA`);
|
||||
R("Connected load, central hydraulics", f0(r.kvaHyd) + " kVA", `${f1(A.hydKVA)} kVA per press`);
|
||||
R("Average power while running", f0(r.kw) + " kW", "Newbilt average figures, machines and hydraulics");
|
||||
R("Energy a year, machines and hydraulics", f0(r.mwh) + " MWh", `${f0(b.hoursYr)} operating hours`);
|
||||
R("Chiller power at peak cooling", f0(r.chillerKW) + " kW", `COP ${f1(A.cop)} (estimate)`);
|
||||
R("Air compressors", f1(r.compKW) + " kW", "estimate");
|
||||
G("Central hydraulics");
|
||||
R("Pump flow", f0(r.hyd.flow) + " l/min", "up to 200 bar");
|
||||
R("Installed motor power", f1(r.hyd.motor) + " kW", "");
|
||||
R("Oil fill, HLP 46", f0(r.hyd.oil) + " l", "");
|
||||
R("Accumulator volume", f0(r.hyd.acc) + " l", "gas loaded");
|
||||
G("Steam");
|
||||
R("Saturated steam, 10 bar, 180 °C", `${f0(r.steam[0])} to ${f0(r.steam[1])} kg/h`, `planned boiler ${f0(A.boiler)} kg/h`);
|
||||
G("Cooling");
|
||||
R("Cooling capacity, peak", f0(r.coolPeak) + " kW", `about 98 % for the moulds; planned chiller ${f0(A.chiller)} kW`);
|
||||
R("Cooling water, typical to peak", `${f0(r.water[0])} to ${f0(r.water[1])} l/min`, "inlet 20 to 25 °C, 10 bar");
|
||||
G("Compressed air");
|
||||
R("Compressed air", f0(r.air) + " m³/h", "6 to 10 bar, dew point 3 °C, filtered to 5 µm");
|
||||
G("Floor and room");
|
||||
R("Press floor", f0(r.floor) + " m²", `Payap press hall ${f0(A.hall)} m²`);
|
||||
R("Press room climate", "20 to 26 °C", "35 to 60 % r.H., at least 2.5 m clear height (Newbilt)");
|
||||
G("Material");
|
||||
R("PVC compound a year", f0(r.compound) + " t", `${f0(A.weight)} g a record plus ${f0(A.reject)} % rejects`);
|
||||
R("Through the extruders", f0(r.extKgH) + " kg/h", "record plus trim, per operating hour");
|
||||
R("Trim back to the granulator", f0(r.trimT) + " t a year", `${f0(A.trim)} g a record, reused as regrind`);
|
||||
G("Stampers and galvanics");
|
||||
R("Titles a year", f0(r.titles), `${f0(A.runLen)} records a title`);
|
||||
R("Electroformed parts a year", f0(r.partsYr), `${r.partsTitle} a title: father, mother and ${r.perSide} stamper${r.perSide > 1 ? "s" : ""} per side`);
|
||||
R("Parts per working day", f1(r.partsDay), "");
|
||||
R("Plating units (Pheenix PDPU)", r.pdpu[0] === r.pdpu[1] ? r.pdpu[0] : `${r.pdpu[0]} to ${r.pdpu[1]}`, `${f0(A.pdpuLo)} to ${f0(A.pdpuHi)} parts a day each, two shifts; plus one for redundancy`);
|
||||
$("reqT").innerHTML = `<tbody>${rows.join("")}</tbody>`;
|
||||
|
||||
$("ladT").innerHTML =
|
||||
`<thead><tr><th>Fleet</th><th class="n">Duplex</th><th class="n">Classic</th><th class="n">Presses</th><th class="n">Records a year</th><th class="n">Specials a year</th><th class="n">Staff per shift</th><th class="n">Steam, kg/h</th><th class="n">Cooling, kW</th><th class="n">Connected, kVA</th><th class="n">Press floor, m²</th></tr></thead><tbody>` +
|
||||
LADDER.map(([n, d, c], i) => {
|
||||
const x = capacityOf(d, c, b), q = compute(d, c, x, b);
|
||||
const cur = r.D === d && r.C === c ? " cur" : "";
|
||||
return `<tr class="lad${cur}" data-i="${i}"><td>${n}</td><td class="n">${d}</td><td class="n">${c}</td><td class="n">${q.P}</td><td class="n">${big(x)}</td><td class="n">${big(q.sp)}</td><td class="n">${q.perShift}</td><td class="n">${f0(q.steam[1])}</td><td class="n">${f0(q.coolPeak)}</td><td class="n">${f0(q.kva)}</td><td class="n">${f0(q.floor)}</td></tr>`;
|
||||
}).join("") + "</tbody>";
|
||||
|
||||
for (const [, id] of AS) {
|
||||
const tr = document.querySelector(`#asT tr[data-id="${id}"]`);
|
||||
if (tr) tr.classList.toggle("chg", A[id] !== A0[id]);
|
||||
}
|
||||
writeHash();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- inputs and state
|
||||
const NUM = ["target", "dMin", "cMin", "dFleet", "cFleet", "shifts", "hrs", "days", "eff", "rateD", "rateC", "specF"];
|
||||
function syncInputs() {
|
||||
for (const k of NUM) $(k).value = S[k];
|
||||
$("targetR").value = S.target;
|
||||
$("spec").value = S.spec;
|
||||
for (const [, id] of AS) { const el = $("a_" + id); if (el) el.value = A[id]; }
|
||||
}
|
||||
function buildAssumptions() {
|
||||
let last = "", html = `<thead><tr><th>Figure</th><th class="n">Value</th><th>Unit</th><th>Basis</th></tr></thead><tbody>`;
|
||||
for (const [g, id, label, , unit, kind, src] of AS) {
|
||||
if (g !== last) { html += `<tr class="grp"><td colspan="4">${esc(g)}</td></tr>`; last = g; }
|
||||
html += `<tr data-id="${id}"><td>${esc(label)}</td><td class="n"><input type="number" id="a_${id}" step="any" aria-label="${esc(label)}"></td>` +
|
||||
`<td>${esc(unit)}</td><td class="src"><span class="badge ${kind}">${KIND[kind]}</span> ${esc(src)}</td></tr>`;
|
||||
}
|
||||
$("asT").innerHTML = html + "</tbody>";
|
||||
}
|
||||
function writeHash() {
|
||||
const p = [];
|
||||
for (const k in S0) if (S[k] !== S0[k]) p.push(k + "=" + encodeURIComponent(S[k]));
|
||||
for (const k in A0) if (A[k] !== A0[k]) p.push("a." + k + "=" + encodeURIComponent(A[k]));
|
||||
const h = p.length ? "#" + p.join("&") : "";
|
||||
if (location.hash !== h) history.replaceState(null, "", location.pathname + location.search + h);
|
||||
}
|
||||
function readHash() {
|
||||
for (const part of location.hash.replace(/^#/, "").split("&")) {
|
||||
if (!part) continue;
|
||||
const [k, v0] = part.split("="), v = decodeURIComponent(v0 || "");
|
||||
if (k.startsWith("a.")) { const id = k.slice(2); if (id in A0 && v !== "" && isFinite(+v)) A[id] = +v; }
|
||||
else if (k in S0) S[k] = typeof S0[k] === "number" ? (v !== "" && isFinite(+v) ? +v : S0[k]) : v;
|
||||
}
|
||||
if (!["target", "fleet"].includes(S.mode)) S.mode = "target";
|
||||
if (!["classic", "duplex"].includes(S.grow)) S.grow = "classic";
|
||||
}
|
||||
function num(el, fallback) { const v = parseFloat(el.value); return isFinite(v) ? v : fallback; }
|
||||
|
||||
buildAssumptions();
|
||||
readHash();
|
||||
syncInputs();
|
||||
for (const k of NUM) $(k).addEventListener("input", e => { S[k] = num(e.target, S0[k]); if (k === "target") $("targetR").value = S.target; render(); });
|
||||
$("targetR").addEventListener("input", e => { S.target = +e.target.value; $("target").value = S.target; render(); });
|
||||
$("spec").addEventListener("input", e => { S.spec = +e.target.value; render(); });
|
||||
for (const g of ["mode", "grow"]) $(g).addEventListener("click", e => {
|
||||
const v = e.target.dataset && e.target.dataset.v; if (!v) return;
|
||||
if (g === "mode" && v === "fleet" && S.mode === "target") { const { r } = scenario(); S.dFleet = r.D; S.cFleet = r.C; syncInputs(); }
|
||||
S[g] = v; render();
|
||||
});
|
||||
$("asT").addEventListener("input", e => {
|
||||
const id = e.target.id.replace(/^a_/, ""); if (!(id in A0)) return;
|
||||
A[id] = num(e.target, A0[id]); render();
|
||||
});
|
||||
$("ladT").addEventListener("click", e => {
|
||||
const tr = e.target.closest("tr.lad"); if (!tr) return;
|
||||
const [, d, c] = LADDER[+tr.dataset.i];
|
||||
S.mode = "fleet"; S.dFleet = d; S.cFleet = c; syncInputs(); render();
|
||||
});
|
||||
$("reset").addEventListener("click", () => { S = { ...S0 }; A = { ...A0 }; syncInputs(); render(); });
|
||||
$("copy").addEventListener("click", () => {
|
||||
const done = () => { $("copy").textContent = "Link copied"; setTimeout(() => { $("copy").textContent = "Copy link to this scenario"; }, 1600); };
|
||||
if (navigator.clipboard) navigator.clipboard.writeText(location.href).then(done, done); else done();
|
||||
});
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user