data-centre-nameplates / tests /nameplate-model.test.mjs
constructelligence's picture
Upload folder using huggingface_hub
5e2d1eb verified
Raw History Blame Contribute Delete
7.88 kB
// Nameplate parsing, schedule matching and the register CSV (pure functions).
// Run: npm test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
parseNameplate, voltagesIn, parseEquipmentSchedule, scheduleVoltages, candidatesFor, checkAgainst, statusOf,
codeSimilarity, foldCode, duplicateSerials, registerSummary, registerCsv, SAMPLE_SCHEDULE, SAMPLE_PLATES,
} from '../nameplate-model.js';
const plate = lines => parseNameplate(lines.join('\n'));
const val = (r, k) => r.fields[k]?.value;
const schedule = parseEquipmentSchedule(SAMPLE_SCHEDULE).items;
test('a UPS plate reads into every field', () => {
const r = plate(SAMPLE_PLATES[0]);
assert.equal(r.type, 'ups');
assert.equal(val(r, 'manufacturer'), 'Northline Power Systems');
assert.equal(val(r, 'model'), 'NPX-750-480');
assert.equal(val(r, 'serial'), 'NL26A01937');
assert.equal(val(r, 'voltage'), '480Y/277 V');
assert.deepEqual(r.fields.voltage.values, [480, 277]);
assert.equal(val(r, 'phase'), 3);
assert.equal(val(r, 'hz'), 60);
assert.equal(val(r, 'kva'), 750);
assert.equal(val(r, 'kw'), 675);
assert.equal(val(r, 'amps'), 1002);
assert.equal(val(r, 'mfgDate'), '03/2026');
});
test('real-world plate wording: S/N, CAT. NO., kV, thousands separators, known makers', () => {
const gen = plate(['CATERPILLAR', 'GENERATOR SET', 'CAT. NO. 3516C-HD', 'S/N: ZBA00412', '13.8 KV 3 PHASE 60 HZ', '3,750 KVA 3000 EKW', 'FLA 1,569']);
assert.equal(gen.type, 'generator');
assert.equal(val(gen, 'manufacturer'), 'Caterpillar');
assert.equal(val(gen, 'model'), '3516C-HD');
assert.equal(val(gen, 'serial'), 'ZBA00412');
assert.deepEqual(gen.fields.voltage.values, [13800]);
assert.equal(val(gen, 'kva'), 3750);
assert.equal(val(gen, 'kw'), 3000);
assert.equal(val(gen, 'amps'), 1569);
const ch = plate(['AIR-COOLED CHILLER', 'MODEL: AC-1200', 'REFRIGERANT R-513A CHARGE 410 LBS', '460V 3PH 60HZ', 'MCA 612 MOCP 800']);
assert.equal(ch.type, 'chiller');
assert.equal(val(ch, 'refrigerant'), 'R-513A');
assert.equal(val(ch, 'mca'), 612);
assert.equal(val(ch, 'mocp'), 800);
assert.equal(val(ch, 'amps'), undefined, 'R-513A and a charge weight are not amps');
});
test('voltages: transformer pairs, labelled values, and numbers that are not voltages', () => {
assert.deepEqual(voltagesIn('PRI 13,800 V DELTA SEC 480Y/277 V').values, [480, 277, 13800]);
assert.deepEqual(voltagesIn('VOLTS: 208').values, [208]);
assert.deepEqual(voltagesIn('DWG 480/277 REV 2').values, [], 'a bare pair with no voltage word or unit');
assert.deepEqual(scheduleVoltages('480 / 415Y/240'), [480, 415, 240]);
assert.deepEqual(scheduleVoltages('13.8kV'), [13800]);
});
test('"New York" in an address is not the maker York; unknown makers come from a company line', () => {
const r = plate(['ACME SWITCHGEAR CORP', 'NEW YORK NY', 'SWITCHBOARD', 'MODEL SB-4000']);
assert.equal(val(r, 'manufacturer'), 'Acme Switchgear Corp');
assert.equal(r.type, 'switchgear');
});
test('codes compare the way OCR misreads them', () => {
assert.equal(foldCode('NPX-75O-48O'), foldCode('NPX750480'));
assert.equal(codeSimilarity('NL26A0I937', 'NL26A01937'), 1);
assert.ok(codeSimilarity('9395P-750', '9395P-750-N-480') >= 0.9, 'option suffix on the plate');
assert.ok(codeSimilarity('NPX-750-415', 'NPX-750-480') < 0.85, 'a different voltage variant is not the same model');
});
test('schedule CSV: header row found below title rows, duplicate tags reported', () => {
const { items, problems } = parseEquipmentSchedule('Data Hall 1 equipment schedule\n\nMark,Equipment,Mfr,Model No.,Volts,kVA\nUPS-1A,UPS,Eaton,9395XR,480,750\nUPS-1A,UPS,Eaton,9395XR,480,750\n');
assert.equal(items.length, 1);
assert.equal(items[0].manufacturer, 'Eaton');
assert.equal(items[0].kva, 750);
assert.match(problems[0], /UPS-1A appears more than once/);
assert.match(parseEquipmentSchedule('a,b\n1,2').problems[0], /No header row/);
});
test('identical units: the first unscanned tag is offered first', () => {
const r = plate(SAMPLE_PLATES[0]);
const first = candidatesFor(r, schedule);
assert.deepEqual(first.slice(0, 4).map(c => c.item.tag), ['UPS-1A', 'UPS-1B', 'UPS-2A', 'UPS-2B']);
const next = candidatesFor(r, schedule, [{ tag: 'UPS-1A' }]);
assert.equal(next[0].item.tag, 'UPS-1B');
assert.equal(next.find(c => c.item.tag === 'UPS-1A').registered, true);
assert.ok(!first.some(c => c.item.tag.startsWith('GEN')), 'a generator is not a candidate for a UPS plate');
});
test('a serial on the schedule decides the match outright', () => {
const items = parseEquipmentSchedule('Tag,Model,Serial\nUPS-1A,NPX-750-480,NL26A00001\nUPS-1B,NPX-750-480,NL26A01937\n').items;
assert.equal(candidatesFor(plate(SAMPLE_PLATES[0]), items)[0].item.tag, 'UPS-1B');
});
test('checks: the right unit verifies; the wrong-voltage unit is flagged on model and voltage', () => {
const ups1a = schedule.find(i => i.tag === 'UPS-1A');
const good = checkAgainst(plate(SAMPLE_PLATES[0]), ups1a);
assert.ok(good.every(c => c.status === 'ok'), JSON.stringify(good));
assert.equal(statusOf(good, 'UPS-1A'), 'verified');
const bad = checkAgainst(plate(SAMPLE_PLATES[1]), ups1a);
assert.deepEqual(bad.filter(c => c.status === 'mismatch').map(c => c.key), ['model', 'voltage']);
assert.equal(statusOf(bad, 'UPS-1A'), 'mismatch');
const unread = checkAgainst(parseNameplate('MODEL NPX-750-480'), ups1a);
assert.equal(statusOf(unread, 'UPS-1A'), 'partial');
assert.equal(statusOf([], ''), 'unmatched');
});
test('register: summary, duplicate serials, and a CSV that lists what is still unscanned', () => {
const a = plate(SAMPLE_PLATES[0]);
const ups1a = schedule.find(i => i.tag === 'UPS-1A');
const reg = [
{ id: 'a', tag: 'UPS-1A', type: 'ups', fields: a.fields, checks: checkAgainst(a, ups1a), status: 'verified', scannedAt: Date.UTC(2026, 9, 2, 14, 30) },
{ id: 'b', tag: '', type: 'ups', fields: a.fields, checks: [], status: 'unmatched', scannedAt: Date.UTC(2026, 9, 2, 14, 35) },
];
assert.deepEqual(duplicateSerials(reg), [['a', 'b']]);
const s = registerSummary(reg, schedule);
assert.equal(s.scannedOfSchedule, 1);
assert.equal(s.verified, 1);
assert.equal(s.unmatched, 1);
assert.equal(s.missing.length, schedule.length - 1);
const rows = registerCsv(reg, schedule).trim().split('\n');
assert.equal(rows.length, 1 + 2 + schedule.length - 1);
assert.match(rows[1], /^UPS-1A,Verified,UPS,Electrical Room 1A,Northline Power Systems,NPX-750-480,NL26A01937,480Y\/277 V,3,60,750,675,1002,,,,03\/2026,Serial also on another scanned plate,2026-10-02 14:30$/);
assert.match(rows[3], /^UPS-1B,Not scanned yet,/);
});
test('OCR misreads seen on real lettering are undone before parsing', () => {
const r = parseNameplate(['MODEL NO: NPX- 750- 480', '480¥/277 VAC 3 PH 60 Hw', '460 Vv', '750 XVA / 675 XW', 'FLA 38. 5'].join('\n'));
assert.equal(r.fields.model.value, 'NPX-750-480');
assert.deepEqual(r.fields.voltage.values, [480, 277, 460]);
assert.equal(r.fields.hz.value, 60);
assert.equal(r.fields.kva.value, 750);
assert.equal(r.fields.kw.value, 675);
assert.equal(r.fields.amps.value, 38.5);
});
test('MCA and MOCP are checked when the schedule lists them; near-zero-confidence lines are ignored', () => {
const ch = schedule.find(i => i.tag === 'CH-01');
assert.equal(ch.mca, 612);
const r = parseNameplate([{ text: 'MODEL AC-1200', confidence: 90 }, { text: '460 V 3 PH', confidence: 88 }, { text: 'MCA 612 MOCP 700', confidence: 85 },
{ text: 'REFRIGERANT R-513A', confidence: 90 }, { text: 'MODEL ZZ-9 480 V', confidence: 8 }]);
assert.equal(r.fields.model.value, 'AC-1200', 'the 8% line is texture, not a second model');
const checks = checkAgainst(r, ch);
assert.deepEqual(checks.filter(c => c.status === 'mismatch').map(c => c.key), ['mocp']);
});