edf2csv 0.5.82 → 0.5.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,40 @@
3
3
  Notable changes to edf2csv. Versions follow [semantic versioning](https://semver.org); while the
4
4
  major version is 0, a minor bump may contain breaking changes.
5
5
 
6
+ ## 0.5.83
7
+
8
+ ### Fixed: a physical span too small to represent became a flat channel, silently
9
+
10
+ ```
11
+ time_s,MAG
12
+ 0.000,0.000
13
+ 0.250,0.000
14
+ 0.500,0.000
15
+ ```
16
+
17
+ Eight samples spanning digital -16,000 to +12,000, all written as the same number, no
18
+ diagnostic anywhere, `--strict` exiting 0. The header declares -1e-320 to 1e-320 over the full
19
+ 16-bit range: 65,536 distinct physical values, none of them equal to another.
20
+
21
+ The gain is the span over the digital range — 2e-320/65535, or 3e-325, which is smaller than
22
+ the smallest subnormal double and underflows to +0. `makeScaler` tests `gain === 0` and takes
23
+ its flat-range branch, whose comment is correct about the case it was written for: "A flat
24
+ physical range makes every sample the same value ... That mapping is defined, so its constant
25
+ is written." An underflowed gain is not a flat range, and from inside that test the two look
26
+ identical.
27
+
28
+ The answer was already in the function, eight lines below. Overflow gets it: "the physical span
29
+ overflowed a double, so there is no mapping at all. Returning physicalMin filled the column with
30
+ one enormous constant — every distinct sample rendered as the same 300-digit number — and raised
31
+ nothing." Underflow is the same fact about the same header and now takes the same route: empty
32
+ cells, and `UNUSABLE_PHYSICAL_RANGE` saying the span is too small rather than too large.
33
+
34
+ A genuinely flat range still writes its constant. That mapping is defined, every sample really
35
+ is that value, and it has `DEGENERATE_PHYSICAL_RANGE` of its own.
36
+
37
+ One power of ten away, at 1e-319, the same file has always raised `VALUE_RESOLUTION` — this was
38
+ the one gap in a row of neighbours that all report themselves.
39
+
6
40
  ## 0.5.82
7
41
 
8
42
  ### Fixed: `--stdout --gzip` onto a full destination announced every row and exited 0
@@ -318,12 +318,28 @@ export function parseHeader(buf, fileSize) {
318
318
  hint: 'It is described in channels.csv but left out of the converted data.',
319
319
  });
320
320
  }
321
- if (!Number.isFinite(physicalMax - physicalMin)) {
321
+ /*
322
+ Too large to represent, and too small — the second was silent.
323
+
324
+ The gain is the span divided by the digital range, and a span of 2e-320 over 65,535
325
+ codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The
326
+ scaler's flat-range branch then handed every code the same physical value, and a
327
+ channel of 65,536 distinct readings became one repeated number with nothing raised at
328
+ all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.
329
+
330
+ Both are the same fact about the header — the span cannot be turned into a mapping —
331
+ so both get this code, and both leave the cells empty rather than filling them with a
332
+ value the header cannot justify.
333
+ */
334
+ const span = physicalMax - physicalMin;
335
+ const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;
336
+ if (!Number.isFinite(span) || underflowed) {
322
337
  diagnostics.push({
323
338
  code: 'UNUSABLE_PHYSICAL_RANGE',
324
339
  severity: 'warning',
325
340
  message: `Signal ${i} ("${label}") declares a physical range from ${physicalMin} to ` +
326
- `${physicalMax}, whose span is too large to represent, so its values cannot be scaled.`,
341
+ `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +
342
+ `represent, so its values cannot be scaled.`,
327
343
  hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',
328
344
  });
329
345
  }
@@ -1 +1 @@
1
- {"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, listed } from '../format/list.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const count = Number(normaliseNumberField(dec(fixed, 252, 4)).text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`);\n }\n const n = Number(text);\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n 'The file may be truncated, byte-shifted, or not an EDF file at all.',\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n );\n }\n return n;\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n */\nfunction resolveStartDateTime(dateRaw: string, timeRaw: string): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const year = yy >= 85 ? 1900 + yy : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${fileSize} bytes; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`,\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${signalCount} signals; expected at least 1.`,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${recordDuration}s; expected a positive number.`,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${signalCount} signals, which needs a ${expectedHeaderBytes}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${fileSize} bytes.`\n : `but only ${buf.length} bytes of it were handed to the parser.`),\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${headerBytes} bytes, but ${signalCount} signals require ` +\n `${expectedHeaderBytes} bytes. Using the value computed from the signal count.`,\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n const inLabel = [...label].filter(isControlCharacter);\n const inUnit = [...physicalDimension].filter(isControlCharacter);\n const control = [...inLabel, ...inUnit];\n if (control.length > 0) {\n const shown = [...new Set(control)]\n .map((c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`)\n .join(', ');\n const plural = control.length === 1 ? '' : 's';\n const both = inLabel.length > 0 && inUnit.length > 0;\n const field = both ? 'label and unit contain' : inLabel.length > 0 ? 'label contains' : 'unit contains';\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; a unit is a cell of channels.csv and nothing else.\n const lands = both\n ? 'which will appear in the CSV column name and in channels.csv\\'s unit cell'\n : inLabel.length > 0\n ? 'which will appear in the CSV column name'\n : 'which will appear in channels.csv\\'s unit cell';\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${field} ${control.length} control character${plural} ` +\n `(${shown}), ${lands} exactly as the header has them.`,\n hint:\n (inLabel.length > 0\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n 'since the name cannot be typed. '\n : `The column name is unaffected, so --channels \"${label}\" still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n if (!Number.isFinite(physicalMax - physicalMin)) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too large to represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `${indices.length} signals share the label \"${label}\" (positions ${indices.join(', ')}).`,\n hint: 'Their columns are suffixed with the signal number so they stay distinguishable.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n throw new EdfError('FILE_TOO_SMALL', `File is smaller than its own header.`);\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n throw new EdfError(\n 'NO_DATA_RECORDS',\n 'The file contains a header but no complete data record.',\n 'The recording was probably interrupted before any data was written.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n message:\n `The header declares ${declaredRecordCount} data records but the file contains ` +\n `${recordCount}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n message: 'This file has no signal channels; it contains only EDF+ annotations.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${rates.size} different sampling rates ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/** \"EDF\", \"EDF+ (EDF+D)\", \"BDF\", \"BDF+ (EDF+C)\". */\nexport function describeFormat(header: EdfHeader): string {\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n if (Number.isInteger(hz)) return String(hz);\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
1
+ {"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/edf/header.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EDF / EDF+ header parsing.\n *\n * Layout (all fields are ASCII, left-justified, space-padded):\n *\n * fixed header, 256 bytes\n * 0 8 version ('0', or 255 + 'BIOSEMI' for BDF)\n * 8 80 patient identification\n * 88 80 recording identification\n * 168 8 start date dd.mm.yy\n * 176 8 start time hh.mm.ss\n * 184 8 number of bytes in the header record\n * 192 44 reserved ('EDF+C' / 'EDF+D' live here)\n * 236 8 number of data records (-1 if unknown)\n * 244 8 duration of a data record, in seconds (may be fractional)\n * 252 4 number of signals (ns)\n *\n * signal header, ns * 256 bytes, stored FIELD-major rather than signal-major:\n * all ns labels, then all ns transducer types, and so on.\n * ns * 16 label\n * ns * 80 transducer type\n * ns * 8 physical dimension\n * ns * 8 physical minimum\n * ns * 8 physical maximum\n * ns * 8 digital minimum\n * ns * 8 digital maximum\n * ns * 80 prefiltering\n * ns * 8 number of samples in each data record\n * ns * 32 reserved\n */\n\nimport { EdfError } from './errors.js';\nimport type { Diagnostic } from './errors.js';\nimport { counted, listed } from '../format/list.js';\nimport { decodeLatin1 } from './bytes.js';\n\n/** Label the EDF+ spec reserves for the annotations channel. */\nexport const ANNOTATIONS_LABEL = 'EDF Annotations';\n/** BDF+ uses its own spelling for the same channel. */\nexport const BDF_ANNOTATIONS_LABEL = 'BDF Annotations';\n\nexport const FIXED_HEADER_BYTES = 256;\nexport const SIGNAL_HEADER_BYTES = 256;\n\nexport interface EdfSignal {\n /** Position in the file, 0-based. Stable even when labels collide. */\n index: number;\n label: string;\n transducer: string;\n physicalDimension: string;\n physicalMin: number;\n physicalMax: number;\n digitalMin: number;\n digitalMax: number;\n prefiltering: string;\n samplesPerRecord: number;\n reserved: string;\n /** True for the EDF+ 'EDF Annotations' channel, which carries text, not signal. */\n isAnnotations: boolean;\n /** samplesPerRecord / recordDuration, in Hz. */\n samplingRate: number;\n /** Byte offset of this signal's samples within one data record. */\n byteOffsetInRecord: number;\n}\n\nexport interface EdfHeader {\n version: string;\n patientId: string;\n recordingId: string;\n /** Raw 'dd.mm.yy' as written in the file. */\n startDateRaw: string;\n /** Raw 'hh.mm.ss' as written in the file. */\n startTimeRaw: string;\n /** Resolved start instant, or null when the file's date/time fields are unusable. */\n startDateTime: Date | null;\n /**\n * Header size in bytes, computed from the signal count rather than read from the field.\n *\n * Every data record offset is derived from this, so it has to be the one the layout\n * actually uses: 256 for the fixed header plus 256 per signal. A writer that fills the\n * field in carelessly is common enough to have its own warning, HEADER_BYTES_MISMATCH,\n * and trusting the field over the arithmetic would put every sample at the wrong offset.\n */\n headerBytes: number;\n /**\n * What the header's own length field says, which need not be the above.\n *\n * Exposed for the same reason `declaredRecordCount` is: the two disagreeing is a fact\n * about the file, and a caller checking how a recording was written should be able to see\n * what it claimed rather than only what was believed.\n */\n declaredHeaderBytes: number;\n reserved: string;\n isEdfPlus: boolean;\n /** True for BioSemi BDF/BDF+ files, whose samples are 3 bytes rather than 2. */\n isBdf: boolean;\n /** 'EDF+C' continuous, 'EDF+D' discontinuous, or null for plain EDF. */\n continuity: 'EDF+C' | 'EDF+D' | null;\n /** As declared in the header. -1 means \"unknown\", which the spec permits. */\n declaredRecordCount: number;\n recordDuration: number;\n signalCount: number;\n signals: EdfSignal[];\n bytesPerSample: number;\n recordBytes: number;\n}\n\n/** Everything derived by combining the header with the file's real size. */\nexport interface EdfHeaderInfo {\n header: EdfHeader;\n /** Record count implied by the actual file size — the one we trust for reading. */\n recordCount: number;\n /** Bytes after the last complete data record. */\n trailingBytes: number;\n diagnostics: Diagnostic[];\n}\n\n/**\n * A byte the terminal treats as an instruction rather than as text.\n *\n * C0 and C1, plus DEL. Tab is included deliberately: it is harmless to a terminal but it\n * makes a CSV column name that cannot be typed or matched reliably, which is the other half\n * of what this warning is for.\n */\nfunction isControlCharacter(character: string): boolean {\n const code = character.codePointAt(0) as number;\n return code <= 0x1f || (code >= 0x7f && code <= 0x9f);\n}\n\nconst dec = (buf: Uint8Array, start: number, len: number): string =>\n decodeLatin1(buf, start, start + len);\n\n/** EDF fields are space-padded; trailing NULs also occur in files written by sloppy tools. */\nconst trimField = (s: string): string => s.replace(/[\\0\\s]+$/u, '').replace(/^\\s+/u, '');\n\n/**\n * How many signals the fixed header says there are, read exactly as `parseHeader` will.\n *\n * `EdfFile.open` needs this before it can know how much header to read, and it used to work\n * it out with its own `Number(...)` — which was NUL-tolerant but not comma-tolerant, unlike\n * every other numeric field here. A header written with a comma decimal separator, which\n * COMMA_DECIMAL exists to accept and which the documentation lists this field among, was\n * therefore never given its signal headers at all, and the file died on a message that\n * contradicted itself: \"needs a 768-byte header, but the file is only 848 bytes\".\n *\n * Sharing the parse is what keeps the two from disagreeing again about which files are\n * readable. Null means \"not a usable count\", and the caller reads no further header — the\n * real error then comes from `parseHeader`, which is the one place that decides.\n */\nexport function peekSignalCount(fixed: Uint8Array): number | null {\n const count = Number(normaliseNumberField(dec(fixed, 252, 4)).text);\n return Number.isInteger(count) && count > 0 ? count : null;\n}\n\n/** A numeric header field, trimmed and with a comma decimal separator turned into a dot. */\nfunction normaliseNumberField(raw: string): { text: string; sawComma: boolean } {\n const text = trimField(raw);\n // Some writers emit a comma decimal separator despite the spec requiring '.'.\n if (text.includes(',') && !text.includes('.')) {\n return { text: text.replace(',', '.'), sawComma: true };\n }\n return { text, sawComma: false };\n}\n\nfunction parseNumberField(\n raw: string,\n field: string,\n { integer = false, sawComma }: { integer?: boolean; sawComma?: { value: boolean } } = {},\n): number {\n const normalised = normaliseNumberField(raw);\n const text = normalised.text;\n if (normalised.sawComma && sawComma) sawComma.value = true;\n if (text === '') {\n throw new EdfError('BAD_HEADER_FIELD', `Header field \"${field}\" is empty.`);\n }\n const n = Number(text);\n if (!Number.isFinite(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" is not a number (found ${JSON.stringify(text)}).`,\n 'The file may be truncated, byte-shifted, or not an EDF file at all.',\n );\n }\n if (integer && !Number.isInteger(n)) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Header field \"${field}\" must be a whole number (found ${JSON.stringify(text)}).`,\n );\n }\n return n;\n}\n\n/**\n * EDF stores a two-digit year. The spec pins the century: 85-99 mean 1985-1999\n * and 00-84 mean 2000-2084. Files outside 1985-2084 cannot express their date.\n */\nfunction resolveStartDateTime(dateRaw: string, timeRaw: string): Date | null {\n const d = /^(\\d{2})[.\\-/](\\d{2})[.\\-/](\\d{2})$/u.exec(trimField(dateRaw));\n const t = /^(\\d{2})[.:\\-](\\d{2})[.:\\-](\\d{2})$/u.exec(trimField(timeRaw));\n if (!d || !t) return null;\n\n const dd = Number(d[1]);\n const mm = Number(d[2]);\n const yy = Number(d[3]);\n const hh = Number(t[1]);\n const mi = Number(t[2]);\n const ss = Number(t[3]);\n\n if (mm < 1 || mm > 12 || dd < 1 || dd > 31 || hh > 23 || mi > 59 || ss > 60) return null;\n\n const year = yy >= 85 ? 1900 + yy : 2000 + yy;\n const date = new Date(Date.UTC(year, mm - 1, dd, hh, mi, Math.min(ss, 59)));\n // Reject dates that rolled over, e.g. 31.02.\n if (date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd) return null;\n return date;\n}\n\n/**\n * Parse the fixed 256-byte header plus the per-signal header block.\n *\n * @param buf At least FIXED_HEADER_BYTES + ns * SIGNAL_HEADER_BYTES bytes.\n * @param fileSize Total size of the file on disk, used to derive the real record count.\n */\nexport function parseHeader(buf: Uint8Array, fileSize: number): EdfHeaderInfo {\n const diagnostics: Diagnostic[] = [];\n const sawComma = { value: false };\n\n if (buf.length < FIXED_HEADER_BYTES) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n `File is ${fileSize} bytes; an EDF header alone needs at least ${FIXED_HEADER_BYTES}.`,\n );\n }\n\n // BDF (BioSemi) marks itself with byte 255 followed by 'BIOSEMI', and stores\n // 3-byte samples instead of 2. Everything else about the layout is identical.\n const isBdf = buf[0] === 0xff && dec(buf, 1, 7) === 'BIOSEMI';\n const version = isBdf ? 'BIOSEMI' : trimField(dec(buf, 0, 8));\n\n const patientId = trimField(dec(buf, 8, 80));\n const recordingId = trimField(dec(buf, 88, 80));\n const startDateRaw = trimField(dec(buf, 168, 8));\n const startTimeRaw = trimField(dec(buf, 176, 8));\n const headerBytes = parseNumberField(dec(buf, 184, 8), 'number of header bytes', {\n integer: true,\n sawComma,\n });\n const reserved = trimField(dec(buf, 192, 44));\n const declaredRecordCount = parseNumberField(dec(buf, 236, 8), 'number of data records', {\n integer: true,\n sawComma,\n });\n const recordDuration = parseNumberField(dec(buf, 244, 8), 'duration of a data record', {\n sawComma,\n });\n const signalCount = parseNumberField(dec(buf, 252, 4), 'number of signals', {\n integer: true,\n sawComma,\n });\n\n if (signalCount <= 0) {\n throw new EdfError(\n 'INVALID_SIGNAL_COUNT',\n `Header declares ${signalCount} signals; expected at least 1.`,\n );\n }\n if (!(recordDuration > 0)) {\n throw new EdfError(\n 'INVALID_RECORD_DURATION',\n `Header declares a data record duration of ${recordDuration}s; expected a positive number.`,\n );\n }\n\n const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;\n if (buf.length < expectedHeaderBytes) {\n throw new EdfError(\n 'FILE_TOO_SMALL',\n /*\n Which of the two is actually short.\n\n The file size was quoted either way, so a caller that had read too little — the\n signal count parsed one way here and another way there — produced arithmetic that\n refuted itself: \"needs a 768-byte header, but the file is only 848 bytes\". A reader\n following that looks for a truncation that is not there.\n */\n `File declares ${signalCount} signals, which needs a ${expectedHeaderBytes}-byte header, ` +\n (fileSize < expectedHeaderBytes\n ? `but the file is only ${fileSize} bytes.`\n : `but only ${buf.length} bytes of it were handed to the parser.`),\n );\n }\n if (headerBytes !== expectedHeaderBytes) {\n diagnostics.push({\n code: 'HEADER_BYTES_MISMATCH',\n severity: 'warning',\n message:\n `Header says it is ${headerBytes} bytes, but ${signalCount} signals require ` +\n `${expectedHeaderBytes} bytes. Using the value computed from the signal count.`,\n });\n }\n\n // Signal headers are field-major: all labels, then all transducers, and so on.\n const base = FIXED_HEADER_BYTES;\n const readField = (offsetUnits: number, width: number, i: number): string =>\n dec(buf, base + offsetUnits * signalCount + i * width, width);\n\n // EDF+ writes 'EDF+C'/'EDF+D' here; BDF+ writes 'BDF+C'/'BDF+D'. The two mean the\n // same thing, so both are normalised to a single continuity marker.\n const continuityTag = /^(?:EDF|BDF)\\+([CD])/u.exec(reserved);\n const continuity: 'EDF+C' | 'EDF+D' | null =\n continuityTag === null ? null : continuityTag[1] === 'D' ? 'EDF+D' : 'EDF+C';\n\n const signals: EdfSignal[] = [];\n let byteOffsetInRecord = 0;\n const bytesPerSample = isBdf ? 3 : 2;\n const seenLabels = new Map<string, number[]>();\n const emptyLabels: number[] = [];\n\n for (let i = 0; i < signalCount; i++) {\n const label = trimField(readField(0, 16, i));\n const transducer = trimField(readField(16, 80, i));\n const physicalDimension = trimField(readField(96, 8, i));\n const physicalMin = parseNumberField(readField(104, 8, i), `physical minimum (signal ${i})`, {\n sawComma,\n });\n const physicalMax = parseNumberField(readField(112, 8, i), `physical maximum (signal ${i})`, {\n sawComma,\n });\n const digitalMin = parseNumberField(readField(120, 8, i), `digital minimum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const digitalMax = parseNumberField(readField(128, 8, i), `digital maximum (signal ${i})`, {\n integer: true,\n sawComma,\n });\n const prefiltering = trimField(readField(136, 80, i));\n const samplesPerRecord = parseNumberField(\n readField(216, 8, i),\n `samples per record (signal ${i})`,\n { integer: true, sawComma },\n );\n const sigReserved = trimField(readField(224, 32, i));\n\n if (samplesPerRecord < 0) {\n throw new EdfError(\n 'BAD_HEADER_FIELD',\n `Signal ${i} (\"${label}\") declares ${samplesPerRecord} samples per record.`,\n );\n }\n\n const isAnnotations = label === ANNOTATIONS_LABEL || label === BDF_ANNOTATIONS_LABEL;\n\n signals.push({\n index: i,\n label,\n transducer,\n physicalDimension,\n physicalMin,\n physicalMax,\n digitalMin,\n digitalMax,\n prefiltering,\n samplesPerRecord,\n reserved: sigReserved,\n isAnnotations,\n samplingRate: samplesPerRecord / recordDuration,\n byteOffsetInRecord,\n });\n byteOffsetInRecord += samplesPerRecord * bytesPerSample;\n\n if (!isAnnotations) {\n /*\n A label is free text out of the file, and it becomes a column name in signals.csv.\n\n `--info` has escaped control bytes since it was written, because an ANSI escape in a\n header can drive the reader's terminal — `\\x1b[2J` clears the screen. The CSV had no\n such protection and needed none for correctness: quoting makes any byte safe for a\n parser, and this still passes the label through exactly as the file gives it, because\n losing what the header says is not an improvement.\n\n What was missing is the sentence saying so. A recording whose channel is labelled\n `\\x1b[2Jgone` converted with no warning at all, and `cat signals.csv` then cleared\n the terminal — while a script referencing that column by name carried an invisible\n control character in it. NONPRINTABLE_LABEL has been declared and documented as\n reserved since 0.1; this is it doing its job.\n */\n /*\n Which of the two fields carries them, because the consequences are not the same.\n\n The message said \"label or unit\", and then said the bytes \"will appear in the CSV\n column name\" and that \"the name cannot be typed\" — both of which are about the label.\n A channel labelled plainly `ECG` in a unit of `u\\x07V` got all of it: its column is\n `ECG`, `--channels ECG` selects it and exits 0, and the byte is in channels.csv's\n `unit` cell, which the warning never mentioned. Three sentences, none of them true of\n the file that raised it, on a warning whose whole purpose is to say where an invisible\n byte went.\n */\n const inLabel = [...label].filter(isControlCharacter);\n const inUnit = [...physicalDimension].filter(isControlCharacter);\n const control = [...inLabel, ...inUnit];\n if (control.length > 0) {\n const shown = [...new Set(control)]\n .map((c) => `\\\\x${(c.codePointAt(0) as number).toString(16).padStart(2, '0')}`)\n .join(', ');\n const plural = control.length === 1 ? '' : 's';\n const both = inLabel.length > 0 && inUnit.length > 0;\n const field = both ? 'label and unit contain' : inLabel.length > 0 ? 'label contains' : 'unit contains';\n // Where they land, which is the question the reader has. A label becomes a column\n // name in signals.csv; a unit is a cell of channels.csv and nothing else.\n const lands = both\n ? 'which will appear in the CSV column name and in channels.csv\\'s unit cell'\n : inLabel.length > 0\n ? 'which will appear in the CSV column name'\n : 'which will appear in channels.csv\\'s unit cell';\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `Signal ${i}'s ${field} ${control.length} control character${plural} ` +\n `(${shown}), ${lands} exactly as the header has them.`,\n hint:\n (inLabel.length > 0\n ? `Address the channel by position with --channels \"#${i}\" rather than by name, ` +\n 'since the name cannot be typed. '\n : `The column name is unaffected, so --channels \"${label}\" still selects it. `) +\n 'Printing the CSV to a terminal may do more than print it.',\n });\n }\n\n if (label === '') {\n // Collected, not reported here: what this channel's column ends up called depends on\n // whether some later channel is literally labelled `signal_<i>`, and inside this loop\n // the later channels do not exist yet. See the pass below.\n emptyLabels.push(i);\n } else {\n // Collected rather than reported here: a label repeated five times should\n // produce one warning naming all five, not four near-identical pairs.\n const seen = seenLabels.get(label);\n if (seen) seen.push(i);\n else seenLabels.set(label, [i]);\n }\n\n if (samplesPerRecord === 0) {\n diagnostics.push({\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") carries no samples at all (0 per data record).`,\n hint: 'It is described in channels.csv but left out of the converted data.',\n });\n }\n\n /*\n Too large to represent, and too small — the second was silent.\n\n The gain is the span divided by the digital range, and a span of 2e-320 over 65,535\n codes is 3e-325: below the smallest subnormal double, so it underflows to +0. The\n scaler's flat-range branch then handed every code the same physical value, and a\n channel of 65,536 distinct readings became one repeated number with nothing raised at\n all. One power of ten away, at 1e-319, the same file raises VALUE_RESOLUTION.\n\n Both are the same fact about the header — the span cannot be turned into a mapping —\n so both get this code, and both leave the cells empty rather than filling them with a\n value the header cannot justify.\n */\n const span = physicalMax - physicalMin;\n const underflowed = span !== 0 && span / (digitalMax - digitalMin) === 0;\n if (!Number.isFinite(span) || underflowed) {\n diagnostics.push({\n code: 'UNUSABLE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") declares a physical range from ${physicalMin} to ` +\n `${physicalMax}, whose span is too ${underflowed ? 'small' : 'large'} to ` +\n `represent, so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (digitalMax === digitalMin) {\n diagnostics.push({\n code: 'DEGENERATE_DIGITAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has digital minimum equal to digital maximum ` +\n `(${digitalMin}), so its values cannot be scaled.`,\n hint: 'Its cells are left empty rather than filled with a value the header cannot justify.',\n });\n } else if (physicalMax === physicalMin) {\n diagnostics.push({\n code: 'DEGENERATE_PHYSICAL_RANGE',\n severity: 'warning',\n message:\n `Signal ${i} (\"${label}\") has physical minimum equal to physical maximum ` +\n `(${physicalMin}), so every sample converts to the same value.`,\n });\n } else if ((physicalMax - physicalMin) * (digitalMax - digitalMin) < 0) {\n /*\n Polarity is inverted when the gain is negative, and the gain is\n (physicalMax - physicalMin) / (digitalMax - digitalMin) — so it is the sign of the\n two spans together that matters, not the physical pair alone.\n\n Testing only `physicalMax < physicalMin` was wrong in both directions. A file with\n its DIGITAL bounds reversed is just as inverted and drew no warning at all, handing\n back sign-flipped EEG with nothing to indicate it. A file with BOTH pairs reversed\n has a positive gain and is not inverted, yet was warned about — a message that was\n simply untrue of that recording.\n */\n const reversed =\n physicalMax < physicalMin\n ? `physical minimum ${physicalMin} above physical maximum ${physicalMax}`\n : `digital minimum ${digitalMin} above digital maximum ${digitalMax}`;\n diagnostics.push({\n code: 'INVERTED_PHYSICAL_RANGE',\n severity: 'warning',\n message: `Signal ${i} (\"${label}\") declares ${reversed}, which inverts its polarity.`,\n hint: 'The values are converted exactly as the header specifies, inversion included.',\n });\n }\n }\n }\n\n /*\n What an unlabelled channel is actually called, which the message used to guess.\n\n A channel with no label takes `signal_<index>` — unless another channel is literally\n labelled that, which EDF permits, since labels are free text and nothing enforces anything\n about them. Then both collide and both are suffixed. The warning said \"It will appear as\n \"signal_0\"\" while the file's header read `time_s,signal_0_ch0,signal_0_ch1`: the one\n sentence the run printed named a column that exists in neither signals.csv nor\n channels.csv.\n\n The other half was silent. The channel that genuinely carries the label `signal_0` lost\n its own column name to a collision with a synthesised one, and nothing said so —\n DUPLICATE_LABEL did not fire, because the two labels are not the same label. Both halves\n are one sentence here, because they are one event.\n\n No specific suffixed name is quoted. The suffix rule has a second pass for names that are\n still shared afterwards, and a message that hard-coded `_ch<index>` would be guessing again\n in exactly the way this is fixing.\n */\n for (const index of emptyLabels) {\n const taken = seenLabels.get(`signal_${index}`);\n diagnostics.push({\n code: 'EMPTY_LABEL',\n severity: 'warning',\n message:\n taken === undefined\n ? `Signal ${index} has no label. It will appear as \"signal_${index}\".`\n : `Signal ${index} has no label, so it takes the name \"signal_${index}\" — which ` +\n `${taken.length === 1 ? 'signal' : 'signals'} ${listed(taken.map(String))} already ` +\n `${taken.length === 1 ? 'carries' : 'carry'} as a label, so both columns are ` +\n `suffixed with their position instead.`,\n });\n }\n\n for (const [label, indices] of seenLabels) {\n if (indices.length < 2) continue;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `${indices.length} signals share the label \"${label}\" (positions ${indices.join(', ')}).`,\n hint: 'Their columns are suffixed with the signal number so they stay distinguishable.',\n });\n }\n\n const recordBytes = byteOffsetInRecord;\n if (recordBytes <= 0) {\n throw new EdfError(\n 'NO_SAMPLES',\n 'No signal in this file carries any samples (every channel declares 0 samples per record).',\n );\n }\n\n if (sawComma.value) {\n diagnostics.push({\n code: 'COMMA_DECIMAL',\n severity: 'warning',\n message: 'Some header numbers use a comma decimal separator, which the EDF spec does not allow.',\n hint: 'They were read as decimal points. Check the values in the channel table.',\n });\n }\n\n const dataBytes = fileSize - expectedHeaderBytes;\n if (dataBytes < 0) {\n throw new EdfError('FILE_TOO_SMALL', `File is smaller than its own header.`);\n }\n const recordCount = Math.floor(dataBytes / recordBytes);\n const trailingBytes = dataBytes - recordCount * recordBytes;\n\n if (recordCount === 0) {\n throw new EdfError(\n 'NO_DATA_RECORDS',\n 'The file contains a header but no complete data record.',\n 'The recording was probably interrupted before any data was written.',\n );\n }\n\n if (declaredRecordCount === -1) {\n diagnostics.push({\n code: 'RECORD_COUNT_UNKNOWN',\n severity: 'warning',\n message:\n `The header does not say how many data records the file has (-1), which the spec allows ` +\n `for recordings still in progress. Using the ${counted(recordCount, 'record')} the file actually contains.`,\n });\n } else if (declaredRecordCount !== recordCount) {\n diagnostics.push({\n code: 'RECORD_COUNT_MISMATCH',\n severity: 'warning',\n message:\n `The header declares ${declaredRecordCount} data records but the file contains ` +\n `${recordCount}. Converting the ${counted(recordCount, 'record')} that ${recordCount === 1 ? 'is' : 'are'} present.`,\n hint:\n declaredRecordCount > recordCount\n ? 'The recording looks truncated. It may have been cut short or copied incompletely.'\n : 'The file is longer than its header claims.',\n });\n }\n\n if (trailingBytes > 0) {\n diagnostics.push({\n code: 'TRAILING_BYTES',\n severity: 'warning',\n message: `${counted(trailingBytes, 'byte')} after the last complete data record ${trailingBytes === 1 ? 'was' : 'were'} ignored.`,\n });\n }\n\n const isEdfPlus = continuity !== null;\n if (continuity === 'EDF+D') {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This is a discontinuous (${isBdf ? 'BDF+D' : 'EDF+D'}) recording: its data records are ` +\n `not contiguous in time.`,\n hint: 'Each row carries its true recording time, so gaps stay visible instead of being closed.',\n });\n }\n\n const dataSignals = signals.filter((s) => !s.isAnnotations);\n if (dataSignals.length === 0) {\n diagnostics.push({\n code: 'NO_SIGNAL_CHANNELS',\n severity: 'warning',\n message: 'This file has no signal channels; it contains only EDF+ annotations.',\n });\n }\n\n // A channel declaring zero samples per record has no sampling rate to speak of — it is\n // reported separately as NO_SAMPLES and no file is written for it. Counting its nominal\n // 0 Hz as a rate made a single-rate recording warn that it used \"2 different sampling\n // rates (4 Hz, 0 Hz)\" and claim it was splitting output it never split.\n const rates = new Set(dataSignals.filter((s) => s.samplesPerRecord > 0).map((s) => s.samplingRate));\n if (rates.size > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${rates.size} different sampling rates ` +\n `(${listed(formatRates([...rates].sort((a, b) => b - a)).map((r) => `${r} Hz`))}).`,\n hint: 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n return {\n header: {\n version,\n patientId,\n recordingId,\n startDateRaw,\n startTimeRaw,\n startDateTime: resolveStartDateTime(startDateRaw, startTimeRaw),\n headerBytes: expectedHeaderBytes,\n declaredHeaderBytes: headerBytes,\n reserved,\n isEdfPlus,\n isBdf,\n continuity,\n declaredRecordCount,\n recordDuration,\n signalCount,\n signals,\n bytesPerSample,\n recordBytes,\n },\n recordCount,\n trailingBytes,\n diagnostics,\n };\n}\n\n/**\n * The recording start as a zone-less wall clock, \"YYYY-MM-DDTHH:MM:SS\".\n *\n * EDF stores the start time as local wall-clock digits with no timezone anywhere in\n * the format. `startDateTime` is built with Date.UTC purely so those digits survive a\n * round trip unshifted, which makes it a carrier for the wall clock rather than a\n * real instant. Serialising it with `toISOString()` would append a Z and assert UTC,\n * and any reader converting to local time would then shift the recording by their own\n * offset: 13:43:04 in the file becomes 08:43:04 in New York. The Z is omitted because\n * the file genuinely does not say which zone it meant.\n */\nexport function formatWallClock(date: Date | null): string | null {\n if (!date) return null;\n return date.toISOString().slice(0, 19);\n}\n\n/** \"EDF\", \"EDF+ (EDF+D)\", \"BDF\", \"BDF+ (EDF+C)\". */\nexport function describeFormat(header: EdfHeader): string {\n const base = header.isBdf ? 'BDF' : 'EDF';\n if (!header.isEdfPlus) return base;\n return `${base}+ (${header.continuity === 'EDF+D' ? 'discontinuous' : 'continuous'})`;\n}\n\n/** Render a sampling rate without trailing noise: 256, 0.5, 12.5. */\nexport function formatRate(hz: number): string {\n if (Number.isInteger(hz)) return String(hz);\n const rounded = Number(hz.toFixed(6));\n // A rate below 5e-7 rounds away to \"0\", which reads as \"this channel has no sampling\n // rate\" and made the mixed-rate warning contradict itself: it announced two different\n // rates and then printed both as \"0 Hz\". Exponent form keeps a real rate legible, and\n // keeps distinct rates distinct in the channel table and in output filenames.\n if (rounded === 0) return hz.toExponential(3);\n return String(rounded);\n}\n\n/**\n * Renders a group of rates so that rates which differ read as differing.\n *\n * `formatRate` rounds to six decimals, which is what keeps an ordinary rate free of\n * float noise — 30 samples in a 0.1-second record is 299.99999999999994 as a double,\n * and belongs on screen as 300. Two rates separated by less than that round to one\n * string, so a file carrying 1e-6 Hz and 1.25e-6 Hz warned that it used \"2 different\n * sampling rates (0.000001 Hz, 0.000001 Hz)\" and named both files the same thing.\n *\n * That is the contradiction the exponent fallback above already removes for rates that\n * round away to zero; this is the same one a step further out. On a collision every rate\n * in the group switches to its shortest exact form, which is unique for distinct values,\n * rather than only the pair that collided — one column in one notation reads better than\n * two.\n */\nexport function formatRates(rates: readonly number[]): string[] {\n const rounded = rates.map(formatRate);\n const distinct = new Set(rates).size;\n return new Set(rounded).size === distinct ? rounded : rates.map((hz) => String(hz));\n}\n"]}
package/dist/edf/scale.js CHANGED
@@ -37,10 +37,24 @@ export function makeScaler(signal) {
37
37
  if (digitalMax === digitalMin)
38
38
  return () => NaN;
39
39
  const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);
40
- // A flat physical range makes every sample the same value, and would divide by
41
- // zero in the offset below. That mapping is defined, so its constant is written.
40
+ /*
41
+ A flat physical range makes every sample the same value, and would divide by zero in the
42
+ offset below. That mapping is defined, so its constant is written.
43
+
44
+ A gain of zero does not always mean flat, and this could not tell the difference. A range
45
+ of -1e-320 to 1e-320 is not flat — it is 65,536 distinct physical values — but the gain
46
+ is 2e-320/65535, which is smaller than the smallest subnormal double and underflows to
47
+ +0. Every distinct sample then took `physicalMin`, so eight codes spanning -16,000 to
48
+ +12,000 came out as one repeated number, with no diagnostic anywhere and `--strict`
49
+ exiting 0. At 1e-319, one power of ten away, the same file raises VALUE_RESOLUTION.
50
+
51
+ That is the same situation as the overflow below it, which this codebase already reasoned
52
+ about and answered: the span cannot be represented, so there is no mapping, so the cells
53
+ are left empty rather than filled with a value the header cannot justify. Underflow only
54
+ got the flat-range treatment because `gain === 0` is what both look like from here.
55
+ */
42
56
  if (gain === 0)
43
- return () => physicalMin;
57
+ return physicalMax === physicalMin ? () => physicalMin : () => NaN;
44
58
  // A non-finite gain is a different thing: the physical span overflowed a double, so
45
59
  // there is no mapping at all. Returning physicalMin filled the column with one enormous
46
60
  // constant — every distinct sample rendered as the same 300-digit number — and raised
@@ -1 +1 @@
1
- {"version":3,"file":"scale.js","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Digital-to-physical conversion.\n *\n * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)\n * and (digitalMax -> physicalMax), which the specification writes as:\n *\n * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)\n * physical = (digital - digitalMin) * gain + physicalMin\n *\n * That form is evaluated here in EDFlib's algebraically equivalent arrangement:\n *\n * offset = physicalMax / gain - digitalMax\n * physical = gain * (offset + digital)\n *\n * The rearrangement is not cosmetic. Written the first way, a channel spanning\n * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation\n * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact\n * value is 0.19536019536019536. EDFlib's form keeps the intermediate small\n * (offset + digital = 0.5 here) and returns the correctly rounded result.\n *\n * Both properties matter. The values are as accurate as a double can express, and\n * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,\n * so the test suite can assert exact equality against a reference implementation\n * rather than settling for a tolerance.\n */\n\nimport type { EdfSignal } from './header.js';\n\nexport type Scaler = (digital: number) => number;\n\nexport function makeScaler(signal: EdfSignal): Scaler {\n const { digitalMin, digitalMax, physicalMin, physicalMax } = signal;\n\n // A zero digital span leaves the mapping undefined — the header contradicts itself,\n // so there is no physical value for any sample on this channel.\n //\n // NaN rather than a stand-in number. Writing the physical minimum produces a column\n // of plausible readings (\"-100.000\" repeated) that is indistinguishable from a real\n // flat recording once the CSV is opened somewhere else, which is exactly the kind of\n // invented data this tool exists to avoid. NaN carries through to an empty CSV cell\n // and reads back as NaN in pandas, matching how a missing annotation duration is\n // already written. DEGENERATE_DIGITAL_RANGE is raised alongside it.\n if (digitalMax === digitalMin) return () => NaN;\n\n const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);\n\n // A flat physical range makes every sample the same value, and would divide by\n // zero in the offset below. That mapping is defined, so its constant is written.\n if (gain === 0) return () => physicalMin;\n\n // A non-finite gain is a different thing: the physical span overflowed a double, so\n // there is no mapping at all. Returning physicalMin filled the column with one enormous\n // constant — every distinct sample rendered as the same 300-digit number — and raised\n // nothing. NaN takes the same route as a degenerate digital range: empty cells, plus\n // UNUSABLE_PHYSICAL_RANGE from the header parser.\n if (!Number.isFinite(gain)) return () => NaN;\n\n // Deriving the offset divides by the gain. For every realistic calibration that is\n // both safe and more accurate, but an absurd header (a huge physical range over a\n // near-zero gain) could overflow it, so fall back to the specification's own\n // arrangement rather than emitting Infinity.\n const offset = physicalMax / gain - digitalMax;\n if (!Number.isFinite(offset)) {\n return (digital: number): number => (digital - digitalMin) * gain + physicalMin;\n }\n\n return (digital: number): number => gain * (offset + digital);\n}\n\n/**\n * Smallest physical step this channel can express — one digital unit.\n * Used to choose a decimal precision that preserves every distinct sample value.\n */\nexport function quantizationStep(signal: EdfSignal): number {\n const digitalSpan = signal.digitalMax - signal.digitalMin;\n if (digitalSpan === 0) return 0;\n return Math.abs((signal.physicalMax - signal.physicalMin) / digitalSpan);\n}\n\n/**\n * The most `toFixed` accepts. 101 is a RangeError, so this is the ceiling, not a taste.\n *\n * It used to be 20, on the stated grounds that 20 was what `toFixed` allowed. It is not,\n * and the gap was not academic: a magnetometer channel spanning ±1e-16 T over a 16-bit\n * converter has a step of 3.05e-21 and needs 23 places. Clamped to 20, every value landed\n * on a 1e-20 grid — about three digital codes to a printed value — so 69% of the samples\n * could not be recovered, the conversion exited 0, and nothing said a word. The channel\n * type the old comment named as the reason for the ceiling was the one it broke.\n */\nconst MAX_DERIVED_DECIMALS = 100;\n\n/**\n * Decimal places needed so that two adjacent digital codes never round to the same\n * string. Two places past the quantization step keep rounding error far below the\n * resolution the hardware actually recorded, without padding the file with digits\n * that carry no information.\n *\n * Ordinary channels land at three or four: a ±800 µV channel over 12 bits steps by\n * 0.39 µV and needs three. The ceiling is only reached by calibrations whose step is\n * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five\n * characters. Those get VALUE_RESOLUTION rather than silence.\n */\nexport function decimalsForSignal(signal: EdfSignal, max = MAX_DERIVED_DECIMALS): number {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return 3;\n const needed = Math.ceil(-Math.log10(step)) + 2;\n return Math.min(max, Math.max(0, needed));\n}\n\n/**\n * Whether this channel's step is finer than any precision the tool can print.\n *\n * Asked of the ceiling, not of the precision in use. `--decimals 2` on a channel needing 3\n * is a trade the caller made knowingly and is not this warning's business — 0.5.10 fixed a\n * version of this that fired on every ordinary EEG at `--decimals 2` and made\n * `--decimals 2 --strict` impossible. But it fixed it by asking \"did the caller choose the\n * precision\", which suppressed the real case too: at `--decimals 20` a channel stepping by\n * 1e-106 printed every one of its codes as `0.00000000000000000000`, in silence.\n *\n * The question is whether anything the tool can print would separate consecutive codes. When\n * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.\n */\nexport function decimalsAreClamped(signal: EdfSignal): boolean {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return false;\n return Math.ceil(-Math.log10(step)) + 2 > MAX_DERIVED_DECIMALS;\n}\n"]}
1
+ {"version":3,"file":"scale.js","sourceRoot":"","sources":["../../src/edf/scale.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Digital-to-physical conversion.\n *\n * EDF defines the mapping by two calibration points, (digitalMin -> physicalMin)\n * and (digitalMax -> physicalMax), which the specification writes as:\n *\n * gain = (physicalMax - physicalMin) / (digitalMax - digitalMin)\n * physical = (digital - digitalMin) * gain + physicalMin\n *\n * That form is evaluated here in EDFlib's algebraically equivalent arrangement:\n *\n * offset = physicalMax / gain - digitalMax\n * physical = gain * (offset + digital)\n *\n * The rearrangement is not cosmetic. Written the first way, a channel spanning\n * +/-800 uV computes a value near 800 and then subtracts 800, and the cancellation\n * throws away low-order bits: digital 0 yields 0.19536019536019467 when the exact\n * value is 0.19536019536019536. EDFlib's form keeps the intermediate small\n * (offset + digital = 0.5 here) and returns the correctly rounded result.\n *\n * Both properties matter. The values are as accurate as a double can express, and\n * they are bit-identical to pyEDFlib and EDFbrowser, which share EDFlib's arithmetic,\n * so the test suite can assert exact equality against a reference implementation\n * rather than settling for a tolerance.\n */\n\nimport type { EdfSignal } from './header.js';\n\nexport type Scaler = (digital: number) => number;\n\nexport function makeScaler(signal: EdfSignal): Scaler {\n const { digitalMin, digitalMax, physicalMin, physicalMax } = signal;\n\n // A zero digital span leaves the mapping undefined — the header contradicts itself,\n // so there is no physical value for any sample on this channel.\n //\n // NaN rather than a stand-in number. Writing the physical minimum produces a column\n // of plausible readings (\"-100.000\" repeated) that is indistinguishable from a real\n // flat recording once the CSV is opened somewhere else, which is exactly the kind of\n // invented data this tool exists to avoid. NaN carries through to an empty CSV cell\n // and reads back as NaN in pandas, matching how a missing annotation duration is\n // already written. DEGENERATE_DIGITAL_RANGE is raised alongside it.\n if (digitalMax === digitalMin) return () => NaN;\n\n const gain = (physicalMax - physicalMin) / (digitalMax - digitalMin);\n\n /*\n A flat physical range makes every sample the same value, and would divide by zero in the\n offset below. That mapping is defined, so its constant is written.\n\n A gain of zero does not always mean flat, and this could not tell the difference. A range\n of -1e-320 to 1e-320 is not flat — it is 65,536 distinct physical values — but the gain\n is 2e-320/65535, which is smaller than the smallest subnormal double and underflows to\n +0. Every distinct sample then took `physicalMin`, so eight codes spanning -16,000 to\n +12,000 came out as one repeated number, with no diagnostic anywhere and `--strict`\n exiting 0. At 1e-319, one power of ten away, the same file raises VALUE_RESOLUTION.\n\n That is the same situation as the overflow below it, which this codebase already reasoned\n about and answered: the span cannot be represented, so there is no mapping, so the cells\n are left empty rather than filled with a value the header cannot justify. Underflow only\n got the flat-range treatment because `gain === 0` is what both look like from here.\n */\n if (gain === 0) return physicalMax === physicalMin ? (): number => physicalMin : (): number => NaN;\n\n // A non-finite gain is a different thing: the physical span overflowed a double, so\n // there is no mapping at all. Returning physicalMin filled the column with one enormous\n // constant — every distinct sample rendered as the same 300-digit number — and raised\n // nothing. NaN takes the same route as a degenerate digital range: empty cells, plus\n // UNUSABLE_PHYSICAL_RANGE from the header parser.\n if (!Number.isFinite(gain)) return () => NaN;\n\n // Deriving the offset divides by the gain. For every realistic calibration that is\n // both safe and more accurate, but an absurd header (a huge physical range over a\n // near-zero gain) could overflow it, so fall back to the specification's own\n // arrangement rather than emitting Infinity.\n const offset = physicalMax / gain - digitalMax;\n if (!Number.isFinite(offset)) {\n return (digital: number): number => (digital - digitalMin) * gain + physicalMin;\n }\n\n return (digital: number): number => gain * (offset + digital);\n}\n\n/**\n * Smallest physical step this channel can express — one digital unit.\n * Used to choose a decimal precision that preserves every distinct sample value.\n */\nexport function quantizationStep(signal: EdfSignal): number {\n const digitalSpan = signal.digitalMax - signal.digitalMin;\n if (digitalSpan === 0) return 0;\n return Math.abs((signal.physicalMax - signal.physicalMin) / digitalSpan);\n}\n\n/**\n * The most `toFixed` accepts. 101 is a RangeError, so this is the ceiling, not a taste.\n *\n * It used to be 20, on the stated grounds that 20 was what `toFixed` allowed. It is not,\n * and the gap was not academic: a magnetometer channel spanning ±1e-16 T over a 16-bit\n * converter has a step of 3.05e-21 and needs 23 places. Clamped to 20, every value landed\n * on a 1e-20 grid — about three digital codes to a printed value — so 69% of the samples\n * could not be recovered, the conversion exited 0, and nothing said a word. The channel\n * type the old comment named as the reason for the ceiling was the one it broke.\n */\nconst MAX_DERIVED_DECIMALS = 100;\n\n/**\n * Decimal places needed so that two adjacent digital codes never round to the same\n * string. Two places past the quantization step keep rounding error far below the\n * resolution the hardware actually recorded, without padding the file with digits\n * that carry no information.\n *\n * Ordinary channels land at three or four: a ±800 µV channel over 12 bits steps by\n * 0.39 µV and needs three. The ceiling is only reached by calibrations whose step is\n * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five\n * characters. Those get VALUE_RESOLUTION rather than silence.\n */\nexport function decimalsForSignal(signal: EdfSignal, max = MAX_DERIVED_DECIMALS): number {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return 3;\n const needed = Math.ceil(-Math.log10(step)) + 2;\n return Math.min(max, Math.max(0, needed));\n}\n\n/**\n * Whether this channel's step is finer than any precision the tool can print.\n *\n * Asked of the ceiling, not of the precision in use. `--decimals 2` on a channel needing 3\n * is a trade the caller made knowingly and is not this warning's business — 0.5.10 fixed a\n * version of this that fired on every ordinary EEG at `--decimals 2` and made\n * `--decimals 2 --strict` impossible. But it fixed it by asking \"did the caller choose the\n * precision\", which suppressed the real case too: at `--decimals 20` a channel stepping by\n * 1e-106 printed every one of its codes as `0.00000000000000000000`, in silence.\n *\n * The question is whether anything the tool can print would separate consecutive codes. When\n * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.\n */\nexport function decimalsAreClamped(signal: EdfSignal): boolean {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return false;\n return Math.ceil(-Math.log10(step)) + 2 > MAX_DERIVED_DECIMALS;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.5.82",
3
+ "version": "0.5.83",
4
4
  "description": "Convert EDF, EDF+ and BDF biosignal recordings (European Data Format) to CSV from the command line. Local, streaming, and never resamples or alters units.",
5
5
  "keywords": [
6
6
  "edf",