smithkit 0.4.0 → 0.6.0
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/README.md +47 -774
- package/dist/lib/OptionsValidation.js +7 -3
- package/dist/lib/Smith.d.ts +7 -1
- package/dist/lib/Smith.js +17 -9
- package/dist/lib/grid/SmithGridLayer.js +3 -1
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/io/Touchstone.d.ts +13 -7
- package/dist/lib/io/Touchstone.js +146 -46
- package/dist/lib/io/types.d.ts +17 -0
- package/dist/lib/io/types.js +1 -0
- package/dist/lib/layers.d.ts +4 -0
- package/dist/lib/math/Complex.d.ts +1 -1
- package/dist/lib/math/Complex.js +2 -2
- package/dist/lib/measurements.d.ts +2 -0
- package/dist/lib/options.d.ts +4 -0
- package/dist/lib/rendering/SvgChartRenderer.d.ts +3 -3
- package/dist/lib/rendering/SvgChartRenderer.js +28 -3
- package/dist/lib/rf/RfCalculations.d.ts +3 -1
- package/dist/lib/rf/RfCalculations.js +23 -6
- package/dist/lib/samples.d.ts +5 -1
- package/dist/lib/scales/SmithPeripheralScales.d.ts +5 -0
- package/dist/lib/scales/SmithPeripheralScales.js +20 -1
- package/dist/lib/scales/SmithScales.js +2 -2
- package/dist/lib/svg/LabelLayout.js +9 -2
- package/dist/lib/svg/SvgExporter.d.ts +1 -1
- package/dist/lib/svg/SvgExporter.js +4 -3
- package/dist/lib/svg/export.d.ts +2 -0
- package/dist/lib/traces/SmithData.d.ts +10 -4
- package/dist/lib/traces/SmithData.js +14 -4
- package/dist/lib/traces/SmithMarker.js +1 -0
- package/dist/lib/traces/TraceBuffer.d.ts +19 -0
- package/dist/lib/traces/TraceBuffer.js +176 -0
- package/dist/lib/traces/TraceLineDetail.d.ts +8 -0
- package/dist/lib/traces/TraceLineDetail.js +52 -0
- package/dist/lib/traces/TraceModel.d.ts +6 -8
- package/dist/lib/traces/TraceModel.js +24 -49
- package/dist/lib/traces/TraceRenderer.d.ts +13 -3
- package/dist/lib/traces/TraceRenderer.js +83 -23
- package/package.json +25 -6
|
@@ -96,9 +96,13 @@ export class OptionsValidation {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
if (options.peripheralScales !== undefined) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const keys = ['visible', 'captionsVisible', 'tickLabelsVisible'];
|
|
100
|
+
OptionsValidation.object(options.peripheralScales, 'Peripheral scales', keys);
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
const value = options.peripheralScales[key];
|
|
103
|
+
if (value !== undefined) {
|
|
104
|
+
OptionsValidation.boolean(value, `Peripheral scales ${key}`);
|
|
105
|
+
}
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
if (options.grid !== undefined) {
|
package/dist/lib/Smith.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SmithOptions } from './options.js';
|
|
2
2
|
import type { SmithImageExportOptions } from './svg/export.js';
|
|
3
3
|
import type { SmithAppearance } from './appearance/types.js';
|
|
4
|
-
import type { TraceSamples } from './samples.js';
|
|
4
|
+
import type { TraceInput, TraceSamples, TraceTuple } from './samples.js';
|
|
5
5
|
import type { ChartLayers, GridDetail, PeripheralScales } from './layers.js';
|
|
6
6
|
import type { SmithReading } from './rf/RfCalculations.js';
|
|
7
7
|
import type { TraceOptions, TraceUpdateOptions, TraceInfo, MarkerSnapshot, MarkerComparison } from './measurements.js';
|
|
@@ -62,6 +62,9 @@ export declare class Smith {
|
|
|
62
62
|
resetView(): void;
|
|
63
63
|
/** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
|
|
64
64
|
addTrace(values: TraceSamples, options?: TraceOptions): string;
|
|
65
|
+
addTrace(values: readonly TraceTuple[], options?: TraceOptions): string;
|
|
66
|
+
addTrace(values: Float64Array, options?: TraceOptions): string;
|
|
67
|
+
addTrace(values: TraceInput, options?: TraceOptions): string;
|
|
65
68
|
/** Detached metadata snapshots; IDs and marker display numbers survive removals. */
|
|
66
69
|
getTraces(): TraceInfo[];
|
|
67
70
|
private traceIndex;
|
|
@@ -71,6 +74,9 @@ export declare class Smith {
|
|
|
71
74
|
setTraceOptions(id: string, options: TraceOptions): boolean;
|
|
72
75
|
/** Replace samples; by default each marker follows its nearest measured frequency. */
|
|
73
76
|
updateTrace(id: string, values: TraceSamples, options?: TraceUpdateOptions): boolean;
|
|
77
|
+
updateTrace(id: string, values: readonly TraceTuple[], options?: TraceUpdateOptions): boolean;
|
|
78
|
+
updateTrace(id: string, values: Float64Array, options?: TraceUpdateOptions): boolean;
|
|
79
|
+
updateTrace(id: string, values: TraceInput, options?: TraceUpdateOptions): boolean;
|
|
74
80
|
removeTrace(id: string): boolean;
|
|
75
81
|
/** Place a marker on an existing sample; no interpolation is performed. */
|
|
76
82
|
addMarker(traceId: string, sampleIndex?: number): string | undefined;
|
package/dist/lib/Smith.js
CHANGED
|
@@ -80,6 +80,12 @@ export class Smith {
|
|
|
80
80
|
if (options.peripheralScales?.visible !== undefined) {
|
|
81
81
|
this.peripheralScales.setVisible(options.peripheralScales.visible);
|
|
82
82
|
}
|
|
83
|
+
if (options.peripheralScales?.captionsVisible !== undefined) {
|
|
84
|
+
this.peripheralScales.setCaptionsVisible(options.peripheralScales.captionsVisible);
|
|
85
|
+
}
|
|
86
|
+
if (options.peripheralScales?.tickLabelsVisible !== undefined) {
|
|
87
|
+
this.peripheralScales.setTickLabelsVisible(options.peripheralScales.tickLabelsVisible);
|
|
88
|
+
}
|
|
83
89
|
for (const name of ['resistance', 'reactance', 'conductance', 'susceptance']) {
|
|
84
90
|
const layer = this.layers[name];
|
|
85
91
|
for (const settings of [options.grid, options.grid?.layers?.[name]]) {
|
|
@@ -143,13 +149,10 @@ export class Smith {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
/** Export the current mounted view as standalone SVG with its configured background. */
|
|
146
|
-
toSvg(options) {
|
|
152
|
+
toSvg(options = {}) {
|
|
147
153
|
this.assertAlive();
|
|
148
154
|
this.validateExportOptions(options);
|
|
149
|
-
const source = this.renderer.toSvg();
|
|
150
|
-
if (options === undefined) {
|
|
151
|
-
return source;
|
|
152
|
-
}
|
|
155
|
+
const source = this.renderer.toSvg(options.lineDetail ?? 'full');
|
|
153
156
|
const sources = [source];
|
|
154
157
|
if (options.scales) {
|
|
155
158
|
sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
|
|
@@ -157,6 +160,9 @@ export class Smith {
|
|
|
157
160
|
return ImageExporter.svg(sources, options, this.exportLegend(options));
|
|
158
161
|
}
|
|
159
162
|
validateExportOptions(options) {
|
|
163
|
+
if (options?.lineDetail !== undefined && !['full', 'view'].includes(options.lineDetail)) {
|
|
164
|
+
throw new TypeError('Export line detail must be full or view.');
|
|
165
|
+
}
|
|
160
166
|
if (options?.scaleReadout !== undefined && !options.scales) {
|
|
161
167
|
throw new TypeError('Scale readout requires included radial scales.');
|
|
162
168
|
}
|
|
@@ -178,7 +184,7 @@ export class Smith {
|
|
|
178
184
|
async toPng(options = {}) {
|
|
179
185
|
this.assertAlive();
|
|
180
186
|
this.validateExportOptions(options);
|
|
181
|
-
const sources = [this.toSvg()];
|
|
187
|
+
const sources = [this.renderer.toSvg(options.lineDetail ?? 'full')];
|
|
182
188
|
if (options.scales) {
|
|
183
189
|
sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
|
|
184
190
|
}
|
|
@@ -231,7 +237,6 @@ export class Smith {
|
|
|
231
237
|
this.assertAlive();
|
|
232
238
|
this.renderer.resetView();
|
|
233
239
|
}
|
|
234
|
-
/** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
|
|
235
240
|
addTrace(values, options = {}) {
|
|
236
241
|
this.assertAlive();
|
|
237
242
|
this.validateTraceOptions(options);
|
|
@@ -285,6 +290,10 @@ export class Smith {
|
|
|
285
290
|
if (options.mode !== undefined && !['points', 'line', 'both'].includes(options.mode)) {
|
|
286
291
|
throw new TypeError('Trace mode must be points, line, or both.');
|
|
287
292
|
}
|
|
293
|
+
if (options.lineTolerancePx !== undefined &&
|
|
294
|
+
(!Number.isFinite(options.lineTolerancePx) || options.lineTolerancePx < 0)) {
|
|
295
|
+
throw new RangeError('Line tolerance must be finite and non-negative.');
|
|
296
|
+
}
|
|
288
297
|
for (const key of ['lineWidth', 'pointRadius']) {
|
|
289
298
|
const value = options[key];
|
|
290
299
|
if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
|
|
@@ -323,7 +332,6 @@ export class Smith {
|
|
|
323
332
|
}
|
|
324
333
|
return true;
|
|
325
334
|
}
|
|
326
|
-
/** Replace samples; by default each marker follows its nearest measured frequency. */
|
|
327
335
|
updateTrace(id, values, options = {}) {
|
|
328
336
|
this.assertAlive();
|
|
329
337
|
const data = this.data[this.traceIndex(id)];
|
|
@@ -419,7 +427,7 @@ export class Smith {
|
|
|
419
427
|
if (referenceImpedanceOhms === this.referenceOhms) {
|
|
420
428
|
return;
|
|
421
429
|
}
|
|
422
|
-
const samples = this.data.map((data) =>
|
|
430
|
+
const samples = this.data.map((data) => data.Samples.renormalize(this.referenceOhms, referenceImpedanceOhms));
|
|
423
431
|
this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
|
|
424
432
|
this.referenceOhms = referenceImpedanceOhms;
|
|
425
433
|
this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
|
|
@@ -31,7 +31,9 @@ export class SmithGridLayer extends ConstCircles {
|
|
|
31
31
|
const label = GridGeometry.label(kind, tick.definition);
|
|
32
32
|
const text = new SmithText(this.scaler.point(label.point), label.text, {
|
|
33
33
|
rotate: label.rotate,
|
|
34
|
-
|
|
34
|
+
// Keep the zero clear of the opposite grid's high-value labels even
|
|
35
|
+
// when minimum font sizing or browser font metrics enlarge it.
|
|
36
|
+
dx: real && tick.definition.point.r === 0 ? '0.9em' : this.scaler.r(label.dx).toString(),
|
|
35
37
|
dy: this.scaler.r(label.dy).toString(),
|
|
36
38
|
textAnchor: label.textAnchor,
|
|
37
39
|
dominantBaseline: label.dominantBaseline,
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { Smith, SmithEventType } from './Smith.js';
|
|
2
2
|
export type { SmithEvent } from './Smith.js';
|
|
3
3
|
export { Complex } from './math/Complex.js';
|
|
4
|
-
export type { TraceSamples, TraceSample } from './samples.js';
|
|
4
|
+
export type { TraceSamples, TraceSample, TraceTuple, TraceInput } from './samples.js';
|
|
5
5
|
export { Touchstone } from './io/Touchstone.js';
|
|
6
|
-
export type { TouchstoneData } from './io/
|
|
6
|
+
export type { TouchstoneData, TouchstoneParseOptions, TouchstoneWriteOptions } from './io/types.js';
|
|
7
7
|
export { SmithScales } from './scales/SmithScales.js';
|
|
8
8
|
export { MarkerMeasurements } from './MarkerMeasurements.js';
|
|
9
9
|
export type { TraceOptions, TraceUpdateOptions, MarkerSelectionStrategy, TraceStyle, TraceRenderMode, TraceInfo, MarkerInfo, MarkerSnapshot, MarkerComparison, } from './measurements.js';
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { TraceSamples } from '../samples.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
referenceImpedanceOhms: number;
|
|
5
|
-
}
|
|
1
|
+
import type { TraceInput, TraceSamples } from '../samples.js';
|
|
2
|
+
import type { TouchstoneData, TouchstoneParseOptions, TouchstoneWriteOptions } from './types.js';
|
|
3
|
+
/** DOM-independent Touchstone 1.x reflection-trace import and one-port export. */
|
|
6
4
|
export declare class Touchstone {
|
|
7
5
|
private constructor();
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
static parse(text: string, options: TouchstoneParseOptions & {
|
|
7
|
+
output: 'packed';
|
|
8
|
+
}): TouchstoneData<Float64Array>;
|
|
9
|
+
static parse(text: string, options?: TouchstoneParseOptions & {
|
|
10
|
+
output?: 'objects';
|
|
11
|
+
}): TouchstoneData;
|
|
12
|
+
static parse(text: string, options: TouchstoneParseOptions): TouchstoneData<TraceSamples | Float64Array>;
|
|
13
|
+
/** Serialize a reflection trace as one-port Touchstone, in Hz/RI without rounding or renormalization. */
|
|
14
|
+
static stringify(samples: TraceInput, options?: TouchstoneWriteOptions): string;
|
|
15
|
+
private static isNumber;
|
|
10
16
|
}
|
|
@@ -1,75 +1,175 @@
|
|
|
1
|
+
import { TraceBuffer } from '../traces/TraceBuffer.js';
|
|
2
|
+
/** DOM-independent Touchstone 1.x reflection-trace import and one-port export. */
|
|
1
3
|
export class Touchstone {
|
|
2
4
|
constructor() { }
|
|
3
|
-
/**
|
|
4
|
-
static parse(text) {
|
|
5
|
+
/** Frequencies are returned in Hz. Two-port input must be explicitly selected. */
|
|
6
|
+
static parse(text, options = {}) {
|
|
7
|
+
const { ports = 1, parameter = 'S11', output = 'objects' } = options;
|
|
8
|
+
if (![1, 2].includes(ports) ||
|
|
9
|
+
!['S11', 'S22'].includes(parameter) ||
|
|
10
|
+
!['objects', 'packed'].includes(output) ||
|
|
11
|
+
(ports === 1 && parameter !== 'S11')) {
|
|
12
|
+
throw new TypeError('Expected ports 1 or 2, S11 (or S22 for two ports), and objects or packed output.');
|
|
13
|
+
}
|
|
5
14
|
const units = { HZ: 1, KHZ: 1e3, MHZ: 1e6, GHZ: 1e9 };
|
|
6
15
|
let frequencyScale = 1e9;
|
|
7
16
|
let format = 'MA';
|
|
8
17
|
let referenceImpedanceOhms = 50;
|
|
9
18
|
let optionsSeen = false;
|
|
19
|
+
let count = 0;
|
|
20
|
+
let lineNumber = 0;
|
|
21
|
+
let recordLine = 0;
|
|
22
|
+
let fields = 0;
|
|
23
|
+
let previousFrequency = -1;
|
|
24
|
+
const width = ports === 1 ? 3 : 9;
|
|
25
|
+
const record = new Float64Array(width);
|
|
10
26
|
const samples = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
let packed = new Float64Array(output === 'packed' ? 3072 : 0);
|
|
28
|
+
const fail = (message) => {
|
|
29
|
+
throw new Error(`Line ${lineNumber}: ${message}`);
|
|
30
|
+
};
|
|
31
|
+
// Scan lines lazily: do not retain a line array or allocate sample objects for packed output.
|
|
32
|
+
for (const match of text.matchAll(/[^\r\n]*(?:\r\n|\r|\n|$)/g)) {
|
|
33
|
+
lineNumber++;
|
|
34
|
+
const line = match[0].split('!', 1)[0].trim();
|
|
16
35
|
if (!line) {
|
|
17
36
|
continue;
|
|
18
37
|
}
|
|
19
|
-
const fail = (message) => {
|
|
20
|
-
throw new Error(`Line ${index + 1}: ${message}`);
|
|
21
|
-
};
|
|
22
38
|
if (line.startsWith('[')) {
|
|
23
|
-
fail('
|
|
39
|
+
fail('Touchstone 2.x keywords are not supported; use a 1.x .s1p or .s2p file.');
|
|
24
40
|
}
|
|
25
41
|
if (line.startsWith('#')) {
|
|
26
|
-
if (optionsSeen ||
|
|
42
|
+
if (optionsSeen || count || fields) {
|
|
27
43
|
fail('The option line must appear once, before the data.');
|
|
28
44
|
}
|
|
29
45
|
optionsSeen = true;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
const tokens = line.slice(1).trim().toUpperCase().split(/\s+/).filter(Boolean);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
49
|
+
const token = tokens[i];
|
|
50
|
+
const kind = Object.hasOwn(units, token)
|
|
51
|
+
? 'unit'
|
|
52
|
+
: ['RI', 'MA', 'DB'].includes(token)
|
|
53
|
+
? 'format'
|
|
54
|
+
: token;
|
|
55
|
+
if (!['unit', 'format', 'S', 'R'].includes(kind) || seen.has(kind)) {
|
|
56
|
+
fail('Expected Hz|kHz|MHz|GHz, S, RI|MA|DB and one R impedance.');
|
|
57
|
+
}
|
|
58
|
+
seen.add(kind);
|
|
59
|
+
if (kind === 'unit') {
|
|
60
|
+
frequencyScale = units[token];
|
|
61
|
+
}
|
|
62
|
+
else if (kind === 'format') {
|
|
63
|
+
format = token;
|
|
64
|
+
}
|
|
65
|
+
else if (kind === 'R') {
|
|
66
|
+
const value = tokens[++i];
|
|
67
|
+
if (!value || !Touchstone.isNumber(value)) {
|
|
68
|
+
fail('Reference impedance must be a positive finite number.');
|
|
69
|
+
}
|
|
70
|
+
referenceImpedanceOhms = Number(value);
|
|
71
|
+
if (!Number.isFinite(referenceImpedanceOhms) || referenceImpedanceOhms <= 0) {
|
|
72
|
+
fail('Reference impedance must be a positive finite number.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
33
75
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
options.length > 5) {
|
|
40
|
-
fail('Expected: # Hz|kHz|MHz|GHz S RI|MA|DB R impedance.');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const token of line.matchAll(/\S+/g)) {
|
|
79
|
+
if (fields === width) {
|
|
80
|
+
fail('Each frequency record must begin on a new line.');
|
|
41
81
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
if (!Touchstone.isNumber(token[0])) {
|
|
83
|
+
fail('Expected finite decimal numbers.');
|
|
84
|
+
}
|
|
85
|
+
const value = Number(token[0]);
|
|
86
|
+
if (!Number.isFinite(value)) {
|
|
87
|
+
fail('Expected finite decimal numbers.');
|
|
88
|
+
}
|
|
89
|
+
if (fields === 0) {
|
|
90
|
+
recordLine = lineNumber;
|
|
91
|
+
const frequencyHz = value * frequencyScale;
|
|
92
|
+
if (frequencyHz < 0 || !Number.isFinite(frequencyHz)) {
|
|
93
|
+
fail('Frequency must be finite and non-negative.');
|
|
94
|
+
}
|
|
95
|
+
// Legacy noise blocks restart the frequency sweep. Never interpret them as S data.
|
|
96
|
+
if (ports === 2 && frequencyHz <= previousFrequency) {
|
|
97
|
+
fail('Two-port frequencies must increase strictly; noise data is not supported.');
|
|
98
|
+
}
|
|
99
|
+
record[fields++] = frequencyHz;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
record[fields++] = value;
|
|
47
103
|
}
|
|
48
|
-
continue;
|
|
49
104
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
fail('Expected frequency and two finite S11 components.');
|
|
105
|
+
if (fields !== width) {
|
|
106
|
+
continue;
|
|
53
107
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
let real = 0;
|
|
109
|
+
let imaginary = 0;
|
|
110
|
+
for (let offset = 1; offset < width; offset += 2) {
|
|
111
|
+
const a = record[offset];
|
|
112
|
+
const b = record[offset + 1];
|
|
113
|
+
if (format === 'MA' && a < 0) {
|
|
114
|
+
fail('Magnitude must be non-negative.');
|
|
115
|
+
}
|
|
116
|
+
const magnitude = format === 'DB' ? 10 ** (a / 20) : a;
|
|
117
|
+
const radians = ((b % 360) * Math.PI) / 180;
|
|
118
|
+
const re = format === 'RI' ? a : magnitude * Math.cos(radians);
|
|
119
|
+
const im = format === 'RI' ? b : magnitude * Math.sin(radians);
|
|
120
|
+
if (!Number.isFinite(re) || !Number.isFinite(im)) {
|
|
121
|
+
fail('An S-parameter is outside the supported numeric range.');
|
|
122
|
+
}
|
|
123
|
+
if (offset === (parameter === 'S11' ? 1 : 7)) {
|
|
124
|
+
real = re;
|
|
125
|
+
imaginary = im;
|
|
126
|
+
}
|
|
58
127
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
128
|
+
if (output === 'packed') {
|
|
129
|
+
if (count * 3 === packed.length) {
|
|
130
|
+
const grown = new Float64Array(packed.length * 2);
|
|
131
|
+
grown.set(packed);
|
|
132
|
+
packed = grown;
|
|
133
|
+
}
|
|
134
|
+
packed[count * 3] = record[0];
|
|
135
|
+
packed[count * 3 + 1] = real;
|
|
136
|
+
packed[count * 3 + 2] = imaginary;
|
|
61
137
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const reflectionCoefficient = format === 'RI' ? [a, b] : [magnitude * Math.cos(radians), magnitude * Math.sin(radians)];
|
|
65
|
-
if (!reflectionCoefficient.every(Number.isFinite)) {
|
|
66
|
-
fail('S11 is outside the supported numeric range.');
|
|
138
|
+
else {
|
|
139
|
+
samples.push({ frequencyHz: record[0], reflectionCoefficient: [real, imaginary] });
|
|
67
140
|
}
|
|
68
|
-
|
|
141
|
+
previousFrequency = record[0];
|
|
142
|
+
count++;
|
|
143
|
+
fields = 0;
|
|
69
144
|
}
|
|
70
|
-
if (
|
|
71
|
-
throw new Error(
|
|
145
|
+
if (fields) {
|
|
146
|
+
throw new Error(`Line ${recordLine}: Incomplete ${ports}-port record; expected ${width} values. Noise data is not supported.`);
|
|
72
147
|
}
|
|
73
|
-
|
|
148
|
+
if (!count) {
|
|
149
|
+
throw new Error(`The file contains no ${parameter} samples.`);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
samples: output === 'packed' ? packed.slice(0, count * 3) : samples,
|
|
153
|
+
referenceImpedanceOhms,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Serialize a reflection trace as one-port Touchstone, in Hz/RI without rounding or renormalization. */
|
|
157
|
+
static stringify(samples, options = {}) {
|
|
158
|
+
const { referenceImpedanceOhms = 50 } = options;
|
|
159
|
+
if (!Number.isFinite(referenceImpedanceOhms) || referenceImpedanceOhms <= 0) {
|
|
160
|
+
throw new RangeError('Reference impedance must be positive and finite.');
|
|
161
|
+
}
|
|
162
|
+
const data = TraceBuffer.from(samples);
|
|
163
|
+
const lines = [`# Hz S RI R ${referenceImpedanceOhms}`];
|
|
164
|
+
for (let i = 0; i < data.length; i++) {
|
|
165
|
+
if (i > 0 && data.frequency(i) <= data.frequency(i - 1)) {
|
|
166
|
+
throw new RangeError('Touchstone export requires strictly increasing frequencies.');
|
|
167
|
+
}
|
|
168
|
+
lines.push(`${data.frequency(i)} ${data.real(i)} ${data.imaginary(i)}`);
|
|
169
|
+
}
|
|
170
|
+
return lines.join('\n') + '\n';
|
|
171
|
+
}
|
|
172
|
+
static isNumber(token) {
|
|
173
|
+
return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(token);
|
|
74
174
|
}
|
|
75
175
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TraceSamples } from '../samples.js';
|
|
2
|
+
export interface TouchstoneData<T extends TraceSamples | Float64Array = TraceSamples> {
|
|
3
|
+
samples: T;
|
|
4
|
+
referenceImpedanceOhms: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TouchstoneParseOptions {
|
|
7
|
+
/** Source port count, normally taken from the filename extension. Default: 1. */
|
|
8
|
+
ports?: 1 | 2;
|
|
9
|
+
/** Reflection parameter to extract. S22 requires ports: 2. Default: S11. */
|
|
10
|
+
parameter?: 'S11' | 'S22';
|
|
11
|
+
/** Packed output contains f/re/im triples in Hz, without intermediate sample objects. */
|
|
12
|
+
output?: 'objects' | 'packed';
|
|
13
|
+
}
|
|
14
|
+
export interface TouchstoneWriteOptions {
|
|
15
|
+
/** Reference of the supplied data, in ohms. Does not renormalize it. Default: 50. */
|
|
16
|
+
referenceImpedanceOhms?: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/lib/layers.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface ChartLayers {
|
|
|
43
43
|
}
|
|
44
44
|
export interface PeripheralScales {
|
|
45
45
|
setVisible(visible: boolean): void;
|
|
46
|
+
/** Show or hide axis captions independently of numeric tick labels. Default: true. */
|
|
47
|
+
setCaptionsVisible(visible: boolean): void;
|
|
48
|
+
/** Show or hide numeric tick labels independently of captions. Default: true. */
|
|
49
|
+
setTickLabelsVisible(visible: boolean): void;
|
|
46
50
|
/** Pass null to clear the indicators. */
|
|
47
51
|
update(reflectionCoefficient: Complex | null): void;
|
|
48
52
|
}
|
|
@@ -117,7 +117,7 @@ export declare class Complex {
|
|
|
117
117
|
private static hyperbolicProduct;
|
|
118
118
|
private static hyperbolicReciprocal;
|
|
119
119
|
/**
|
|
120
|
-
* Elliptic coordinates for principal inverse functions (x
|
|
120
|
+
* Elliptic coordinates for principal inverse functions (non-negative x and y).
|
|
121
121
|
* With a = (|z+1| + |z-1|)/2, return acosh(a) and sqrt(a²-x²).
|
|
122
122
|
* Rationalized hypot differences retain tiny components near the branch cuts.
|
|
123
123
|
* Principal branches: https://dlmf.nist.gov/4.37
|
package/dist/lib/math/Complex.js
CHANGED
|
@@ -34,7 +34,7 @@ export class Complex {
|
|
|
34
34
|
}
|
|
35
35
|
if (args.length === 2) {
|
|
36
36
|
const arg0 = args[0];
|
|
37
|
-
const arg1 = args[1];
|
|
37
|
+
const arg1 = args[1] === undefined ? 0 : args[1];
|
|
38
38
|
if (typeof arg0 === 'number' && typeof arg1 === 'number') {
|
|
39
39
|
return new Complex(arg0, arg1);
|
|
40
40
|
}
|
|
@@ -598,7 +598,7 @@ export class Complex {
|
|
|
598
598
|
return Complex.reciprocal(denominator).mul(Math.exp(Math.LN2 - x));
|
|
599
599
|
}
|
|
600
600
|
/**
|
|
601
|
-
* Elliptic coordinates for principal inverse functions (x
|
|
601
|
+
* Elliptic coordinates for principal inverse functions (non-negative x and y).
|
|
602
602
|
* With a = (|z+1| + |z-1|)/2, return acosh(a) and sqrt(a²-x²).
|
|
603
603
|
* Rationalized hypot differences retain tiny components near the branch cuts.
|
|
604
604
|
* Principal branches: https://dlmf.nist.gov/4.37
|
|
@@ -7,6 +7,8 @@ export interface TraceStyle {
|
|
|
7
7
|
lineWidth: number;
|
|
8
8
|
/** Point radius in CSS pixels; remains constant under chart zoom and container resize. Default: 2. */
|
|
9
9
|
pointRadius: number;
|
|
10
|
+
/** Maximum additional line simplification error in CSS pixels. Zero disables it (default). */
|
|
11
|
+
lineTolerancePx: number;
|
|
10
12
|
}
|
|
11
13
|
export interface TraceOptions extends Partial<TraceStyle> {
|
|
12
14
|
name?: string;
|
package/dist/lib/options.d.ts
CHANGED
|
@@ -41,6 +41,10 @@ export interface InteractionOptions {
|
|
|
41
41
|
export interface PeripheralScalesOptions {
|
|
42
42
|
/** Show the peripheral rulers as one group. Default: false. */
|
|
43
43
|
visible?: boolean;
|
|
44
|
+
/** Show curved axis captions. Default: true. */
|
|
45
|
+
captionsVisible?: boolean;
|
|
46
|
+
/** Show numeric tick labels. Default: true. */
|
|
47
|
+
tickLabelsVisible?: boolean;
|
|
44
48
|
}
|
|
45
49
|
export interface SmithOptions {
|
|
46
50
|
/** Positive real reference impedance in ohms. Default: 50. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SmithTheme } from '../appearance/types.js';
|
|
2
2
|
import { SmithData } from '../traces/SmithData.js';
|
|
3
3
|
import type { SmithMarker } from '../traces/SmithMarker.js';
|
|
4
|
-
import {
|
|
4
|
+
import { TraceInput } from '../samples.js';
|
|
5
5
|
import { Complex } from '../math/Complex.js';
|
|
6
6
|
import type { ChartLayers, PeripheralScales } from '../layers.js';
|
|
7
7
|
import type { TraceStyle } from '../measurements.js';
|
|
@@ -42,7 +42,7 @@ export declare class SvgChartRenderer {
|
|
|
42
42
|
setTheme(theme: SmithTheme): void;
|
|
43
43
|
draw(target: string | HTMLElement): void;
|
|
44
44
|
/** Export the current mounted view as standalone SVG with its configured background. */
|
|
45
|
-
toSvg(): string;
|
|
45
|
+
toSvg(lineDetail?: 'full' | 'view'): string;
|
|
46
46
|
/** Remove this chart and release its event handlers. Safe to call more than once. */
|
|
47
47
|
destroy(): void;
|
|
48
48
|
private updateViewportScale;
|
|
@@ -64,6 +64,6 @@ export declare class SvgChartRenderer {
|
|
|
64
64
|
private cursorContainer;
|
|
65
65
|
private drawReactanceAxis;
|
|
66
66
|
/** Construct trace rendering without exposing chart containers or scalers to Smith. */
|
|
67
|
-
createTrace(values:
|
|
67
|
+
createTrace(values: TraceInput, color: string, style: Partial<TraceStyle>, onMarkerDrag: (marker: SmithMarker, dragging: boolean) => void): SmithData;
|
|
68
68
|
removeTrace(trace: SmithData): void;
|
|
69
69
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OptionsValidation } from '../OptionsValidation.js';
|
|
1
2
|
import { SvgTheme } from '../appearance/SvgTheme.js';
|
|
2
3
|
import * as d3 from 'd3';
|
|
3
4
|
import { MouseGesture } from '../interaction/MouseGesture.js';
|
|
@@ -147,6 +148,16 @@ export class SvgChartRenderer {
|
|
|
147
148
|
}
|
|
148
149
|
this.labelLayout.update(true);
|
|
149
150
|
},
|
|
151
|
+
setCaptionsVisible: (visible) => {
|
|
152
|
+
assertAlive();
|
|
153
|
+
OptionsValidation.boolean(visible, 'Peripheral scale caption visibility');
|
|
154
|
+
this.peripheralScaleRenderer.setCaptionsVisible(visible);
|
|
155
|
+
},
|
|
156
|
+
setTickLabelsVisible: (visible) => {
|
|
157
|
+
assertAlive();
|
|
158
|
+
OptionsValidation.boolean(visible, 'Peripheral scale tick label visibility');
|
|
159
|
+
this.peripheralScaleRenderer.setTickLabelsVisible(visible);
|
|
160
|
+
},
|
|
150
161
|
update: (gamma) => {
|
|
151
162
|
assertAlive();
|
|
152
163
|
this.peripheralScaleRenderer.update(gamma);
|
|
@@ -172,9 +183,23 @@ export class SvgChartRenderer {
|
|
|
172
183
|
this.updateViewportScale();
|
|
173
184
|
}
|
|
174
185
|
/** Export the current mounted view as standalone SVG with its configured background. */
|
|
175
|
-
toSvg() {
|
|
186
|
+
toSvg(lineDetail = 'full') {
|
|
176
187
|
this.assertAlive();
|
|
177
|
-
|
|
188
|
+
const replacements = new Map();
|
|
189
|
+
if (lineDetail === 'full') {
|
|
190
|
+
for (const trace of this.traces) {
|
|
191
|
+
const replacement = trace.fullExportLine();
|
|
192
|
+
if (replacement) {
|
|
193
|
+
replacements.set(replacement.node, replacement.path);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return SvgExporter.chart(this.svg.Node, (original, copy) => {
|
|
198
|
+
const replacement = replacements.get(original);
|
|
199
|
+
if (replacement !== undefined) {
|
|
200
|
+
copy.setAttribute('d', replacement);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
178
203
|
}
|
|
179
204
|
/** Remove this chart and release its event handlers. Safe to call more than once. */
|
|
180
205
|
destroy() {
|
|
@@ -331,7 +356,7 @@ export class SvgChartRenderer {
|
|
|
331
356
|
this.traces.forEach((d) => d.zoom(transform));
|
|
332
357
|
}
|
|
333
358
|
cursorContainer() {
|
|
334
|
-
const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none' });
|
|
359
|
+
const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none', strokeWidth: '0' });
|
|
335
360
|
shape.Element.style('pointer-events', 'all')
|
|
336
361
|
.on('pointermove.smithkit', (event) => {
|
|
337
362
|
this.cursorMove(d3.pointer(event));
|
|
@@ -54,8 +54,10 @@ export declare class RfCalculations {
|
|
|
54
54
|
* unrepresentable result is undefined; non-finite inputs throw RangeError.
|
|
55
55
|
*/
|
|
56
56
|
static renormalizeReflection(gamma: Complex, fromOhms: number, toOhms: number): Complex | undefined;
|
|
57
|
-
/** Copy and renormalize
|
|
57
|
+
/** Copy and renormalize samples, preserving object or packed representation. */
|
|
58
|
+
static renormalizeSamples(samples: Float64Array, fromOhms: number, toOhms: number): Float64Array;
|
|
58
59
|
static renormalizeSamples(samples: TraceSamples, fromOhms: number, toOhms: number): TraceSamples;
|
|
60
|
+
static renormalizeSamples(samples: TraceSamples | Float64Array, fromOhms: number, toOhms: number): TraceSamples | Float64Array;
|
|
59
61
|
/**
|
|
60
62
|
* Convert reactance in ohms at a frequency in Hz to an ideal series L or C.
|
|
61
63
|
* Zero frequency, zero reactance, or an unrepresentable component returns undefined.
|
|
@@ -115,20 +115,37 @@ export class RfCalculations {
|
|
|
115
115
|
(inverseScale / denominatorScale);
|
|
116
116
|
return Number.isFinite(re) && Number.isFinite(im) ? Complex.from(re, im) : undefined;
|
|
117
117
|
}
|
|
118
|
-
/** Copy and renormalize every sample; reject invalid/singular data without modifying the input. */
|
|
119
118
|
static renormalizeSamples(samples, fromOhms, toOhms) {
|
|
120
119
|
RfCalculations.validateReference(fromOhms);
|
|
121
120
|
RfCalculations.validateReference(toOhms);
|
|
122
|
-
|
|
123
|
-
if (!Number.isFinite(
|
|
124
|
-
sample.frequencyHz < 0 ||
|
|
125
|
-
sample.reflectionCoefficient.length !== 2) {
|
|
121
|
+
const convert = (frequencyHz, re, im, index) => {
|
|
122
|
+
if (!Number.isFinite(frequencyHz) || frequencyHz < 0) {
|
|
126
123
|
throw new RangeError(`Invalid sample at index ${index}.`);
|
|
127
124
|
}
|
|
128
|
-
const gamma = RfCalculations.renormalizeReflection(Complex.from(
|
|
125
|
+
const gamma = RfCalculations.renormalizeReflection(Complex.from(re, im), fromOhms, toOhms);
|
|
129
126
|
if (!gamma) {
|
|
130
127
|
throw new RangeError(`Renormalization is singular or outside the numeric range at sample ${index}.`);
|
|
131
128
|
}
|
|
129
|
+
return gamma;
|
|
130
|
+
};
|
|
131
|
+
if (samples instanceof Float64Array) {
|
|
132
|
+
if (samples.length % 3 !== 0) {
|
|
133
|
+
throw new RangeError('Packed samples must contain complete f/re/im triples.');
|
|
134
|
+
}
|
|
135
|
+
const result = new Float64Array(samples.length);
|
|
136
|
+
for (let offset = 0; offset < samples.length; offset += 3) {
|
|
137
|
+
const gamma = convert(samples[offset], samples[offset + 1], samples[offset + 2], offset / 3);
|
|
138
|
+
result[offset] = samples[offset];
|
|
139
|
+
result[offset + 1] = gamma.re;
|
|
140
|
+
result[offset + 2] = gamma.im;
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
return samples.map((sample, index) => {
|
|
145
|
+
if (sample.reflectionCoefficient.length !== 2) {
|
|
146
|
+
throw new RangeError(`Invalid sample at index ${index}.`);
|
|
147
|
+
}
|
|
148
|
+
const gamma = convert(sample.frequencyHz, ...sample.reflectionCoefficient, index);
|
|
132
149
|
return { frequencyHz: sample.frequencyHz, reflectionCoefficient: gamma.toVector() };
|
|
133
150
|
});
|
|
134
151
|
}
|
package/dist/lib/samples.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
/** One
|
|
1
|
+
/** One reflection sample. The reflection coefficient is [real, imaginary]. */
|
|
2
2
|
export interface TraceSample {
|
|
3
3
|
readonly frequencyHz: number;
|
|
4
4
|
readonly reflectionCoefficient: readonly [number, number];
|
|
5
5
|
}
|
|
6
6
|
export type TraceSamples = readonly TraceSample[];
|
|
7
|
+
/** One reflection sample: frequency in Hz, followed by the real and imaginary parts of Γ. */
|
|
8
|
+
export type TraceTuple = readonly [frequencyHz: number, re: number, im: number];
|
|
9
|
+
/** Homogeneous object samples, tuples, or packed f/re/im triples. Inputs are copied. */
|
|
10
|
+
export type TraceInput = TraceSamples | readonly TraceTuple[] | Float64Array;
|