// isokit/datacentre: multi-storey data centres. // // `dc_building` is the shell: `storeys` of `storey_h` (the project parameter // the house parts use too), cut open by the levels gf, 1f, ... (storeys below // closed, the walls in front cut low, everything above left out). Inside it // every part lives on a storey (its z) and stands on the `raised_floor` under // it: rack rows, pods with cold or hot aisle containment, CRAH units, power // cabinets. Overhead: busways, cable trays, fire pipes (VESDA, sprinklers, // gas nozzles) and detector grids. Plant: chillers, cooling towers, pipe // runs, transformers. Parts on closed storeys inside a dc_building are not // drawn (the shell hides them); the project parameter `ceiling` = false // leaves the overhead services out. // // Parts read the document: racks total their IT load per storey, CRAH units // set the storey's cooling against it (N+1 and so on), gas cylinder banks // size the agent for the halls on their storey, batteries give their runtime // at the building's IT load. fn dc_rack_types() { ["rack_row", "dc_pod"] } /// Leave part o out? Like house parts (part_hidden in isokit/house): above /// the storey a level cuts open, or on a closed storey inside a dc_building. fn dc_skip(o) { part_hidden(o) } /// Overhead services (trays, busways, fire pipes, detectors) draw in their /// own layer after everything standing on the floor, lower ones first: runs /// crossing at different heights over racks would otherwise make depth-sort /// cycles (a tray beside a pod, a busway over the pod and under the tray). fn dc_ceiling_layer() { layer("ceiling", #{ sort: true, zfirst: true }); } /// Overhead services left out (project parameter `ceiling` = false)? fn dc_no_ceiling() { let v = cfg("ceiling"); type_of(v) == "bool" && !v } /// Height of the floor surface at world (wx, wy) above o.z: the top of a /// raised floor there on o's storey, else 0. fn dc_lift_at(o, wx, wy) { let s = storey_of(o.z); let best = 0.0; for f in objects_at(["raised_floor"], wx, wy) { if storey_of(f.z) == s { best = fmax(best, f.z + num(f.p.h) - o.z); } } best } fn dc_lift(o) { dc_lift_at(o, o.x, o.y) } /// Level name of storey j: "GF", "1F", ... fn dc_storey_name(j) { if j == 0 { "GF" } else { `${j}F` } } /// x for a tooltip: whole numbers as they are, others to one decimal. fn dc_num(x) { let r = round1(x); if (r - r.round()).abs() < 1e-9 { `${r.round().to_int()}` } else { `${r}` } } // ---------------------------------------------------------------- loads /// Height of a cabinet of u rack units (42U: 2.0 m). fn dc_rack_h(u) { num(u) * 0.04445 + 0.13 } /// The units of a line of racks along local x, centred on 0: /// [[x0, x1, cooler], ...]. With p.inrow = k > 0 an in-row cooler (0.3 m /// wide) follows every k racks (at the end when there is only one group). fn dc_units(p) { let k = p.inrow; let n = p.n; let seq = []; for i in 0..n { seq.push(false); if k > 0 && (i + 1) % k == 0 && i + 1 < n { seq.push(true); } } if k > 0 && !seq.contains(true) { seq.push(true); } let rw = num(p.rw); let L = 0.0; for q in seq { L += if q { 0.3 } else { rw }; } let out = []; let x = -L / 2.0; for q in seq { let w = if q { 0.3 } else { rw }; out.push([x, x + w, q]); x += w; } out } /// In-row coolers in one line of racks. fn dc_coolers(p) { let c = 0; for q in dc_units(p) { if q[2] { c += 1; } } c } fn dc_racks_of(r) { if r.type == "dc_pod" { 2 * r.p.n } else { r.p.n } } fn dc_kw_of(r) { if r.p.kind == "empty" { 0.0 } else { dc_racks_of(r).to_float() * num(r.p.kw) } } /// IT load of the racks on storey s (every storey when s < 0): /// #{ kw, racks, rows }. fn dc_it(s) { let kw = 0.0; let n = 0; let rows = 0; for r in objects_of(dc_rack_types()) { if s >= 0 && storey_of(r.z) != s { continue; } n += dc_racks_of(r); kw += dc_kw_of(r); rows += 1; } #{ kw: kw, racks: n, rows: rows } } /// Room cooling on storey s: CRAH units on duty and on standby and the /// in-row coolers of its racks: #{ duty, standby, units }. fn dc_cool(s) { let duty = 0.0; let sb = 0.0; let n = 0; for c in objects_of(["crah"]) { if storey_of(c.z) != s { continue; } n += 1; if c.p.standby { sb += num(c.p.kw); } else { duty += num(c.p.kw); } } for r in objects_of(dc_rack_types()) { if storey_of(r.z) != s { continue; } let k = dc_coolers(r.p); if r.type == "dc_pod" { k *= 2; } n += k; duty += k.to_float() * num(r.p.inrow_kw); } #{ duty: duty, standby: sb, units: n } } /// Redundancy of cooling c against kw of load: "N+1", "N", "short by ...". fn dc_redundancy(c, kw) { let total = c.duty + c.standby; if c.units == 0 { return "no cooling units"; } if total + 1e-6 < kw { return `short by ${fmt(kw - total)} kW`; } let per = total / c.units.to_float(); let spare = ((total - kw) / per + 1e-6).floor().to_int(); if spare <= 0 { "N (no spare unit)" } else { `N+${spare}, ${fmt(total - kw)} kW spare` } } /// Plant cooling (chillers and cooling towers) on the site: #{ kw, units }. fn dc_plant() { let kw = 0.0; let n = 0; for c in objects_of(["chiller", "cooling_tower"]) { let k = if c.type == "cooling_tower" { c.p.cells } else { 1 }; n += k; kw += k.to_float() * num(c.p.kw); } #{ kw: kw, units: n } } // ---------------------------------------------------------------- building fn meta_dc_building() { #{ label: "DC building", category: "Data centre", icon: "🏢", shape: "rect", help: "The shell of a multi-storey data centre: storeys of the project's storey height (parameter storey_h), outer walls, slabs and a flat roof with a parapet for the plant. Levels GF, 1F, ... cut it open at that storey; parts inside it on closed storeys are not drawn.", props: [ #{ key: "w", type: "float", def: 40.0, min: 8.0, max: 200.0, step: 0.5, label: "Width (m)" }, #{ key: "d", type: "float", def: 24.0, min: 8.0, max: 200.0, step: 0.5, label: "Depth (m)" }, #{ key: "storeys", type: "int", def: 3, min: 1, max: 12, step: 1, label: "Storeys" }, #{ key: "t", type: "float", def: 0.3, min: 0.15, max: 0.8, step: 0.05, label: "Walls (m)" }, #{ key: "parapet", type: "float", def: 1.2, min: 0.0, max: 4.0, step: 0.1, label: "Parapet (m)" }, #{ key: "mat", type: "material", def: "concb", choices: wall_materials(), label: "Walls" }, #{ key: "roof_mat", type: "material", def: "roof2", choices: roof_materials(), label: "Roof" }, #{ key: "door", type: "bool", def: true, label: "Front door" }, #{ key: "door_at", type: "float", def: 0.25, min: 0.0, max: 1.0, step: 0.01, label: "Door at (0-1)" }, #{ key: "dock", type: "bool", def: true, label: "Loading dock" }, #{ key: "dock_at", type: "float", def: 0.75, min: 0.0, max: 1.0, step: 0.01, label: "Dock at (0-1)" }, #{ key: "louvres", type: "bool", def: true, label: "Louvres" }, ] } } /// The building's sprite (ghosted on levels above its top storey). fn dc_sprite(o, ghost) { if ghost { sprite(o.id, "dim") } else { sprite() } } fn prefab_dc_building(o) { let p = o.p; let H = storey_h(); let n = p.storeys; let w = num(p.w) / 2.0; let d = num(p.d) / 2.0; let t = num(p.t); let k0 = floor_of(o.level); let k = if k0 < 0 { -1 } else { k0 - storey_of(o.z) }; let ghost = k >= n; let closed = k < 0 || ghost; let top = n * H; // the faces: start point, direction and length; walls along x run // through the corners, the others stop at them let faces = [ #{ f: "S", a: [-w, -d], u: [1.0, 0.0], L: 2.0 * w, n: [0.0, -1.0], lo: 0.0 }, #{ f: "E", a: [w, -d], u: [0.0, 1.0], L: 2.0 * d, n: [1.0, 0.0], lo: t }, #{ f: "N", a: [w, d], u: [-1.0, 0.0], L: 2.0 * w, n: [0.0, 1.0], lo: 0.0 }, #{ f: "W", a: [-w, d], u: [0.0, -1.0], L: 2.0 * d, n: [-1.0, 0.0], lo: t }, ]; for F in faces { let zt = if closed { top + num(p.parapet) } else if vis(F.n[0], F.n[1]) { k * H + 1.2 } else { (k + 1) * H }; let cap = if closed { p.mat } else { "cut" }; let ops = dc_openings(p, F, H, n, top); let lo = F.lo; let hi = F.L - F.lo; let m = ((hi - lo) / 15.0).ceiling().to_int(); for i in 0..m { let ua = lo + (hi - lo) * i / m; let ub = lo + (hi - lo) * (i + 1) / m; let pa = [F.a[0] + F.u[0] * ua, F.a[1] + F.u[1] * ua]; let pb = [F.a[0] + F.u[0] * ub, F.a[1] + F.u[1] * ub]; // the wall piece behind the face pa -> pb, t thick inwards let ix = -F.n[0] * t; let iy = -F.n[1] * t; let x0 = fmin(fmin(pa[0], pb[0]), fmin(pa[0], pb[0]) + ix); let x1 = fmax(fmax(pa[0], pb[0]), fmax(pa[0], pb[0]) + ix); let y0 = fmin(fmin(pa[1], pb[1]), fmin(pa[1], pb[1]) + iy); let y1 = fmax(fmax(pa[1], pb[1]), fmax(pa[1], pb[1]) + iy); let s = dc_sprite(o, ghost); s.box(x0, y0, x1, y1, 0.0, zt, p.mat, #{ top: cap }); for c in ops.keys() { let list = []; for q in ops[c] { let u0 = fmax(q[0], ua); let u1 = fmin(q[1], ub); let z1 = fmin(q[3], zt); if u1 > u0 + 0.01 && z1 > q[2] + 0.01 { list.push([u0 - ua, u1 - ua, q[2], z1]); } } if list.len() > 0 { s.open(pa, pb, list, c); } } } } let ix0 = -w + t; let ix1 = w - t; let iy0 = -d + t; let iy1 = d - t; if closed { dc_sprite(o, ghost).box(ix0, iy0, ix1, iy1, top - 0.3, top, "plinth", #{ top: p.roof_mat }); } else if k == 0 { layer("floor"); sprite().flat(rect(ix0, iy0, ix1, iy1), 0.005, "z-floor t"); layer("main"); } else { sprite().box(ix0, iy0, ix1, iy1, k * H - 0.3, k * H, "plinth", #{ top: "z-floor" }); } info_row("Storeys", `${n} × ${dc_num(H)} m, ${fmt(num(p.w) * num(p.d) * n)} m² gross`); let s0 = storey_of(o.z); for j in 0..n { let it = dc_it(s0 + j); if it.racks > 0 { info_row(dc_storey_name(j), `${it.racks} racks, ${fmt(it.kw)} kW IT`); } } let all = dc_it(-1); let pl = dc_plant(); if all.racks > 0 { info_row("IT load", `${fmt(all.kw)} kW in ${all.racks} racks`); } if pl.units > 0 { info_row("Plant cooling", `${fmt(pl.kw)} kW in ${pl.units} chiller / tower units`); } } /// Openings on face F of a dc_building, in face coordinates (u from the /// face's start, z from the ground), by class: slab edges, louvres on the /// side faces, the front door and the loading dock on the front (-y) face /// (door_at, dock_at: how far along it, 0 to 1). fn dc_openings(p, F, H, n, top) { let out = #{}; let bands = []; for j in 1..n { bands.push([0.0, F.L, j * H - 0.3, j * H]); } bands.push([0.0, F.L, top - 0.3, top]); out["dcband"] = bands; if p.louvres && (F.f == "E" || F.f == "W") { let lv = []; let m = ((F.L - 4.0) / 7.0).floor().to_int(); for j in 0..n { for i in 0..m { let u = (F.L - (m - 1) * 7.0) / 2.0 + i * 7.0; lv.push([u - 1.6, u + 1.6, j * H + 1.4, j * H + 3.4]); } } if lv.len() > 0 { out["louvre"] = lv; } } if F.f == "S" { // along the front from its -x end, kept off the corners if p.door { let u = clamp(F.L * num(p.door_at), 1.2, F.L - 1.2); out["door"] = [[u - 0.9, u + 0.9, 0.0, 2.4]]; } if p.dock { let u = clamp(F.L * num(p.dock_at), 2.4, F.L - 2.4); out["door2"] = [[u - 2.0, u + 2.0, 0.0, 4.2]]; } } out } // ---------------------------------------------------------------- raised floor fn meta_raised_floor() { #{ label: "Raised floor", category: "Data centre", icon: "▦", shape: "rect", storey: true, help: "An access floor on pedestals (0.6 m tiles) whose void is the supply plenum. Racks, pods, CRAH units and cabinets on it stand on its top. Fit it inside the walls; walls take it for a room.", props: [ #{ key: "w", type: "float", def: 12.0, min: 1.2, max: 200.0, step: 0.6, label: "Width (m)" }, #{ key: "d", type: "float", def: 8.4, min: 1.2, max: 200.0, step: 0.6, label: "Depth (m)" }, #{ key: "h", type: "float", def: 0.6, min: 0.15, max: 1.5, step: 0.05, label: "Void (m)" }, #{ key: "mat", type: "material", def: "dcrf", choices: ["dcrf", "dcrf2", "dcrf3"], label: "Tiles" }, #{ key: "grid", type: "bool", def: true, label: "Tile joints" }, ] } } fn prefab_raised_floor(o) { if dc_skip(o) { return; } let p = o.p; let w = num(p.w) / 2.0; let d = num(p.d) / 2.0; let h = num(p.h); let s = sprite(); s.box(-w, -d, w, d, 0.0, h, "dcrfs", #{ top: p.mat }); // the void on the sides: dark, with the pedestals every tile let sd = sides(-w, -d, w, d); for f in ["S", "E", "N", "W"] { let L = if f == "S" || f == "N" { 2.0 * w } else { 2.0 * d }; let legs = []; let u = 0.3; while u < L - 0.1 { legs.push([u - 0.03, u + 0.03, 0.0, h - 0.04]); u += 0.6; } s.open(sd[f][0], sd[f][1], [[0.0, L, 0.0, h - 0.04]], "dcvoid"); if legs.len() > 0 && legs.len() < 400 { s.open(sd[f][0], sd[f][1], legs, "dcleg"); } } if p.grid { let segs = []; let x = -w + 0.6; while x < w - 0.05 { segs.push([x, -d, x, d]); x += 0.6; } let y = -d + 0.6; while y < d - 0.05 { segs.push([-w, y, w, y]); y += 0.6; } if segs.len() < 700 { s.lines(segs, h, "dctile"); } } // what stands on it let racks = 0; let kw = 0.0; for r in objects_of(dc_rack_types()) { if storey_of(r.z) != storey_of(o.z) { continue; } let l = local_of(o, r.x, r.y); if l[0].abs() <= w && l[1].abs() <= d { racks += dc_racks_of(r); kw += dc_kw_of(r); } } let area = num(p.w) * num(p.d); info_row("Area", `${fmt(area)} m², ${fmt(area / 0.36)} tiles of 0.6 m`); info_row("Void", `${dc_num(h)} m (supply air plenum)`); if racks > 0 { info_row("On it", `${racks} racks, ${fmt(kw)} kW IT`); info_row("Density", `${dc_num(kw / area)} kW/m² over the floor, ${dc_num(kw / racks.to_float())} kW per rack`); } } // ---------------------------------------------------------------- racks fn dc_rack_kinds() { ["servers", "storage", "GPU", "network", "empty"] } fn dc_rack_props() { [ #{ key: "n", type: "int", def: 10, min: 1, max: 40, step: 1, label: "Racks" }, #{ key: "kind", type: "choice", def: "servers", choices: dc_rack_kinds(), label: "Equipment" }, #{ key: "kw", type: "float", def: 8.0, min: 0.0, max: 150.0, step: 0.5, label: "kW per rack" }, #{ key: "fill", type: "int", def: 70, min: 0, max: 100, step: 5, label: "Filled (%)" }, #{ key: "u", type: "int", def: 42, min: 20, max: 52, step: 1, label: "Height (U)" }, #{ key: "rw", type: "float", def: 0.6, min: 0.6, max: 1.0, step: 0.1, label: "Rack width (m)" }, #{ key: "rd", type: "float", def: 1.2, min: 0.6, max: 1.4, step: 0.05, label: "Rack depth (m)" }, #{ key: "doors", type: "choice", def: "mesh", choices: ["mesh", "glass", "solid", "none"], label: "Doors" }, #{ key: "inrow", type: "int", def: 0, min: 0, max: 20, step: 1, label: "Cooler every" }, #{ key: "inrow_kw", type: "float", def: 30.0, min: 5.0, max: 150.0, step: 5.0, label: "Cooler kW" }, #{ key: "leds", type: "bool", def: true, label: "Status LEDs" }, ] } /// A face along local x at y with outward normal ny (-1 or 1), from -B to /// B: #{ a, b, k } with u(x) = k * x + B along it. fn dc_xface(B, y, ny) { if ny < 0.0 { #{ a: [-B, y], b: [B, y], k: 1.0 } } else { #{ a: [B, y], b: [-B, y], k: -1.0 } } } /// [u0, u1, z0, z1] on face F (dc_xface) for x0..x1. fn dc_span(F, B, x0, x1, z0, z1) { let u0 = F.k * x0 + B; let u1 = F.k * x1 + B; [fmin(u0, u1), fmax(u0, u1), z0, z1] } /// A line of racks (and in-row coolers) along local x, centred at y = yc, /// fronts towards f (-1: local -y, 1: +y), standing at z0, drawn into s. /// key seeds which slots are filled and the LED timing. fn dc_rack_line(s, p, yc, f, z0, key) { let units = dc_units(p); let h = dc_rack_h(p.u); let zt = z0 + h; let rd = num(p.rd) / 2.0; let y0 = yc - rd; let y1 = yc + rd; let items = []; for q in units { let a = q[0]; let b = q[1]; let m = if q[2] { "dcinrow" } else { "dccab" }; let t = if q[2] { "dcinrow" } else { "dccabt" }; items.push([(a + b) / 2.0, yc, |c| c.box(a, y0, b, y1, z0, zt, m, #{ top: t })]); } s.seq(items); let B = units[units.len() - 1][1]; let F = dc_xface(B, yc + f * rd, f); let R = dc_xface(B, yc - f * rd, -f); let kind = p.kind; let bays = []; let gear = []; let gear2 = []; let strips = []; let rear = []; let pdus = []; let grilles = []; let rgrilles = []; let screens = []; let handles = []; let leds = []; let zb = z0 + 0.1; let zh = zt - 0.09; let i = 0; for q in units { let a = q[0]; let b = q[1]; if q[2] { grilles.push(dc_span(F, B, a + 0.03, b - 0.03, z0 + 0.12, zt - 0.32)); screens.push(dc_span(F, B, a + 0.07, b - 0.07, zt - 0.26, zt - 0.14)); rgrilles.push(dc_span(R, B, a + 0.03, b - 0.03, z0 + 0.12, zt - 0.12)); continue; } let ia = a + 0.045; let ib = b - 0.045; bays.push(dc_span(F, B, ia, ib, zb, zh)); rear.push(dc_span(R, B, ia, ib, zb, zh)); pdus.push(dc_span(R, B, ia + 0.03, ia + 0.07, zb + 0.15, zh - 0.15)); pdus.push(dc_span(R, B, ib - 0.07, ib - 0.03, zb + 0.15, zh - 0.15)); // equipment from the bottom (network: from the top), about `fill` of the slots let slot = if kind == "storage" { 0.178 } else if kind == "GPU" { 0.267 } else if kind == "network" { 0.0445 } else { 0.0889 }; let ns = ((zh - zb - 0.04) / slot).floor().to_int(); let r = hash(key * 17.3 + i * 3.71); let used = if kind == "empty" { 0 } else { clamp((ns * p.fill / 100.0 + (r - 0.5) * 3.0).round(), 0.0, ns).to_int() }; for j in 0..used { if kind == "network" { let zz = zh - 0.02 - (j + 1) * slot; if j % 3 == 2 { continue; } let row = dc_span(F, B, ia + 0.03, ib - 0.03, zz + 0.004, zz + slot - 0.004); if j % 3 == 0 { gear2.push(row); } else { gear.push(row); } } else { // a gap now and then if hash(key * 5.1 + i * 2.3 + j * 0.77) < 0.08 { continue; } let za = zb + 0.02 + j * slot; gear.push(dc_span(F, B, ia + 0.02, ib - 0.02, za + 0.004, za + slot - 0.004)); if kind == "GPU" { strips.push(dc_span(F, B, ia + 0.06, ib - 0.06, za + slot - 0.05, za + slot - 0.025)); } if kind == "storage" { gear2.push(dc_span(F, B, ia + 0.05, ib - 0.05, za + slot * 0.45, za + slot * 0.55)); } } } if p.doors == "solid" { handles.push(dc_span(F, B, ib - 0.07, ib - 0.05, z0 + 0.9, z0 + 1.3)); } leds.push([(a + b) / 2.0, i]); i += 1; } let gc = if kind == "storage" { "dcstor" } else if kind == "GPU" { "dcgpu" } else if kind == "network" { "dcnet" } else { "dcsrv" }; let g2 = if kind == "network" { "dcpatch" } else { "dcslot" }; if bays.len() > 0 { s.open(F.a, F.b, bays, if p.doors == "solid" { "dcdoor" } else { "dcbay" }); if p.doors != "solid" { if gear.len() > 0 { s.open(F.a, F.b, gear, gc); } if gear2.len() > 0 { s.open(F.a, F.b, gear2, g2); } if strips.len() > 0 { s.open(F.a, F.b, strips, "dcstrip"); } if p.doors == "mesh" { s.open(F.a, F.b, bays, "dcmesh"); } if p.doors == "glass" { s.open(F.a, F.b, bays, "dcglassd"); } } if handles.len() > 0 { s.open(F.a, F.b, handles, "metal"); } s.open(R.a, R.b, rear, "dcrear"); s.open(R.a, R.b, pdus, "dcpdu0"); } if grilles.len() > 0 { s.open(F.a, F.b, grilles, "grille"); s.open(F.a, F.b, screens, "dcscr"); s.open(R.a, R.b, rgrilles, "grille"); } if p.leds && vis(0.0, f) && kind != "empty" { let yl = yc + f * (rd + 0.01); for l in leds { let x = l[0]; s.begin("a dcblink", `animation-delay:-${round1(hash(key * 7.7 + l[1] * 1.37) * 2.6)}s`); s.circle(x - 0.12, yl, zt - 0.05, 0.3, "ledg"); s.end(); s.circle(x + 0.12, yl, zt - 0.05, 0.3, if hash(key + l[1] * 9.1) < 0.12 { "ledo" } else { "ledg" }); } } #{ B: B, h: h } } /// Vent tiles (flat at the floor top z0) between y = ya and yb in front of /// the racks of a line, as rectangles for one flat. fn dc_vents(p, ya, yb) { let out = []; for q in dc_units(p) { if !q[2] { out.push(rect(q[0] + 0.02, ya + 0.02, q[1] - 0.02, yb - 0.02)); } } out } /// Tooltip rows shared by rack rows and pods. fn dc_rack_info(o, lines) { let p = o.p; let n = lines * p.n; let kw = if p.kind == "empty" { 0.0 } else { n.to_float() * num(p.kw) }; let side = if lines > 1 { `${lines} × ${p.n}` } else { `${p.n}` }; info_row("Racks", `${side} × ${p.u}U ${p.kind}, ${dc_num(num(p.rw) * 1000.0)} × ${dc_num(num(p.rd) * 1000.0)} mm`); if p.kind != "empty" { info_row("IT load", `${dc_num(num(p.kw))} kW per rack, ${fmt(kw)} kW in all`); info_row("Filled", `about ${p.fill} % of ${n * p.u} U`); } let c = dc_coolers(p) * lines; if c > 0 { info_row("In-row cooling", `${c} × ${fmt(num(p.inrow_kw))} kW = ${fmt(c.to_float() * num(p.inrow_kw))} kW`); } } fn meta_rack_row() { #{ label: "Rack row", category: "Data centre", icon: "▥", storey: true, help: "A row of cabinets along local x, fronts (the cold side) to local -y. It stands on the raised floor under it, with vent tiles in front; in-row coolers between groups of racks (Cooler every).", props: dc_rack_props() + [ #{ key: "vents", type: "bool", def: true, label: "Vent tiles" } ] } } fn prefab_rack_row(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let r = dc_rack_line(sprite(), p, 0.0, -1.0, z0, hash(o.x * 3.1 + o.y * 7.7 + o.z)); if p.vents && z0 > 0.0 && p.kind != "empty" { let rd = num(p.rd) / 2.0; sprite().flat(dc_vents(p, -rd - 0.6, -rd), z0, "dcperf"); } dc_rack_info(o, 1); } fn meta_dc_pod() { #{ label: "Rack pod", category: "Data centre", icon: "▤", storey: true, help: "Two rows of racks along local x around a shared aisle. Cold aisle containment: the fronts face the aisle, which gets vent tiles, a roof and sliding doors at both ends; hot aisle: the backs face it (optionally with a duct to the ceiling). Stands on the raised floor under it.", props: dc_rack_props() + [ #{ key: "contain", type: "choice", def: "cold", choices: ["cold", "hot", "none"], label: "Containment" }, #{ key: "aisle", type: "float", def: 1.2, min: 0.9, max: 3.0, step: 0.3, label: "Aisle (m)" }, #{ key: "roof", type: "bool", def: true, label: "Aisle roof" }, #{ key: "ends", type: "bool", def: true, label: "End doors" }, #{ key: "duct", type: "bool", def: false, label: "Duct to ceiling" }, #{ key: "duct_h", type: "float", def: 4.4, min: 3.0, max: 12.0, step: 0.1, label: "Ceiling (m)" }, #{ key: "air", type: "bool", def: true, label: "Airflow" }, ] } } fn prefab_dc_pod(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let a = num(p.aisle) / 2.0; let rd = num(p.rd) / 2.0; let hot = p.contain == "hot"; let cont = p.contain != "none"; let key = hash(o.x * 3.1 + o.y * 7.7 + o.z); // cold aisle: fronts face the aisle; hot aisle: backs do let fa = if hot { -1.0 } else { 1.0 }; let r = dc_rack_line(sprite(), p, -a - rd, fa, z0, key); dc_rack_line(sprite(), p, a + rd, -fa, z0, key + 0.37); let B = r.B; let zt = z0 + r.h; let units = dc_units(p); let s = sprite(); let ends = if !cont || !p.ends { [] } else if vis(1.0, 0.0) { [-B, B] } else { [B, -B] }; if ends.len() > 0 { dc_aisle_door(s, ends[0], a, z0, zt, hot); } if !hot && z0 > 0.0 && p.kind != "empty" { s.flat(dc_vents(p, -a, a), z0, "dcperf"); } if p.air && p.kind != "empty" { let i = 0; for q in units { if q[2] { continue; } if i % 2 == 0 { let x = (q[0] + q[1]) / 2.0; if hot { s.puffs(x, 0.0, zt - 0.5, "dcheat", 2, 3.4); } else if z0 > 0.0 { s.puffs(x, 0.0, z0 + 0.15, "dcair", 2, 3.0); } } i += 1; } } if ends.len() > 0 { dc_aisle_door(s, ends[1], a, z0, zt, hot); } if cont && p.roof { s.box(-B, -a, B, a, zt, zt + 0.04, if hot { "dcglassh" } else { "dcglass" }); let segs = [[-B, -a, B, -a], [-B, a, B, a]]; for q in units { segs.push([q[1], -a, q[1], a]); } segs.push([-B, -a, -B, a]); s.lines(segs, zt + 0.04, "dcframe"); } if hot && p.duct && num(p.duct_h) > r.h + 0.3 { s.box(-B + 0.2, -a + 0.05, B - 0.2, a - 0.05, zt + 0.04, num(p.duct_h), "dcalu"); } s.ext_box(-B, -a, B, a, z0, z0); dc_rack_info(o, 2); let what = if p.contain == "cold" { "cold aisle" } else if hot { "hot aisle" } else { "none (open aisles)" }; let parts = []; if cont && p.roof { parts.push("roof"); } if cont && p.ends { parts.push("end doors"); } if hot && p.duct { parts.push("ceiling duct"); } let extra = ""; for x in parts { extra += if extra == "" { ", " + x } else { " and " + x }; } info_row("Containment", `${what}, ${dc_num(num(p.aisle))} m${extra}`); } /// The sliding doors closing a contained aisle at x (an end of it). fn dc_aisle_door(s, x, a, z0, zt, hot) { let x0 = if x > 0.0 { x - 0.05 } else { x }; let x1 = x0 + 0.05; let g = if hot { "dcglassh" } else { "dcglass" }; s.box(x0, -a, x1, -a + 0.05, z0, zt, "dcalu"); s.box(x0, a - 0.05, x1, a, z0, zt, "dcalu"); s.box(x0, -a + 0.05, x1, a - 0.05, zt - 0.1, zt, "dcalu"); s.box(x0 + 0.01, -a + 0.05, x1 - 0.01, a - 0.05, z0, zt - 0.1, g); // the meeting stiles, on whichever side faces the viewer s.open([x1, -a], [x1, a], [[a - 0.02, a + 0.02, z0, zt - 0.1]], "dcalu"); s.open([x0, a], [x0, -a], [[a - 0.02, a + 0.02, z0, zt - 0.1]], "dcalu"); s } fn meta_dc_cage() { #{ label: "Cage", category: "Data centre", icon: "▢", shape: "rect", storey: true, help: "A colocation cage: mesh panels on posts round the rectangle, a door on the local -y side. Leave room between the mesh and the racks inside.", props: [ #{ key: "w", type: "float", def: 6.0, min: 1.2, max: 60.0, step: 0.3, label: "Width (m)" }, #{ key: "d", type: "float", def: 4.8, min: 1.2, max: 60.0, step: 0.3, label: "Depth (m)" }, #{ key: "h", type: "float", def: 2.4, min: 1.0, max: 4.0, step: 0.1, label: "Height (m)" }, #{ key: "door", type: "bool", def: true, label: "Door" }, ] } } fn prefab_dc_cage(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let w = num(p.w) / 2.0; let d = num(p.d) / 2.0; let h = num(p.h); let t = 0.05; // one sprite per side (the ones along x run through the corners) let walls = [ [-w, -d, w, -d + t, "x"], [-w, d - t, w, d, "x"], [-w, -d + t, -w + t, d - t, "y"], [w - t, -d + t, w, d - t, "y"], ]; let k = 0; for q in walls { let s = sprite(); let alongx = q[4] == "x"; let L = if alongx { q[2] - q[0] } else { q[3] - q[1] }; let gap = if k == 0 && p.door && L > 2.0 { [-0.5, 0.5] } else { [] }; // mesh panels between posts every 1.2 m (and either side of the door) let cuts = [0.0]; let m = (L / 1.2).ceiling().to_int(); for i in 1..m { cuts.push(L * i / m); } cuts.push(L); let posts = []; for c in cuts { posts.push(c); } if gap.len() > 0 { posts.push(L / 2.0 + gap[0]); posts.push(L / 2.0 + gap[1]); } let items = []; for c in posts { let px = if alongx { q[0] + clamp(c, 0.03, L - 0.03) } else { (q[0] + q[2]) / 2.0 }; let py = if alongx { (q[1] + q[3]) / 2.0 } else { q[1] + clamp(c, 0.03, L - 0.03) }; items.push([px, py, |r| r.box(px - 0.03, py - 0.03, px + 0.03, py + 0.03, z0, z0 + h, "steel")]); } let panels = []; for i in 0..(cuts.len() - 1) { let u0 = cuts[i] + 0.03; let u1 = cuts[i + 1] - 0.03; if gap.len() > 0 && u1 > L / 2.0 + gap[0] && u0 < L / 2.0 + gap[1] { // the door leaf (closed) in the gap, panels either side of it if L / 2.0 + gap[0] - 0.03 > u0 + 0.05 { panels.push([u0, L / 2.0 + gap[0] - 0.03]); } if u1 > L / 2.0 + gap[1] + 0.08 { panels.push([L / 2.0 + gap[1] + 0.03, u1]); } continue; } panels.push([u0, u1]); } for u in panels { let a = u[0]; let b = u[1]; let cx = if alongx { q[0] + (a + b) / 2.0 } else { (q[0] + q[2]) / 2.0 }; let cy = if alongx { (q[1] + q[3]) / 2.0 } else { q[1] + (a + b) / 2.0 }; items.push([cx, cy, |r| if alongx { r.box(q[0] + a, q[1] + 0.01, q[0] + b, q[3] - 0.01, z0 + 0.05, z0 + h - 0.05, "dcmeshp") } else { r.box(q[0] + 0.01, q[1] + a, q[2] - 0.01, q[1] + b, z0 + 0.05, z0 + h - 0.05, "dcmeshp") }]); } if gap.len() > 0 { let a = L / 2.0 + gap[0] + 0.03; let b = L / 2.0 + gap[1] - 0.03; items.push([q[0] + (a + b) / 2.0, (q[1] + q[3]) / 2.0, |r| r.box(q[0] + a, q[1] + 0.01, q[0] + b, q[3] - 0.01, z0 + 0.02, z0 + 2.05, "dcmeshd")]); } s.seq(items); k += 1; } let racks = 0; for r in objects_of(dc_rack_types()) { if storey_of(r.z) != storey_of(o.z) { continue; } let l = local_of(o, r.x, r.y); if l[0].abs() <= w && l[1].abs() <= d { racks += dc_racks_of(r); } } info_row("Cage", `${dc_num(num(p.w))} × ${dc_num(num(p.d))} m, ${dc_num(h)} m mesh`); if racks > 0 { info_row("Inside", `${racks} racks`); } } // ---------------------------------------------------------------- overhead fn meta_cable_tray() { #{ label: "Cable tray", category: "Data centre", icon: "☰", shape: "line", storey: true, help: "An overhead ladder tray (power), fibre duct (yellow) or wire basket (copper) along local x, hung above the racks. Height above the storey's slab.", props: [ #{ key: "w", type: "float", def: 10.0, min: 0.5, max: 60.0, step: 0.1, label: "Length (m)" }, #{ key: "kind", type: "choice", def: "ladder", choices: ["ladder", "fibre", "basket"], label: "Kind" }, #{ key: "width", type: "float", def: 0.45, min: 0.1, max: 1.2, step: 0.05, label: "Width (m)" }, #{ key: "h", type: "float", def: 3.6, min: 2.0, max: 8.0, step: 0.05, label: "Height (m)" }, #{ key: "fill", type: "int", def: 50, min: 0, max: 100, step: 10, label: "Cables (%)" }, ] } } fn prefab_cable_tray(o) { if dc_skip(o) || dc_no_ceiling() { return; } let p = o.p; let w = num(p.w) / 2.0; let b = num(p.width) / 2.0; let h = num(p.h); let m = if p.kind == "fibre" { "dcfibre" } else if p.kind == "basket" { "dcbasket" } else { "dctray" }; let side = if p.kind == "fibre" { 0.1 } else { 0.08 }; let cab = if p.kind == "fibre" { "dcfibrec" } else if p.kind == "basket" { "dccablec" } else { "dccable" }; dc_ceiling_layer(); let near = if vis(0.0, 1.0) { 1.0 } else { -1.0 }; // the rails: the far one is drawn first, the near one last let fy0 = if near > 0.0 { -b } else { b - 0.02 }; let ny0 = if near > 0.0 { b - 0.02 } else { -b }; let pieces = (num(p.w) / 15.0).ceiling().to_int(); for i in 0..pieces { let x0 = -w + num(p.w) * i / pieces; let x1 = -w + num(p.w) * (i + 1) / pieces; let s = sprite(); // hangers first (behind), then the far rail, the bed, cables, the near rail let x = x0 + 0.4; while x < x1 - 0.1 { s.stem(x, -near * (b - 0.02), h + side, h + side + 0.2, 0.1, "metal"); x += 1.5; } s.box(x0, fy0, x1, fy0 + 0.02, h, h + side, m); if p.kind == "ladder" { let rungs = []; let x = x0 + 0.15; while x < x1 { rungs.push([x - 0.02, -b + 0.02, x + 0.02, b - 0.02]); x += 0.3; } s.flat(rungs, h + 0.005, m + " t"); } else { s.box(x0, -b + 0.02, x1, b - 0.02, h, h + 0.015, m); } if p.fill > 0 { let ch = 0.01 + (side - 0.02) * p.fill / 100.0; s.box(x0, -b + 0.03, x1, b - 0.03, h + 0.015, h + 0.015 + ch, cab); } s.box(x0, ny0, x1, ny0 + 0.02, h, h + side, m); // sort by the tray alone: its hangers would reach into the runs above s.ext_reset(); s.ext_box(x0, -b, x1, b, h, h + side); } let what = if p.kind == "fibre" { "fibre duct" } else if p.kind == "basket" { "wire basket (copper)" } else { "ladder tray (power)" }; info_row("Tray", `${what}, ${dc_num(num(p.width) * 1000.0)} mm wide, ${dc_num(num(p.w))} m`); info_row("Height", `${dc_num(h)} m above the slab, cables about ${p.fill} % full`); } fn meta_busway() { #{ label: "Busway", category: "Power", icon: "≡", shape: "line", storey: true, help: "Overhead busbar trunking along local x, hung over a rack row, with a tap-off box for each rack. Height above the storey's slab.", props: [ #{ key: "w", type: "float", def: 8.0, min: 1.0, max: 60.0, step: 0.1, label: "Length (m)" }, #{ key: "h", type: "float", def: 3.2, min: 2.0, max: 8.0, step: 0.05, label: "Height (m)" }, #{ key: "amps", type: "int", def: 400, min: 100, max: 1600, step: 50, label: "Rating (A)" }, #{ key: "taps", type: "float", def: 0.6, min: 0.0, max: 5.0, step: 0.1, label: "Tap-off every (m)" }, ] } } fn prefab_busway(o) { if dc_skip(o) || dc_no_ceiling() { return; } let p = o.p; let w = num(p.w) / 2.0; let h = num(p.h); let tp = num(p.taps); dc_ceiling_layer(); let pieces = (num(p.w) / 15.0).ceiling().to_int(); let taps = 0; for i in 0..pieces { let x0 = -w + num(p.w) * i / pieces; let x1 = -w + num(p.w) * (i + 1) / pieces; let s = sprite(); let x = x0 + 0.5; while x < x1 - 0.1 { s.stem(x, 0.0, h + 0.15, h + 0.35, 0.1, "metal"); x += 2.0; } if tp > 0.05 { let items = []; let x = -w + tp / 2.0; while x < w { if x >= x0 && x < x1 { let c = x; items.push([c, 0.0, |q| q.box(c - 0.11, -0.1, c + 0.11, 0.1, h - 0.26, h, "dctap")]); taps += 1; } x += tp; } s.seq(items); } s.box(x0, -0.08, x1, 0.08, h, h + 0.15, "dcbus"); s.ext_reset(); s.ext_box(x0, -0.11, x1, 0.11, h - 0.26, h + 0.15); } info_row("Busway", `${fmt(p.amps)} A, about ${fmt(1.732 * 0.415 * p.amps * 0.95)} kW at 415 V`); info_row("Length", `${dc_num(num(p.w))} m, ${taps} tap-off boxes, ${dc_num(h)} m above the slab`); } // ---------------------------------------------------------------- cooling fn meta_crah() { #{ label: "CRAH unit", category: "Cooling", icon: "❄", storey: true, help: "A computer room air handler (chilled water) or CRAC (DX) unit, front to local -y, blowing down into the raised floor. Its tooltip sets the storey's cooling (units on duty, standby, in-row coolers) against the IT load of its racks.", props: [ #{ key: "kind", type: "choice", def: "CRAH", choices: ["CRAH", "CRAC"], label: "Type" }, #{ key: "kw", type: "float", def: 120.0, min: 10.0, max: 600.0, step: 5.0, label: "Cooling (kW)" }, #{ key: "w", type: "float", def: 2.5, min: 0.8, max: 5.0, step: 0.1, label: "Width (m)" }, #{ key: "standby", type: "bool", def: false, label: "Standby" }, #{ key: "running", type: "bool", def: true, label: "Fans running" }, ] } } fn prefab_crah(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let W = num(p.w); let w = W / 2.0; let d = 0.45; let h = 1.95; let s = sprite(); s.box(-w, -d, w, d, z0, z0 + h, "dccrah"); let sd = sides(-w, -d, w, d); let nd = fmax(1.0, (W / 0.85).round()).to_int(); let doors = []; for i in 0..nd { doors.push([i * W / nd + 0.03, (i + 1) * W / nd - 0.03, z0 + 0.06, z0 + h - 0.06]); } s.open(sd.S[0], sd.S[1], doors, "seam"); s.open(sd.S[0], sd.S[1], [[0.2, 0.62, z0 + h - 0.42, z0 + h - 0.2]], "dcscr"); s.open(sd.N[0], sd.N[1], [[0.1, W - 0.1, z0 + h - 0.5, z0 + h - 0.1]], "grille"); // return air grille on top, the fans under it s.flat(rect(-w + 0.08, -d + 0.08, w - 0.08, d - 0.08), z0 + h, "grille"); let nf = fmax(1.0, (W / 0.8).floor()).to_int(); let spin = p.running && !p.standby; for i in 0..nf { let x = -w + (i + 0.5) * W / nf; if spin { s.fan(x, 0.0, z0 + h + 0.005, 0.28); } else { s.disc(x, 0.0, z0 + h + 0.005, 0.28, "fanring"); } } if vis(0.0, -1.0) { s.circle(-w + 0.75, -d - 0.01, z0 + h - 0.3, 0.35, if p.standby { "ledo" } else if p.running { "ledg" } else { "ledr" }); } let st = storey_of(o.z); let it = dc_it(st); let c = dc_cool(st); info_row("Type", if p.kind == "CRAC" { "CRAC (DX refrigerant)" } else { "CRAH (chilled water)" }); info_row("Cooling", `${fmt(num(p.kw))} kW, ${if p.standby { "standby" } else if p.running { "on duty" } else { "stopped" }}`); info_row("This storey", `${fmt(it.kw)} kW IT in ${it.racks} racks; ${fmt(c.duty)} kW duty + ${fmt(c.standby)} kW standby cooling`); info_row("Redundancy", dc_redundancy(c, it.kw)); } fn meta_chiller() { #{ label: "Chiller", category: "Cooling", icon: "⛆", storey: true, help: "An air-cooled chiller, dry cooler or adiabatic cooler along local x with two rows of fans on top, on the roof or in the yard. Its tooltip sets all the chillers and towers against the IT load of every storey.", props: [ #{ key: "kind", type: "choice", def: "chiller", choices: ["chiller", "dry cooler", "adiabatic"], label: "Type" }, #{ key: "w", type: "float", def: 9.0, min: 3.0, max: 20.0, step: 0.5, label: "Length (m)" }, #{ key: "kw", type: "float", def: 1000.0, min: 50.0, max: 5000.0, step: 50.0, label: "Cooling (kW)" }, #{ key: "running", type: "bool", def: true, label: "Fans running" }, ] } } fn prefab_chiller(o) { if dc_skip(o) { return; } let p = o.p; let W = num(p.w); let w = W / 2.0; let d = 1.15; let h = 2.4; let s = sprite(); s.box(-w, -d, w, d, 0.0, 0.15, "steel"); s.box(-w, -d + 0.08, w, d - 0.08, 0.15, h, "chiller"); let n = fmax(1.0, (W / 1.2).floor()).to_int(); let coils = []; for i in 0..n { coils.push([i * W / n + 0.06, (i + 1) * W / n - 0.06, 0.45, h - 0.15]); } let sd = sides(-w, -d + 0.08, w, d - 0.08); let cc = if p.kind == "adiabatic" { "dcpad" } else { "dccoil" }; s.open(sd.S[0], sd.S[1], coils, cc); s.open(sd.N[0], sd.N[1], coils, cc); s.open(sd.W[0], sd.W[1], [[0.25, 1.1, 0.5, 1.9]], "panel"); s.open(sd.E[0], sd.E[1], [[0.3, 2.0 * d - 0.46, 0.4, h - 0.2]], "grille"); for i in 0..n { let x = -w + (i + 0.5) * W / n; for y in [-0.52, 0.52] { if p.running { s.fan(x, y, h + 0.005, 0.42); } else { s.disc(x, y, h + 0.005, 0.42, "fanring"); } } } let all = dc_it(-1); let pl = dc_plant(); info_row("Type", if p.kind == "dry cooler" { "dry cooler (free cooling)" } else if p.kind == "adiabatic" { "adiabatic cooler (wetted pads)" } else { "air-cooled chiller" }); info_row("Cooling", `${fmt(num(p.kw))} kW, ${2 * n} fans${if p.running { "" } else { " (stopped)" }}`); info_row("Plant", `${pl.units} units, ${fmt(pl.kw)} kW for ${fmt(all.kw)} kW IT (${dc_redundancy(#{ duty: pl.kw, standby: 0.0, units: pl.units }, all.kw)})`); } fn meta_cooling_tower() { #{ label: "Cooling tower", category: "Cooling", icon: "♨", storey: true, help: "Open evaporative cooling tower cells along local x (4 m each) with a fan stack on each and a plume when running.", props: [ #{ key: "cells", type: "int", def: 2, min: 1, max: 6, step: 1, label: "Cells" }, #{ key: "kw", type: "float", def: 2000.0, min: 100.0, max: 10000.0, step: 100.0, label: "kW per cell" }, #{ key: "running", type: "bool", def: true, label: "Running (plume)" }, ] } } fn prefab_cooling_tower(o) { if dc_skip(o) { return; } let p = o.p; let n = p.cells; let c = 4.0; let h = 3.6; let x0 = -n * c / 2.0; let s = sprite(); let items = []; for i in 0..n { let a = x0 + i * c; let b = a + c; let x = (a + b) / 2.0; items.push([x, 0.0, |q| { q.box(a + 0.02, -c / 2.0, b - 0.02, c / 2.0, 0.0, h, "dctower", #{ top: "dctowert" }); let sd = sides(a + 0.02, -c / 2.0, b - 0.02, c / 2.0); for f in ["S", "N"] { q.open(sd[f][0], sd[f][1], [[0.2, c - 0.24, 0.3, 1.5]], "louvre"); } for f in ["E", "W"] { q.open(sd[f][0], sd[f][1], [[0.2, c - 0.2, 0.3, 1.5]], "louvre"); } q.cyl(x, 0.0, 1.55, h, h + 1.0, "dctower", #{ no_top: true }); if p.running { q.fan(x, 0.0, h + 0.95, 1.35); } else { q.disc(x, 0.0, h + 0.95, 1.35, "fanring"); } }]); } s.seq(items); if p.running { for i in 0..n { s.puffs(x0 + (i + 0.5) * c, 0.0, h + 1.3, "puff2", 3, 4.0); } } let all = dc_it(-1); info_row("Cells", `${n} × ${fmt(num(p.kw))} kW = ${fmt(n * num(p.kw))} kW`); if all.racks > 0 { info_row("Site IT load", `${fmt(all.kw)} kW`); } } fn meta_pipe_run() { #{ label: "Pipe run", category: "Cooling", icon: "═", shape: "line", storey: true, help: "Flow and return pipes along local x (drag to draw) on stands or overhead: chilled water (blue / red), condenser water (green) or glycol (purple). Height of the pipe centre above the object.", props: [ #{ key: "w", type: "float", def: 10.0, min: 0.5, max: 60.0, step: 0.1, label: "Length (m)" }, #{ key: "kind", type: "choice", def: "chilled water", choices: ["chilled water", "condenser", "glycol"], label: "Medium" }, #{ key: "dn", type: "float", def: 0.25, min: 0.05, max: 0.8, step: 0.05, label: "Diameter (m)" }, #{ key: "h", type: "float", def: 0.8, min: 0.2, max: 8.0, step: 0.05, label: "Height (m)" }, #{ key: "pair", type: "bool", def: true, label: "Flow and return" }, #{ key: "stands", type: "bool", def: true, label: "Stands" }, ] } } fn prefab_pipe_run(o) { if dc_skip(o) { return; } let p = o.p; let w = num(p.w) / 2.0; let r = num(p.dn) / 2.0; let z = num(p.h); let off = if p.pair { r + 0.06 } else { 0.0 }; let mats = if p.kind == "condenser" { ["dcpipeg", "dcpipeg2"] } else if p.kind == "glycol" { ["dcpipev", "dcpipev"] } else { ["pipec", "pipes"] }; // the -y pipe is the flow, +y the return; the far one first let ys = if p.pair { if vis(0.0, 1.0) { [[-off, mats[0]], [off, mats[1]]] } else { [[off, mats[1]], [-off, mats[0]]] } } else { [[0.0, mats[0]]] }; let pieces = (num(p.w) / 15.0).ceiling().to_int(); for i in 0..pieces { let x0 = -w + num(p.w) * i / pieces; let x1 = -w + num(p.w) * (i + 1) / pieces; let s = sprite(); if p.stands && z - r > 0.25 { let items = []; let x = x0 + 0.6; while x < x1 - 0.2 { let c = x; items.push([c, 0.0, |q| { q.box(c - 0.04, -0.04, c + 0.04, 0.04, 0.0, z - r - 0.05, "steel"); q.box(c - 0.05, -off - r, c + 0.05, off + r, z - r - 0.05, z - r, "steel"); }]); x += 3.0; } s.seq(items); } // faces edged in their own shade, so a pipe reads as one surface for q in ys { s.tube([x0, q[0], z], [x1, q[0], z], r, r, 10, q[1] + " beast"); } } let what = if p.kind == "condenser" { "condenser water" } else if p.kind == "glycol" { "glycol" } else { "chilled water" }; info_row("Pipes", `${what}, ${if p.pair { "flow and return" } else { "one pipe" }}, DN ${fmt(num(p.dn) * 1000.0)}`); info_row("Run", `${dc_num(num(p.w))} m at ${dc_num(z)} m`); } // ---------------------------------------------------------------- fire safety fn meta_smoke_detectors() { #{ label: "Detectors", category: "Fire safety", icon: "◉", shape: "rect", storey: true, help: "Point detectors on a grid over the rectangle at ceiling height, with their coverage circles. Data halls with high air change rates need a closer spacing than the usual 7.5 m radius.", props: [ #{ key: "w", type: "float", def: 18.0, min: 1.0, max: 200.0, step: 0.5, label: "Width (m)" }, #{ key: "d", type: "float", def: 12.0, min: 1.0, max: 200.0, step: 0.5, label: "Depth (m)" }, #{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 15.0, step: 0.5, label: "Spacing (m)" }, #{ key: "h", type: "float", def: 4.5, min: 2.0, max: 12.0, step: 0.05, label: "Height (m)" }, #{ key: "kind", type: "choice", def: "smoke", choices: ["smoke", "heat", "multi-sensor"], label: "Kind" }, #{ key: "coverage", type: "bool", def: true, label: "Coverage circles" }, #{ key: "zone", type: "text", def: "", label: "Zone" }, ] } } fn prefab_smoke_detectors(o) { if dc_skip(o) || dc_no_ceiling() { return; } let p = o.p; let W = num(p.w); let D = num(p.d); let sp = num(p.spacing); let h = num(p.h); let nx = fmax(1.0, (W / sp).ceiling()).to_int(); let ny = fmax(1.0, (D / sp).ceiling()).to_int(); let r = (W / nx) * (W / nx) / 4.0 + (D / ny) * (D / ny) / 4.0; r = r.sqrt(); dc_ceiling_layer(); let s = sprite(); let pts = []; for i in 0..nx { for j in 0..ny { pts.push([-W / 2.0 + (i + 0.5) * W / nx, -D / 2.0 + (j + 0.5) * D / ny]); } } if p.coverage { for q in pts { s.disc(q[0], q[1], h - 0.06, r, "dccov"); } } // the detectors seen from above (the ceiling is not drawn): white discs let m = if p.kind == "heat" { "dcdethd" } else { "dcdetd" }; for q in pts { s.disc(q[0], q[1], h - 0.04, 0.11, m); } s.begin("a dcflash", `animation-delay:-${round1(hash(o.x + o.y) * 4.0)}s`); for q in pts { s.circle(q[0], q[1], h - 0.075, 0.28, "dcdled"); } s.end(); s.ext_reset(); s.ext_box(-W / 2.0, -D / 2.0, W / 2.0, D / 2.0, h - 0.08, h); let n = nx * ny; let z = if p.zone == "" { "" } else { `${p.zone}: ` }; info_row("Detectors", `${z}${n} ${p.kind} detectors, ${nx} × ${ny} at ${dc_num(W / nx)} × ${dc_num(D / ny)} m`); info_row("Coverage", `${dc_num(W * D / n)} m² each, radius ${dc_num(r)} m to the farthest corner`); } fn meta_fire_pipe() { #{ label: "Fire pipe", category: "Fire safety", icon: "┅", shape: "line", storey: true, help: "A pipe at ceiling height along local x: VESDA aspirating smoke detection (sampling holes, the detector unit at the start, down the wall), pre-action sprinkler heads, or clean-agent gas discharge nozzles. Height above the storey's slab.", props: [ #{ key: "w", type: "float", def: 12.0, min: 0.5, max: 100.0, step: 0.1, label: "Length (m)" }, #{ key: "kind", type: "choice", def: "VESDA", choices: ["VESDA", "sprinkler", "gas"], label: "Kind" }, #{ key: "spacing", type: "float", def: 4.0, min: 0.5, max: 12.0, step: 0.1, label: "Holes / heads every (m)" }, #{ key: "h", type: "float", def: 4.3, min: 2.0, max: 12.0, step: 0.05, label: "Height (m)" }, #{ key: "unit", type: "bool", def: true, label: "Detector unit (VESDA)" }, ] } } fn prefab_fire_pipe(o) { if dc_skip(o) || dc_no_ceiling() { return; } let p = o.p; let w = num(p.w) / 2.0; let h = num(p.h); let sp = num(p.spacing); let k = p.kind; let cls = if k == "gas" { "dcgasl" } else if k == "sprinkler" { "dcsprl" } else { "dcvesl" }; let n = fmax(1.0, (num(p.w) / sp).floor()).to_int(); let xs = []; for i in 0..n { xs.push(-w + (i + 0.5) * num(p.w) / n); } dc_ceiling_layer(); let s = sprite(); s.polyline([[[-w, 0.0, h], [w, 0.0, h]]], 0.0, cls); if k == "VESDA" { for x in xs { s.circle(x, 0.0, h - 0.02, 0.4, "dchole"); } } else if k == "sprinkler" { for x in xs { s.stem(x, 0.0, h - 0.12, h, 0.12, "dcspr"); s.circle(x, 0.0, h - 0.13, 0.42, "dcsprh"); } } else { let items = []; for x in xs { let c = x; items.push([c, 0.0, |q| q.cyl(c, 0.0, 0.05, h - 0.14, h, "dcgaspipe")]); } s.seq(items); } // a tight sort box (VESDA holes sit on the pipe, heads and nozzles hang below) s.ext_reset(); s.ext_box(-w, -0.05, w, 0.05, if k == "VESDA" { h - 0.03 } else { h - 0.15 }, h + 0.01); if k == "VESDA" && p.unit { // the detector unit at the start, at eye height, the pipe dropping to it let z0 = dc_lift_at(o, world_of(o, -w, 0.0)[0], world_of(o, -w, 0.0)[1]); layer("main"); let u = sprite(); u.polyline([[[-w, 0.0, h], [-w, 0.0, z0 + 1.75]]], 0.0, cls); u.box(-w - 0.06, -0.18, -w + 0.06, 0.18, z0 + 1.3, z0 + 1.75, "dcvesda", #{ top: "dcvesda" }); u.ext_box(-w - 0.06, -0.18, -w + 0.06, 0.18, z0 + 1.3, h); } let what = if k == "gas" { "gas discharge pipe" } else if k == "sprinkler" { "pre-action sprinkler pipe" } else { "VESDA sampling pipe" }; let heads = if k == "gas" { "nozzles" } else if k == "sprinkler" { "heads" } else { "sampling holes" }; info_row("Pipe", `${what}, ${dc_num(num(p.w))} m at ${dc_num(h)} m`); info_row(if k == "VESDA" { "Holes" } else { "Heads" }, `${n} ${heads}, ${dc_num(num(p.w) / n)} m apart`); if k == "VESDA" { info_row("Transport", `about ${fmt(num(p.w) / 1.5 + 5.0)} s from the far hole (aim for < 60 s)`); } } /// Clean agent needed for V m³ at t °C: #{ need, per (one cylinder), unit, c }. fn dc_agent(agent, V, t, litres) { if agent == "FM-200" { let S = 0.1269 + 0.000513 * t; let c = 7.0; #{ need: V / S * c / (100.0 - c), per: litres * 1.0, unit: "kg", c: c, fill: "1.0 kg/l" } } else if agent == "Inergen" { let S = 0.65799 + 0.00239 * t; let c = 39.9; #{ need: 2.303 * (0.707 / S) * (100.0 / (100.0 - c)).ln() / 2.302585 * V, per: litres * 0.295, unit: "m³", c: c, fill: "300 bar" } } else { let S = 0.0664 + 0.0002741 * t; let c = 4.5; #{ need: V / S * c / (100.0 - c), per: litres * 1.2, unit: "kg", c: c, fill: "1.2 kg/l" } } } /// Volume a gas bank on storey s protects: its halls (raised floors, void and /// ceiling included) or, without any, the dc_building it stands in. fn dc_gas_volume(o) { let s = storey_of(o.z); let H = storey_h() - 0.3; let v = 0.0; for f in objects_of(["raised_floor"]) { if storey_of(f.z) == s { v += num(f.p.w) * num(f.p.d) * H; } } if v > 0.0 { return v; } for b in objects_at(["dc_building"], o.x, o.y) { v = (num(b.p.w) - 2.0 * num(b.p.t)) * (num(b.p.d) - 2.0 * num(b.p.t)) * H; } v } fn meta_gas_bank() { #{ label: "Gas cylinders", category: "Fire safety", icon: "⛽", storey: true, help: "A bank of clean-agent cylinders along local x, backs to +y (against a wall), with the manifold and release heads. It sizes the agent for the protected volume (the raised-floor halls on its storey, void and ceiling included, unless Volume is set) and warns when the bank is short.", props: [ #{ key: "agent", type: "choice", def: "Novec 1230", choices: ["Novec 1230", "FM-200", "Inergen"], label: "Agent" }, #{ key: "n", type: "int", def: 8, min: 1, max: 40, step: 1, label: "Cylinders" }, #{ key: "litres", type: "int", def: 140, min: 40, max: 180, step: 10, label: "Cylinder (l)" }, #{ key: "vol", type: "float", def: 0.0, min: 0.0, max: 200000.0, step: 50.0, label: "Volume (m³, 0: auto)" }, #{ key: "temp", type: "float", def: 20.0, min: 0.0, max: 40.0, step: 1.0, label: "Design °C" }, ] } } fn prefab_gas_bank(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let r = 0.13 + (num(p.litres) - 80.0) * 0.0005; let ch = 1.0 + num(p.litres) * 0.0045; let pitch = 2.0 * r + 0.08; let n = p.n; let B = n * pitch / 2.0; let s = sprite(); // the wall bracket behind, then the cylinders back to front, the manifold over them s.box(-B, r + 0.02, B, r + 0.07, z0 + ch * 0.55, z0 + ch * 0.62, "steel"); let items = []; let shoulder = if p.agent == "Inergen" { "dcgreen" } else { "firetank" }; for i in 0..n { let x = -B + (i + 0.5) * pitch; items.push([x, 0.0, |q| { q.cyl(x, 0.0, r, z0, z0 + ch - r * 0.6, "firetank", #{ no_top: true }); q.frust(x, 0.0, r, r * 0.35, z0 + ch - r * 0.6, z0 + ch, shoulder, 12); q.box(x - 0.06, -0.06, x + 0.06, 0.06, z0 + ch, z0 + ch + 0.1, "keyb"); }]); } s.seq(items); let zm = z0 + ch + 0.25; s.tube([-B, 0.0, zm], [B, 0.0, zm], 0.05, 0.05, 8, "dcgaspipe beast"); s.tube([B, 0.0, zm], [B, 0.0, zm + 1.0], 0.05, 0.05, 8, "dcgaspipe beast"); let V = if num(p.vol) > 0.0 { num(p.vol) } else { dc_gas_volume(o) }; let a = dc_agent(p.agent, V, num(p.temp), num(p.litres)); let have = n * a.per; info_row("Agent", `${p.agent}, ${dc_num(a.c)} % design concentration at ${dc_num(num(p.temp))} °C`); info_row("Volume", `${fmt(V)} m³${if num(p.vol) > 0.0 { "" } else { " (halls on this storey, void and ceiling included)" }}`); info_row("Needed", `${fmt(a.need)} ${a.unit}`); info_row("Installed", `${n} × ${p.litres} l = ${fmt(have)} ${a.unit} (${a.fill})`); if V <= 0.0 { info_row("Status", "no volume: set one or put raised floors on this storey"); } else if have + 1e-6 < a.need { let more = ((a.need - have) / a.per).ceiling().to_int(); info_row("Status", `short: ${more} more cylinder${if more == 1 { "" } else { "s" }}`); tag(0.0, 0.0, z0 + ch + 0.6, `Gas: ${more} cylinder${if more == 1 { "" } else { "s" }} short`, 9, o.id, "warntag"); } else { info_row("Status", `enough, ${fmt((have / a.need - 1.0) * 100.0)} % spare`); } } fn dc_device_kinds() { ["alarm panel", "release panel", "call point", "abort switch", "beacon", "extinguisher"] } fn meta_fire_device() { #{ label: "Fire device", category: "Fire safety", icon: "🔔", storey: true, help: "A fire alarm panel, extinguishant release panel, manual call point, gas abort switch, sounder-beacon or extinguisher, its back to local +y (against a wall).", props: [ #{ key: "kind", type: "choice", def: "call point", choices: dc_device_kinds(), label: "Kind" }, #{ key: "label", type: "text", def: "", label: "Zone / label" }, ] } } fn prefab_fire_device(o) { if dc_skip(o) { return; } let p = o.p; let z0 = dc_lift(o); let k = p.kind; // a wall device turned away from the viewer on the storey cut open hangs // on the far side of a wall that is cut low: ghost it let s = if k != "extinguisher" && house_view(o) == 1 && !vis(0.0, -1.0) { sprite(o.id, "ghost") } else { sprite() }; if k == "alarm panel" { s.box(-0.3, 0.02, 0.3, 0.17, z0 + 1.1, z0 + 1.9, "dcpanel"); let sd = sides(-0.3, 0.02, 0.3, 0.17); s.open(sd.S[0], sd.S[1], [[0.12, 0.48, z0 + 1.55, z0 + 1.78]], "dcscr"); s.open(sd.S[0], sd.S[1], [[0.12, 0.48, z0 + 1.2, z0 + 1.45]], "seam"); if vis(0.0, -1.0) { s.circle(-0.22, 0.01, z0 + 1.5, 0.3, "ledg"); s.circle(-0.22, 0.01, z0 + 1.4, 0.3, "ledo"); } } else if k == "release panel" { s.box(-0.22, 0.04, 0.22, 0.17, z0 + 1.2, z0 + 1.75, "dcswg"); let sd = sides(-0.22, 0.04, 0.22, 0.17); s.open(sd.S[0], sd.S[1], [[0.04, 0.4, z0 + 1.6, z0 + 1.7]], "dcyel"); s.open(sd.S[0], sd.S[1], [[0.08, 0.36, z0 + 1.35, z0 + 1.52]], "dcscr"); if vis(0.0, -1.0) { s.circle(0.12, 0.03, z0 + 1.28, 0.3, "ledr"); } } else if k == "call point" || k == "abort switch" { let m = if k == "call point" { "dcpanel" } else { "dcyel" }; s.box(-0.07, 0.11, 0.07, 0.17, z0 + 1.33, z0 + 1.47, m); let sd = sides(-0.07, 0.11, 0.07, 0.17); s.open(sd.S[0], sd.S[1], [[0.03, 0.11, z0 + 1.36, z0 + 1.44]], if k == "call point" { "dcwhite" } else { "dcpanel" }); } else if k == "beacon" { // the dome stays in front of the wall face at +y s.box(-0.07, 0.07, 0.07, 0.17, z0 + 2.3, z0 + 2.4, "dcpanel"); s.frust(0.0, 0.12, 0.05, 0.03, z0 + 2.4, z0 + 2.52, "dcpanel", 10); s.circle(0.0, 0.12, z0 + 2.47, 0.55, "ledr"); } else { s.cyl(0.0, 0.05, 0.09, z0, z0 + 0.62, "firetank"); s.box(-0.03, 0.02, 0.03, 0.08, z0 + 0.62, z0 + 0.72, "keyb"); s.box(0.03, 0.03, 0.16, 0.07, z0 + 0.6, z0 + 0.66, "keyb"); } let what = if k == "alarm panel" { "fire alarm control panel" } else if k == "release panel" { "extinguishant release control panel" } else if k == "call point" { "manual call point" } else if k == "abort switch" { "gas release abort switch (hold to delay)" } else if k == "beacon" { "sounder and flashing beacon" } else { "CO2 extinguisher, 5 kg" }; info_row("Device", what); if p.label != "" { info_row("Zone", p.label); } } // ---------------------------------------------------------------- power fn meta_power_cab() { #{ label: "Power cabinets", category: "Power", icon: "🔋", storey: true, help: "A line-up of UPS, battery, PDU, switchgear or remote power panel cabinets along local x, fronts to local -y. UPS and battery tooltips set rating and runtime against the IT load of the building (every rack row and pod).", props: [ #{ key: "kind", type: "choice", def: "UPS", choices: ["UPS", "battery", "PDU", "switchgear", "RPP"], label: "Kind" }, #{ key: "n", type: "int", def: 4, min: 1, max: 30, step: 1, label: "Cabinets" }, #{ key: "kw", type: "float", def: 250.0, min: 0.0, max: 5000.0, step: 10.0, label: "kW / kVA each" }, #{ key: "kwh", type: "float", def: 40.0, min: 0.0, max: 1000.0, step: 5.0, label: "kWh (battery)" }, ] } } fn prefab_power_cab(o) { if dc_skip(o) { return; } let p = o.p; let k = p.kind; let z0 = dc_lift(o); let cw = if k == "battery" || k == "RPP" { 0.6 } else { 0.8 }; let cd = if k == "switchgear" { 1.0 } else if k == "RPP" { 0.5 } else if k == "UPS" { 0.9 } else { 0.8 }; let h = if k == "switchgear" { 2.2 } else if k == "PDU" { 1.9 } else { 2.0 }; let m = if k == "UPS" { "dcups" } else if k == "battery" { "dcbatt" } else if k == "PDU" || k == "RPP" { "dcpdu" } else { "dcswg" }; let n = p.n; let B = n * cw / 2.0; let d = cd / 2.0; let s = sprite(); let items = []; for i in 0..n { let a = -B + i * cw; let b = a + cw; items.push([(a + b) / 2.0, 0.0, |q| q.box(a, -d, b, d, z0, z0 + h, m)]); } s.seq(items); let F = dc_xface(B, -d, -1.0); let lists = #{ seam: [], grille: [], dcscr: [], dcpanelb: [], dcbrk: [] }; for i in 0..n { let a = -B + i * cw; let b = a + cw; lists.seam.push(dc_span(F, B, a + 0.03, b - 0.03, z0 + 0.05, z0 + h - 0.05)); if k == "UPS" { lists.dcscr.push(dc_span(F, B, a + 0.2, b - 0.2, z0 + 1.45, z0 + 1.65)); lists.grille.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.15, z0 + 0.6)); } else if k == "battery" { for j in 0..6 { lists.grille.push(dc_span(F, B, a + 0.08, b - 0.08, z0 + 0.2 + j * 0.28, z0 + 0.32 + j * 0.28)); } } else if k == "PDU" { lists.grille.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.15, z0 + 0.9)); lists.dcscr.push(dc_span(F, B, a + 0.25, b - 0.25, z0 + 1.35, z0 + 1.5)); } else if k == "RPP" { lists.dcpanelb.push(dc_span(F, B, a + 0.08, b - 0.08, z0 + 0.5, z0 + 1.7)); for j in 0..10 { lists.dcbrk.push(dc_span(F, B, a + 0.12, b - 0.12, z0 + 0.58 + j * 0.11, z0 + 0.62 + j * 0.11)); } } else { for j in 0..3 { lists.dcpanelb.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.2 + j * 0.65, z0 + 0.75 + j * 0.65)); } } } for c in ["seam", "dcpanelb", "dcbrk", "grille", "dcscr"] { if lists[c].len() > 0 { s.open(F.a, F.b, lists[c], c); } } if vis(0.0, -1.0) && k != "battery" { for i in 0..n { s.circle(-B + (i + 0.5) * cw + 0.2, -d - 0.01, z0 + h - 0.15, 0.3, "ledg"); } } let all = dc_it(-1); let tot = n * num(p.kw); if k == "UPS" { info_row("UPS", `${n} × ${fmt(num(p.kw))} kW = ${fmt(tot)} kW`); if all.kw > 0.0 { info_row("Load", `${fmt(all.kw)} kW IT = ${fmt(all.kw / tot * 100.0)} % of the line-up`); info_row("Redundancy", dc_redundancy(#{ duty: tot, standby: 0.0, units: n }, all.kw)); } } else if k == "battery" { // the strings work together: runtime from every battery cabinet on the site let e = n * num(p.kwh); let te = 0.0; let tn = 0; for b in objects_of(["power_cab"]) { if b.p.kind == "battery" { te += b.p.n * num(b.p.kwh); tn += b.p.n; } } info_row("Batteries", `${n} × ${fmt(num(p.kwh))} kWh = ${fmt(e)} kWh`); if tn > n { info_row("All strings", `${tn} cabinets, ${fmt(te)} kWh`); } if all.kw > 0.0 { info_row("Runtime", `about ${dc_num(te * 60.0 * 0.9 / all.kw)} min at ${fmt(all.kw)} kW IT`); } } else if k == "PDU" { info_row("PDUs", `${n} × ${fmt(num(p.kw))} kVA = ${fmt(tot)} kVA`); let it = dc_it(storey_of(o.z)); if it.kw > 0.0 { info_row("This storey", `${fmt(it.kw)} kW IT in ${it.racks} racks`); } } else if k == "RPP" { info_row("Power panels", `${n} remote power panels, ${n * 42} ways`); } else { info_row("Switchgear", `${n} panels`); } } fn meta_transformer() { #{ label: "Transformer", category: "Power", icon: "⚡", help: "A distribution transformer on a plinth: oil-filled with radiators and bushings, or a dry type in a louvred enclosure.", props: [ #{ key: "kva", type: "int", def: 2500, min: 100, max: 10000, step: 100, label: "Rating (kVA)" }, #{ key: "kind", type: "choice", def: "oil", choices: ["oil", "dry"], label: "Type" }, ] } } fn prefab_transformer(o) { if dc_skip(o) { return; } let p = o.p; let s = sprite(); s.box(-1.6, -1.3, 1.6, 1.3, 0.0, 0.2, "plinth"); if p.kind == "dry" { s.box(-1.3, -1.0, 1.3, 1.0, 0.2, 2.4, "trafo"); let sd = sides(-1.3, -1.0, 1.3, 1.0); for f in ["S", "N"] { s.open(sd[f][0], sd[f][1], [[0.2, 1.2, 0.5, 2.1], [1.4, 2.4, 0.5, 2.1]], "louvre"); } for f in ["E", "W"] { s.open(sd[f][0], sd[f][1], [[0.2, 1.8, 1.4, 2.1]], "louvre"); } } else { // radiators either side of the tank, the far one first let ys = if vis(0.0, 1.0) { [-1.0, 1.0] } else { [1.0, -1.0] }; let fins = []; for i in 0..9 { fins.push([0.08 + i * 0.2, 0.16 + i * 0.2, 0.45, 1.75]); } for y in ys { let y0 = if y < 0.0 { -1.2 } else { 0.7 }; if y == ys[1] { s.box(-0.95, -0.7, 0.95, 0.7, 0.2, 2.0, "trafo"); } s.box(-0.9, y0, 0.9, y0 + 0.5, 0.4, 1.8, "trafo"); let sd = sides(-0.9, y0, 0.9, y0 + 0.5); s.open(sd.S[0], sd.S[1], fins, "seam"); s.open(sd.N[0], sd.N[1], fins, "seam"); } let items = []; for x in [-0.5, 0.0, 0.5] { let c = x; items.push([c, -0.3, |q| q.cyl(c, -0.3, 0.07, 2.0, 2.5, "bushing")]); } for x in [-0.6, -0.2, 0.2, 0.6] { let c = x; items.push([c, 0.35, |q| q.cyl(c, 0.35, 0.05, 2.0, 2.25, "bushing")]); } s.seq(items); } info_row("Transformer", `${fmt(p.kva)} kVA, ${if p.kind == "dry" { "dry type (cast resin)" } else { "oil-filled (ONAN)" }}`); info_row("Carries", `about ${fmt(p.kva * 0.9 * 0.8)} kW at 0.9 pf and 80 % loading`); }