smithkit 0.4.0 → 0.5.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 +179 -15
- package/dist/lib/OptionsValidation.js +7 -3
- package/dist/lib/Smith.d.ts +7 -1
- package/dist/lib/Smith.js +16 -5
- 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/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 +27 -2
- 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/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/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 +4 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ interactive SVG Smith charts, RF calculations, and S11 measurement exploration.
|
|
|
5
5
|
|
|
6
6
|
[Try the interactive demo](https://rosek86.github.io/smithkit/) or browse the
|
|
7
7
|
[integration examples](https://rosek86.github.io/smithkit/examples/index.html):
|
|
8
|
-
static charts, marker controls, themes, report exports, and lifecycle cleanup.
|
|
8
|
+
static charts, marker controls, a 100,000-sample trace, themes, report exports, and lifecycle cleanup.
|
|
9
9
|
Each example includes its TypeScript source.
|
|
10
10
|
|
|
11
11
|
Upgrading from 0.3.x? Version 0.4.0 introduces grouped constructor options and
|
|
@@ -34,7 +34,7 @@ define the public contract and the compatibility scope intended for 1.0. During
|
|
|
34
34
|
- Physical impedance/admittance conversions and DOM-independent RF readings.
|
|
35
35
|
- Twelve parameter scales mounted independently of chart zoom.
|
|
36
36
|
- Four peripheral rulers for phase and electrical length.
|
|
37
|
-
-
|
|
37
|
+
- Touchstone 1.x `.s1p` / `.s2p` reflection import (RI/MA/DB), packed output, and `.s1p` export.
|
|
38
38
|
- Standalone SVG and configurable PNG export of the chart and radial scales.
|
|
39
39
|
- ESM modules and TypeScript declarations.
|
|
40
40
|
|
|
@@ -121,6 +121,69 @@ events work independently.
|
|
|
121
121
|
container throws. Call `chart.destroy()` on unmount; it removes the SVG, releases
|
|
122
122
|
event handlers, and cancels queued notifications. Repeated destruction is safe.
|
|
123
123
|
|
|
124
|
+
## Trace input formats
|
|
125
|
+
|
|
126
|
+
`addTrace()` and `updateTrace()` accept the same `TraceInput` union:
|
|
127
|
+
|
|
128
|
+
- `TraceSamples`: an array of objects with `frequencyHz` and `reflectionCoefficient`.
|
|
129
|
+
- `readonly TraceTuple[]`: `[frequencyHz, re, im]` tuples, including `as const` arrays.
|
|
130
|
+
- `Float64Array`: packed triples in the order `f, re, im, f, re, im, …`.
|
|
131
|
+
|
|
132
|
+
In every format, frequency is in **Hz** and `re`/`im` are the dimensionless
|
|
133
|
+
voltage reflection coefficient **Γ**, relative to the chart's reference impedance.
|
|
134
|
+
A single input must use one format throughout; mixed tuples and objects are rejected.
|
|
135
|
+
Typed-array views are supported; only values inside the supplied view are copied.
|
|
136
|
+
The packed length must be a positive multiple of three. All formats use the same
|
|
137
|
+
finite-value checks and retain input order, duplicate frequencies, and full sample precision.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { Smith, type TraceTuple } from 'smithkit';
|
|
141
|
+
|
|
142
|
+
const chart = new Smith();
|
|
143
|
+
chart.draw('#smith');
|
|
144
|
+
const samples: readonly TraceTuple[] = [
|
|
145
|
+
[1e9, 0.4, -0.4],
|
|
146
|
+
[1.5e9, 0, 0],
|
|
147
|
+
[2e9, 0.4, 0.4],
|
|
148
|
+
];
|
|
149
|
+
const traceId = chart.addTrace(samples, { name: 'S11', mode: 'line' });
|
|
150
|
+
|
|
151
|
+
// A later update may use a different format. Each triple is one sample.
|
|
152
|
+
chart.updateTrace(traceId, new Float64Array([1e9, 0.3, -0.3, 1.5e9, 0, 0, 2e9, 0.3, 0.3]));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For large datasets, fill the packed array directly to avoid creating intermediate
|
|
156
|
+
objects or tuples:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { Smith } from 'smithkit';
|
|
160
|
+
|
|
161
|
+
const count = 100_000;
|
|
162
|
+
const samples = new Float64Array(count * 3);
|
|
163
|
+
for (let i = 0; i < count; i++) {
|
|
164
|
+
samples[3 * i] = 1e9 + i * 1e4;
|
|
165
|
+
samples[3 * i + 1] = 0.7 * Math.cos((i / count) * 10);
|
|
166
|
+
samples[3 * i + 2] = 0.7 * Math.sin((i / count) * 10);
|
|
167
|
+
}
|
|
168
|
+
const chart = new Smith();
|
|
169
|
+
chart.draw('#smith');
|
|
170
|
+
const traceId = chart.addTrace(samples, { mode: 'points' });
|
|
171
|
+
chart.addMarker(traceId, count - 1);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
All inputs are copied into owned packed storage (24 bytes per sample for the numeric
|
|
175
|
+
payload). Modifying the input after adding or updating a trace does not change the
|
|
176
|
+
chart. Marker readings and events keep their existing object shape. `Touchstone.parse()` returns object samples by default, or packed triples with
|
|
177
|
+
`output: 'packed'`. `RfCalculations.renormalizeSamples()` preserves object or packed
|
|
178
|
+
representation.
|
|
179
|
+
Invalid replacements leave data, markers, and rendering unchanged.
|
|
180
|
+
|
|
181
|
+
For more than 5,000 samples, point rendering groups overlapping points into screen
|
|
182
|
+
cells and skips offscreen points; markers still select from the full data. Lines
|
|
183
|
+
retain every sample, so very large SVG paths can remain expensive to render and export.
|
|
184
|
+
Sorted ascending frequencies accelerate frequency selection; unsorted inputs remain
|
|
185
|
+
supported without reordering. There is no automatic interpolation or data reduction.
|
|
186
|
+
|
|
124
187
|
## Constructor configuration
|
|
125
188
|
|
|
126
189
|
`new Smith(options?)` accepts a typed `SmithOptions` object. Every field is optional:
|
|
@@ -308,21 +371,41 @@ after destruction throw; trace/marker lookups return empty or missing results.
|
|
|
308
371
|
|
|
309
372
|
## Trace appearance
|
|
310
373
|
|
|
311
|
-
Pass `mode: 'points' | 'line' | 'both'`, `lineWidth`, and `
|
|
374
|
+
Pass `mode: 'points' | 'line' | 'both'`, `lineWidth`, `pointRadius`, and `lineTolerancePx` to
|
|
312
375
|
`addTrace` or `setTraceOptions`. Defaults are points, 2 px line width, and 2 px point radius. Both sizes are
|
|
313
376
|
CSS pixels and remain constant under chart zoom and container resize. Widths and radii must be positive and finite.
|
|
314
377
|
`getTraces()` includes the current style.
|
|
315
378
|
|
|
316
|
-
|
|
379
|
+
By default, lines connect all samples in input order, without smoothing or frequency sorting.
|
|
317
380
|
A line-only trace creates one SVG path and no sample circles, suitable for dense
|
|
318
381
|
sweeps. Points/both render every sample for traces up to 5,000 samples. Larger traces
|
|
319
382
|
render one representative per point-radius-sized display cell and skip points
|
|
320
383
|
outside the viewport; representatives are recalculated during zoom/pan.
|
|
321
384
|
This only simplifies the image: all original samples remain available to markers,
|
|
322
|
-
snapping, readouts, and comparisons.
|
|
385
|
+
snapping, readouts, and comparisons.
|
|
323
386
|
Styles do not change marker identity or layer ordering. Hidden traces retain their style after sample replacement.
|
|
324
387
|
The demo exposes the same controls per trace.
|
|
325
388
|
|
|
389
|
+
For dense lines, opt into `lineTolerancePx` (finite, non-negative; default `0` disables
|
|
390
|
+
simplification):
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
chart.setTraceOptions(traceId, { mode: 'line', lineTolerancePx: 0.5 });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The tolerance bounds additional geometric deviation in **CSS pixels**, accounting for
|
|
397
|
+
chart zoom and container size. It does not include SVG coordinate rounding or browser
|
|
398
|
+
antialiasing. Cached, nested geometry levels retain endpoints and input order; zooming
|
|
399
|
+
in selects finer geometry, while panning reuses the current level. The cache is rebuilt
|
|
400
|
+
when samples change, including reference-impedance renormalization. Markers, frequency
|
|
401
|
+
selection, RF calculations, and sample counts always use the full data. The option
|
|
402
|
+
applies to the line in `line` and `both` modes; point rendering is unchanged.
|
|
403
|
+
|
|
404
|
+
Preparing levels takes extra CPU and memory, and noisy traces may simplify very little.
|
|
405
|
+
This is an opt-in display optimization, not data compression. See the
|
|
406
|
+
[large-trace example](https://rosek86.github.io/smithkit/examples/large-trace/) to compare
|
|
407
|
+
full geometry with pixel tolerances and [local measurements](docs/performance.md).
|
|
408
|
+
|
|
326
409
|
## Events and readings
|
|
327
410
|
|
|
328
411
|
`onEvent(listener)` supports multiple independent subscriptions and returns an
|
|
@@ -638,7 +721,30 @@ from S11 alone. At \|Γ\| = 0.1 it reads 10 dB, while return loss reads 20 dB.
|
|
|
638
721
|
|
|
639
722
|
Peripheral rulers are hidden by default. Enable them with
|
|
640
723
|
`{ peripheralScales: { visible: true } }` or `chart.peripheralScales.setVisible(true)`.
|
|
641
|
-
|
|
724
|
+
Axis captions and numeric tick labels can be controlled independently, through the
|
|
725
|
+
constructor, `setOptions()`, or the peripheral scale controls:
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
import { Smith } from 'smithkit';
|
|
729
|
+
|
|
730
|
+
const chart = new Smith({
|
|
731
|
+
peripheralScales: { visible: true, captionsVisible: false, tickLabelsVisible: true },
|
|
732
|
+
});
|
|
733
|
+
chart.draw('#smith');
|
|
734
|
+
|
|
735
|
+
chart.setOptions({ peripheralScales: { captionsVisible: true } });
|
|
736
|
+
chart.peripheralScales.setTickLabelsVisible(false); // Captions and rulers remain visible.
|
|
737
|
+
chart.peripheralScales.setCaptionsVisible(false); // Rulers and indicators remain visible.
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Both text options default to `true`; the entire group remains hidden by default.
|
|
741
|
+
When captions are hidden, numeric labels fill the sectors normally reserved for them.
|
|
742
|
+
Hiding/re-showing the rulers preserves both preferences. Text controls do not affect
|
|
743
|
+
ruler lines, ticks, indicators, grid labels, or the independent radial parameter scales.
|
|
744
|
+
SVG/PNG exports preserve the current text visibility. The demo exposes **Axis captions**
|
|
745
|
+
and **Tick values** checkboxes in **Chart settings**.
|
|
746
|
+
|
|
747
|
+
`chart.peripheralScales.update(Complex | null)` supplies or clears the indicators.
|
|
642
748
|
Its four rulers zoom with the chart: reflection phase, voltage transmission phase,
|
|
643
749
|
wavelengths toward generator, and wavelengths toward load. Wavelength rulers start
|
|
644
750
|
at the negative real axis, with 0.5 λ per revolution (generator clockwise, load
|
|
@@ -677,17 +783,29 @@ Import renormalization is enabled by a labeled checkbox. Disable it to reject
|
|
|
677
783
|
files whose reference differs from the chart. Complex reference impedances and
|
|
678
784
|
multiport renormalization are outside the supported scope.
|
|
679
785
|
|
|
680
|
-
## Touchstone import
|
|
786
|
+
## Touchstone import and export
|
|
681
787
|
|
|
682
|
-
`Touchstone.parse(text)` returns `{ samples, referenceImpedanceOhms }`.
|
|
683
|
-
|
|
684
|
-
into Hz, and throws
|
|
788
|
+
`Touchstone.parse(text, options?)` returns `{ samples, referenceImpedanceOhms }`.
|
|
789
|
+
It reads Touchstone 1.x S-parameters, converts RI/MA/DB into Cartesian Γ and
|
|
790
|
+
frequencies into Hz, and throws an `Error` with a line number for invalid content.
|
|
791
|
+
Invalid parser options throw `TypeError`.
|
|
792
|
+
|
|
793
|
+
| Option | Default | Meaning |
|
|
794
|
+
| ----------- | ----------- | -------------------------------------------------------------------------------- |
|
|
795
|
+
| `ports` | `1` | Source port count: `1` for `.s1p`, `2` for `.s2p`. It is not inferred from text. |
|
|
796
|
+
| `parameter` | `'S11'` | Reflection parameter: `'S11'` or, with two ports, `'S22'`. |
|
|
797
|
+
| `output` | `'objects'` | Object samples or `'packed'` `Float64Array` containing f/re/im triples. |
|
|
685
798
|
|
|
686
799
|
```ts
|
|
687
800
|
import { Smith, Touchstone } from 'smithkit';
|
|
688
801
|
|
|
689
802
|
async function showMeasurement(file: File): Promise<Smith> {
|
|
690
|
-
const
|
|
803
|
+
const ports = /\.s2p$/i.test(file.name) ? 2 : 1;
|
|
804
|
+
const { samples, referenceImpedanceOhms } = Touchstone.parse(await file.text(), {
|
|
805
|
+
ports,
|
|
806
|
+
parameter: ports === 2 ? 'S22' : 'S11',
|
|
807
|
+
output: 'packed',
|
|
808
|
+
});
|
|
691
809
|
const chart = new Smith({ referenceImpedanceOhms });
|
|
692
810
|
chart.draw('#smith');
|
|
693
811
|
chart.addTrace(samples, { name: file.name });
|
|
@@ -695,10 +813,51 @@ async function showMeasurement(file: File): Promise<Smith> {
|
|
|
695
813
|
}
|
|
696
814
|
```
|
|
697
815
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
816
|
+
Packed parsing writes directly into growing typed storage, without creating an
|
|
817
|
+
object or coordinate array per sample. It scans lines incrementally within the
|
|
818
|
+
supplied string; this is not a streaming file API. The returned buffer has exact
|
|
819
|
+
length and is caller-owned. The default object result remains unchanged.
|
|
820
|
+
|
|
821
|
+
Omitted header options use GHz, S, MA, 50 Ω. The parser accepts comments, BOM,
|
|
822
|
+
LF/CRLF/CR line endings, decimal scientific notation, and continued records. Each
|
|
823
|
+
frequency record starts on a new line. Two-port records use S11, S21, S12, S22
|
|
824
|
+
order; all four parameters are validated even though only one reflection trace is
|
|
825
|
+
returned. Two-port frequencies must increase strictly. Three or more ports,
|
|
826
|
+
per-port/complex reference impedances, noise blocks and Touchstone 2.x keywords
|
|
827
|
+
are rejected. See the [IBIS Touchstone specification](https://www.ibis.org/touchstone_ver2.1/touchstone_ver2_1.pdf)
|
|
828
|
+
for the legacy 1.x data layout.
|
|
829
|
+
|
|
830
|
+
`Touchstone.stringify(samples, { referenceImpedanceOhms })` returns `.s1p` text
|
|
831
|
+
with a `# Hz S RI R …` header and a final newline. It accepts object samples,
|
|
832
|
+
tuples, or packed triples, writes numeric values without a precision limit, and
|
|
833
|
+
does not modify or renormalize the input. The reference defaults to 50 Ω; supply
|
|
834
|
+
the actual reference of your data. Empty/invalid samples, nonpositive/nonfinite
|
|
835
|
+
reference impedance, and duplicate or descending frequencies throw `RangeError`.
|
|
836
|
+
Samples are never silently sorted or discarded.
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
import { RfCalculations, Touchstone } from 'smithkit';
|
|
840
|
+
|
|
841
|
+
const sourceText = '# GHz S RI R 50\n1 0 0\n2 0.5 0';
|
|
842
|
+
const parsed = Touchstone.parse(sourceText, { output: 'packed' });
|
|
843
|
+
const samples = RfCalculations.renormalizeSamples(
|
|
844
|
+
parsed.samples,
|
|
845
|
+
parsed.referenceImpedanceOhms,
|
|
846
|
+
75,
|
|
847
|
+
);
|
|
848
|
+
const text = Touchstone.stringify(samples, { referenceImpedanceOhms: 75 });
|
|
849
|
+
// Save text as measurement.s1p in your application.
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
For an extracted S11/S22 trace, this is **one-port renormalization**: the other
|
|
853
|
+
port remains terminated in the original file reference. It is not renormalization
|
|
854
|
+
of the complete two-port network. Exporting S22 to `.s1p` represents that selected
|
|
855
|
+
port as the single port of the new file; transmission data is not exported.
|
|
856
|
+
|
|
857
|
+
The [Touchstone example](https://rosek86.github.io/smithkit/examples/touchstone/index.html)
|
|
858
|
+
shows parameter selection, packed import, renormalization and download. In the demo,
|
|
859
|
+
choose **Two-port reflection** before importing `.s2p`; the trace name includes the
|
|
860
|
+
selected parameter. Both `.s1p` and `.s2p` use packed import.
|
|
702
861
|
|
|
703
862
|
## PNG and report export
|
|
704
863
|
|
|
@@ -725,6 +884,11 @@ const scalePng = await scales.toPng({ width: 1200, background: 'transparent' });
|
|
|
725
884
|
- `background`: omit to keep component backgrounds, or pass a concrete CSS color
|
|
726
885
|
(for example `'#fff'`) or `'transparent'` to replace them. Chart colors stay as
|
|
727
886
|
configured; choose a suitable chart theme for your report background.
|
|
887
|
+
- `lineDetail` (chart only): `'full'` (default) includes every sample in each trace line;
|
|
888
|
+
`'view'` preserves the currently displayed line geometry, including simplification.
|
|
889
|
+
Both work for SVG and PNG without changing the live chart. Increasing export dimensions
|
|
890
|
+
scales the current-view error too; use `'full'` for reports requiring all samples.
|
|
891
|
+
This option does not change point culling, the viewport, markers, or scales.
|
|
728
892
|
- `legend` (chart only): append the names and colors of visible traces. Long names
|
|
729
893
|
wrap; hidden traces are omitted. Default: `false`.
|
|
730
894
|
- `markerLegend` (chart only): independently append marker descriptions below the chart/scales.
|
|
@@ -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]]) {
|
|
@@ -146,7 +152,7 @@ export class Smith {
|
|
|
146
152
|
toSvg(options) {
|
|
147
153
|
this.assertAlive();
|
|
148
154
|
this.validateExportOptions(options);
|
|
149
|
-
const source = this.renderer.toSvg();
|
|
155
|
+
const source = this.renderer.toSvg(options?.lineDetail ?? 'full');
|
|
150
156
|
if (options === undefined) {
|
|
151
157
|
return source;
|
|
152
158
|
}
|
|
@@ -157,6 +163,9 @@ export class Smith {
|
|
|
157
163
|
return ImageExporter.svg(sources, options, this.exportLegend(options));
|
|
158
164
|
}
|
|
159
165
|
validateExportOptions(options) {
|
|
166
|
+
if (options?.lineDetail !== undefined && !['full', 'view'].includes(options.lineDetail)) {
|
|
167
|
+
throw new TypeError('Export line detail must be full or view.');
|
|
168
|
+
}
|
|
160
169
|
if (options?.scaleReadout !== undefined && !options.scales) {
|
|
161
170
|
throw new TypeError('Scale readout requires included radial scales.');
|
|
162
171
|
}
|
|
@@ -178,7 +187,7 @@ export class Smith {
|
|
|
178
187
|
async toPng(options = {}) {
|
|
179
188
|
this.assertAlive();
|
|
180
189
|
this.validateExportOptions(options);
|
|
181
|
-
const sources = [this.toSvg()];
|
|
190
|
+
const sources = [this.renderer.toSvg(options.lineDetail ?? 'full')];
|
|
182
191
|
if (options.scales) {
|
|
183
192
|
sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
|
|
184
193
|
}
|
|
@@ -231,7 +240,6 @@ export class Smith {
|
|
|
231
240
|
this.assertAlive();
|
|
232
241
|
this.renderer.resetView();
|
|
233
242
|
}
|
|
234
|
-
/** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
|
|
235
243
|
addTrace(values, options = {}) {
|
|
236
244
|
this.assertAlive();
|
|
237
245
|
this.validateTraceOptions(options);
|
|
@@ -285,6 +293,10 @@ export class Smith {
|
|
|
285
293
|
if (options.mode !== undefined && !['points', 'line', 'both'].includes(options.mode)) {
|
|
286
294
|
throw new TypeError('Trace mode must be points, line, or both.');
|
|
287
295
|
}
|
|
296
|
+
if (options.lineTolerancePx !== undefined &&
|
|
297
|
+
(!Number.isFinite(options.lineTolerancePx) || options.lineTolerancePx < 0)) {
|
|
298
|
+
throw new RangeError('Line tolerance must be finite and non-negative.');
|
|
299
|
+
}
|
|
288
300
|
for (const key of ['lineWidth', 'pointRadius']) {
|
|
289
301
|
const value = options[key];
|
|
290
302
|
if (value !== undefined && (!Number.isFinite(value) || value <= 0)) {
|
|
@@ -323,7 +335,6 @@ export class Smith {
|
|
|
323
335
|
}
|
|
324
336
|
return true;
|
|
325
337
|
}
|
|
326
|
-
/** Replace samples; by default each marker follows its nearest measured frequency. */
|
|
327
338
|
updateTrace(id, values, options = {}) {
|
|
328
339
|
this.assertAlive();
|
|
329
340
|
const data = this.data[this.traceIndex(id)];
|
|
@@ -419,7 +430,7 @@ export class Smith {
|
|
|
419
430
|
if (referenceImpedanceOhms === this.referenceOhms) {
|
|
420
431
|
return;
|
|
421
432
|
}
|
|
422
|
-
const samples = this.data.map((data) =>
|
|
433
|
+
const samples = this.data.map((data) => data.Samples.renormalize(this.referenceOhms, referenceImpedanceOhms));
|
|
423
434
|
this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
|
|
424
435
|
this.referenceOhms = referenceImpedanceOhms;
|
|
425
436
|
this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
|
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
|
}
|