edf2csv 0.7.66 → 0.7.68

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.
@@ -31,17 +31,6 @@ export declare function makeScaler(signal: EdfSignal): Scaler;
31
31
  * Used to choose a decimal precision that preserves every distinct sample value.
32
32
  */
33
33
  export declare function quantizationStep(signal: EdfSignal): number;
34
- /**
35
- * Decimal places needed so that two adjacent digital codes never round to the same
36
- * string. Two places past the quantization step keep rounding error far below the
37
- * resolution the hardware actually recorded, without padding the file with digits
38
- * that carry no information.
39
- *
40
- * Ordinary channels land at three or four: a ±800 µV channel over 12 bits steps by
41
- * 0.39 µV and needs three. The ceiling is only reached by calibrations whose step is
42
- * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five
43
- * characters. Those get VALUE_RESOLUTION rather than silence.
44
- */
45
34
  export declare function decimalsForSignal(signal: EdfSignal, max?: number): number;
46
35
  /**
47
36
  * Whether this channel's step is finer than any precision the tool can print.
package/dist/edf/scale.js CHANGED
@@ -104,11 +104,27 @@ const MAX_DERIVED_DECIMALS = 100;
104
104
  * below 1e-98, which an 8-character physical bound can still express — `1e-99` is five
105
105
  * characters. Those get VALUE_RESOLUTION rather than silence.
106
106
  */
107
- export function decimalsForSignal(signal, max = MAX_DERIVED_DECIMALS) {
107
+ /**
108
+ * Places this channel needs before any ceiling, or null when it has no step to derive one from.
109
+ *
110
+ * One expression, because two functions depend on agreeing about it. `decimalsForSignal`
111
+ * computed `Math.ceil(-Math.log10(step)) + 2` and clamped it; `decimalsAreClamped` computed the
112
+ * same thing again and compared it to the same ceiling. Two copies of one formula whose only
113
+ * job is to give the same answer — change either `+ 2` and they part company at the boundary,
114
+ * so a channel whose precision really was capped is reported as not capped, VALUE_RESOLUTION is
115
+ * not raised, and its codes print indistinguishable in silence. Which is the exact thing that
116
+ * warning exists to say.
117
+ */
118
+ function decimalsNeeded(signal) {
108
119
  const step = quantizationStep(signal);
109
120
  if (!(step > 0) || !Number.isFinite(step))
121
+ return null;
122
+ return Math.ceil(-Math.log10(step)) + 2;
123
+ }
124
+ export function decimalsForSignal(signal, max = MAX_DERIVED_DECIMALS) {
125
+ const needed = decimalsNeeded(signal);
126
+ if (needed === null)
110
127
  return 3;
111
- const needed = Math.ceil(-Math.log10(step)) + 2;
112
128
  return Math.min(max, Math.max(0, needed));
113
129
  }
114
130
  /**
@@ -125,9 +141,7 @@ export function decimalsForSignal(signal, max = MAX_DERIVED_DECIMALS) {
125
141
  * the answer is no, that is a ceiling nobody chose, and it holds whatever `--decimals` says.
126
142
  */
127
143
  export function decimalsAreClamped(signal) {
128
- const step = quantizationStep(signal);
129
- if (!(step > 0) || !Number.isFinite(step))
130
- return false;
131
- return Math.ceil(-Math.log10(step)) + 2 > MAX_DERIVED_DECIMALS;
144
+ const needed = decimalsNeeded(signal);
145
+ return needed !== null && needed > MAX_DERIVED_DECIMALS;
132
146
  }
133
147
  //# sourceMappingURL=scale.js.map
@@ -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 /*\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"]}
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 */\n/**\n * Places this channel needs before any ceiling, or null when it has no step to derive one from.\n *\n * One expression, because two functions depend on agreeing about it. `decimalsForSignal`\n * computed `Math.ceil(-Math.log10(step)) + 2` and clamped it; `decimalsAreClamped` computed the\n * same thing again and compared it to the same ceiling. Two copies of one formula whose only\n * job is to give the same answer — change either `+ 2` and they part company at the boundary,\n * so a channel whose precision really was capped is reported as not capped, VALUE_RESOLUTION is\n * not raised, and its codes print indistinguishable in silence. Which is the exact thing that\n * warning exists to say.\n */\nfunction decimalsNeeded(signal: EdfSignal): number | null {\n const step = quantizationStep(signal);\n if (!(step > 0) || !Number.isFinite(step)) return null;\n return Math.ceil(-Math.log10(step)) + 2;\n}\n\nexport function decimalsForSignal(signal: EdfSignal, max = MAX_DERIVED_DECIMALS): number {\n const needed = decimalsNeeded(signal);\n if (needed === null) return 3;\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 needed = decimalsNeeded(signal);\n return needed !== null && needed > MAX_DERIVED_DECIMALS;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.7.66",
3
+ "version": "0.7.68",
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",