rf-touchstone 0.0.1 → 0.0.2
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/coverage/coverage-badge.svg +4 -4
- package/dist/Touchstone.cjs.js +7 -13
- package/dist/Touchstone.es.js +3502 -3440
- package/dist/Touchstone.umd.js +7 -13
- package/dist/frequency.d.ts +394 -0
- package/dist/index.d.ts +24 -0
- package/dist/touchstone.d.ts +367 -803
- package/package.json +33 -30
- package/readme.md +3 -1
- package/src/frequency.ts +693 -0
- package/src/index.ts +33 -0
- package/src/touchstone.ts +774 -0
- package/development.md +0 -205
- /package/{LICENSE → LICENSE.md} +0 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abs,
|
|
3
|
+
add,
|
|
4
|
+
arg,
|
|
5
|
+
complex,
|
|
6
|
+
Complex,
|
|
7
|
+
log10,
|
|
8
|
+
index,
|
|
9
|
+
multiply,
|
|
10
|
+
pi,
|
|
11
|
+
pow,
|
|
12
|
+
range,
|
|
13
|
+
round,
|
|
14
|
+
subset,
|
|
15
|
+
} from 'mathjs'
|
|
16
|
+
import type { FrequencyUnit } from './frequency'
|
|
17
|
+
import { Frequency } from './frequency'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* S-parameter format: MA, DB, and RI
|
|
21
|
+
* - RI: real and imaginary, i.e. $A + j \cdot B$
|
|
22
|
+
* - MA: magnitude and angle (in degrees), i.e. $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
|
|
23
|
+
* - DB: decibels and angle (in degrees), i.e. $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
|
|
24
|
+
*/
|
|
25
|
+
export const TouchstoneFormats = ['RI', 'MA', 'DB'] as const
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* S-parameter format: MA, DB, and RI
|
|
29
|
+
* - RI: real and imaginary, i.e. $A + j \cdot B$
|
|
30
|
+
* - MA: magnitude and angle (in degrees), i.e. $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
|
|
31
|
+
* - DB: decibels and angle (in degrees), i.e. $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
|
|
32
|
+
*/
|
|
33
|
+
export type TouchstoneFormat = (typeof TouchstoneFormats)[number]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Type of network parameters
|
|
37
|
+
* - S: Scattering parameters
|
|
38
|
+
* - Y: Admittance parameters
|
|
39
|
+
* - Z: Impedance parameters
|
|
40
|
+
* - H: Hybrid-h parameters
|
|
41
|
+
* - G: Hybrid-g parameters
|
|
42
|
+
*/
|
|
43
|
+
export const TouchstoneParameters = ['S', 'Y', 'Z', 'G', 'H']
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Type of network parameters: 'S' | 'Y' | 'Z' | 'G' | 'H'
|
|
47
|
+
* - S: Scattering parameters
|
|
48
|
+
* - Y: Admittance parameters
|
|
49
|
+
* - Z: Impedance parameters
|
|
50
|
+
* - H: Hybrid-h parameters
|
|
51
|
+
* - G: Hybrid-g parameters
|
|
52
|
+
*/
|
|
53
|
+
export type TouchstoneParameter = (typeof TouchstoneParameters)[number]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The reference resistance(s) for the network parameters.
|
|
57
|
+
* The token "R" (case-insensitive) followed by one or more reference resistance values.
|
|
58
|
+
* Default: 50Ω
|
|
59
|
+
*
|
|
60
|
+
* For Touchstone 1.0, this is a single value for all ports.
|
|
61
|
+
* For Touchstone 1.1, this can be an array of values (one per port)
|
|
62
|
+
*/
|
|
63
|
+
export type TouchstoneImpedance = number | number[]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Network parameter matrix stored as complex numbers.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* The matrix is a 3D array with the following dimensions:
|
|
70
|
+
* - First dimension: output port index (0 to nports-1)
|
|
71
|
+
* - Second dimension: input port index (0 to nports-1)
|
|
72
|
+
* - Third dimension: frequency point index
|
|
73
|
+
*
|
|
74
|
+
* For example:
|
|
75
|
+
* - matrix[i][j][k] represents the parameter from port j+1 to port i+1 at frequency k
|
|
76
|
+
* - For S-parameters: matrix[1][0][5] is S₂₁ at the 6th frequency point
|
|
77
|
+
*
|
|
78
|
+
* Special case for 2-port networks:
|
|
79
|
+
* - Indices are swapped to match traditional Touchstone format
|
|
80
|
+
* - matrix[0][1][k] represents S₁₂ (not S₂₁)
|
|
81
|
+
*/
|
|
82
|
+
export type TouchstoneMatrix = Complex[][][]
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Touchstone class for reading and writing network parameter data in Touchstone format.
|
|
86
|
+
* Supports both version 1.0 and 1.1 of the Touchstone specification.
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* #### Overview
|
|
90
|
+
*
|
|
91
|
+
* The **Touchstone file format** (also known as `.snp` files) is an industry-standard ASCII text format used to represent the n-port network parameters of electrical circuits. These files are commonly used in RF and microwave engineering to describe the behavior of devices such as filters, amplifiers, and interconnects.
|
|
92
|
+
*
|
|
93
|
+
* A Touchstone file contains data about network parameters (e.g., S-parameters, Y-parameters, Z-parameters) at specific frequencies.
|
|
94
|
+
*
|
|
95
|
+
* ##### Key Features:
|
|
96
|
+
* - **File Extensions**: Traditionally, Touchstone files use extensions like `.s1p`, `.s2p`, `.s3p`, etc., where the number indicates the number of ports. For example, `.s2p` represents a 2-port network.
|
|
97
|
+
* - **Case Insensitivity**: Touchstone files are case-insensitive, meaning keywords and values can be written in uppercase or lowercase.
|
|
98
|
+
* - **Versioning**: **Only version 1.0 and 1.1 are supported in this class**
|
|
99
|
+
*
|
|
100
|
+
* ---
|
|
101
|
+
*
|
|
102
|
+
* #### File Structure
|
|
103
|
+
*
|
|
104
|
+
* A Touchstone file consists of several sections, each serving a specific purpose. Below is a breakdown of the structure:
|
|
105
|
+
*
|
|
106
|
+
* ##### 1. Header Section
|
|
107
|
+
*
|
|
108
|
+
* - **Comment Lines**: Lines starting with `!` are treated as comments and ignored during parsing.
|
|
109
|
+
* - **Option Line**: Line starting with `#` defines global settings for the file, such as frequency units, parameter type, and data format. Example:
|
|
110
|
+
* ```plaintext
|
|
111
|
+
* # GHz S MA R 50
|
|
112
|
+
* ```
|
|
113
|
+
* - `GHz`: Frequency unit (can be `Hz`, `kHz`, `MHz`, or `GHz`).
|
|
114
|
+
* - `S`: Parameter type (`S`, `Y`, `Z`, `H`, or `G`).
|
|
115
|
+
* - `MA`: Data format (`MA` for magnitude-angle, `DB` for decibel-angle, or `RI` for real-imaginary).
|
|
116
|
+
* - `R 50`: Reference resistance in ohms (default is 50 ohms if omitted).
|
|
117
|
+
*
|
|
118
|
+
* ##### 2. Network Data
|
|
119
|
+
*
|
|
120
|
+
* The core of the file contains the network parameter data, organized by frequency. Each frequency point is followed by its corresponding parameter values.
|
|
121
|
+
*
|
|
122
|
+
* - **Single-Ended Networks**: Data is arranged in a matrix format. For example, a 2-port network might look like this:
|
|
123
|
+
* ```plaintext
|
|
124
|
+
* <frequency> <N11> <N21> <N12> <N22>
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* ---
|
|
128
|
+
*
|
|
129
|
+
* #### References:
|
|
130
|
+
* - {@link https://ibis.org/touchstone_ver2.1/touchstone_ver2_1.pdf Touchstone(R) File Format Specification (Version 2.1)}
|
|
131
|
+
* - {@link https://books.google.com/books/about/S_Parameters_for_Signal_Integrity.html?id=_dLKDwAAQBAJ S-Parameters for Signal Integrity}
|
|
132
|
+
* - {@link https://github.com/scikit-rf/scikit-rf/blob/master/skrf/io/touchstone.py scikit-rf: Open Source RF Engineering}
|
|
133
|
+
* - {@link https://github.com/Nubis-Communications/SignalIntegrity/blob/master/SignalIntegrity/Lib/SParameters/SParameters.py SignalIntegrity: Signal and Power Integrity Tools}
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
*
|
|
137
|
+
* #### Example 1: Simple 1-Port S-Parameter File
|
|
138
|
+
* ```plaintext
|
|
139
|
+
* ! 1-port S-parameter file
|
|
140
|
+
* # MHz S MA R 50
|
|
141
|
+
* 100 0.99 -4
|
|
142
|
+
* 200 0.80 -22
|
|
143
|
+
* 300 0.707 -45
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* #### Example 2: Simple 2-Port S-Parameter File
|
|
147
|
+
* ```plaintext
|
|
148
|
+
* ! Sample S2P File
|
|
149
|
+
* # HZ S RI R 50
|
|
150
|
+
* ! Freq S11(real) S11(imag) S21(real) S21(imag) S12(real) S12(imag) S22(real) S22(imag)
|
|
151
|
+
* 1000000000 0.9 -0.1 0.01 0.02 0.01 0.02 0.8 -0.15
|
|
152
|
+
* 2000000000 0.8 -0.2 0.02 0.03 0.02 0.03 0.7 -0.25
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export class Touchstone {
|
|
156
|
+
/**
|
|
157
|
+
* Comments in the file header with `!` symbol at the beginning of each row
|
|
158
|
+
*/
|
|
159
|
+
public comments: string[] = []
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Touchstone format: MA, DB, and RI
|
|
163
|
+
*/
|
|
164
|
+
private _format: TouchstoneFormat | undefined
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Set the Touchstone format: MA, DB, RI, or undefined
|
|
168
|
+
* - RI: real and imaginary, i.e. $A + j \cdot B$
|
|
169
|
+
* - MA: magnitude and angle (in degrees), i.e. $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
|
|
170
|
+
* - DB: decibels and angle (in degrees), i.e. $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
|
|
171
|
+
* @param format
|
|
172
|
+
* @returns
|
|
173
|
+
* @throws Will throw an error if the format is not valid
|
|
174
|
+
*/
|
|
175
|
+
set format(format: TouchstoneFormat | undefined | null) {
|
|
176
|
+
if (format === undefined || format === null) {
|
|
177
|
+
this._format = undefined
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (typeof format !== 'string') {
|
|
181
|
+
throw new Error(`Unknown Touchstone format: ${format}`)
|
|
182
|
+
}
|
|
183
|
+
switch (format.toLowerCase()) {
|
|
184
|
+
case 'ma':
|
|
185
|
+
this._format = 'MA'
|
|
186
|
+
break
|
|
187
|
+
case 'db':
|
|
188
|
+
this._format = 'DB'
|
|
189
|
+
break
|
|
190
|
+
case 'ri':
|
|
191
|
+
this._format = 'RI'
|
|
192
|
+
break
|
|
193
|
+
default:
|
|
194
|
+
throw new Error(`Unknown Touchstone format: ${format}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get the Touchstone format
|
|
200
|
+
* @returns
|
|
201
|
+
*/
|
|
202
|
+
get format(): TouchstoneFormat | undefined {
|
|
203
|
+
return this._format
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Type of network parameter: 'S' | 'Y' | 'Z' | 'G' | 'H'
|
|
208
|
+
*/
|
|
209
|
+
private _parameter: TouchstoneParameter | undefined
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Set the type of network parameter
|
|
213
|
+
* - S: Scattering parameters
|
|
214
|
+
* - Y: Admittance parameters
|
|
215
|
+
* - Z: Impedance parameters
|
|
216
|
+
* - H: Hybrid-h parameters
|
|
217
|
+
* - G: Hybrid-g parameters
|
|
218
|
+
* @param parameter
|
|
219
|
+
* @returns
|
|
220
|
+
* @throws Will throw an error if the parameter is not valid
|
|
221
|
+
*/
|
|
222
|
+
set parameter(parameter: TouchstoneParameter | undefined | null) {
|
|
223
|
+
if (parameter === undefined || parameter === null) {
|
|
224
|
+
this._parameter = undefined
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
if (typeof parameter !== 'string') {
|
|
228
|
+
throw new Error(`Unknown Touchstone parameter: ${parameter}`)
|
|
229
|
+
}
|
|
230
|
+
switch (parameter.toLowerCase()) {
|
|
231
|
+
case 's':
|
|
232
|
+
this._parameter = 'S'
|
|
233
|
+
break
|
|
234
|
+
case 'y':
|
|
235
|
+
this._parameter = 'Y'
|
|
236
|
+
break
|
|
237
|
+
case 'z':
|
|
238
|
+
this._parameter = 'Z'
|
|
239
|
+
break
|
|
240
|
+
case 'g':
|
|
241
|
+
this._parameter = 'G'
|
|
242
|
+
break
|
|
243
|
+
case 'h':
|
|
244
|
+
this._parameter = 'H'
|
|
245
|
+
break
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unknown Touchstone parameter: ${parameter}`)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get the type of network parameter
|
|
253
|
+
*/
|
|
254
|
+
get parameter() {
|
|
255
|
+
return this._parameter
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Reference impedance(s) for the network parameters
|
|
260
|
+
* Default: 50Ω
|
|
261
|
+
*/
|
|
262
|
+
private _impedance: TouchstoneImpedance = 50
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Set the Touchstone impedance.
|
|
266
|
+
* Default: 50Ω
|
|
267
|
+
* @param impedance
|
|
268
|
+
* @returns
|
|
269
|
+
* @throws Will throw an error if the impedance is not valid
|
|
270
|
+
*/
|
|
271
|
+
set impedance(impedance: TouchstoneImpedance) {
|
|
272
|
+
if (typeof impedance === 'number') {
|
|
273
|
+
this._impedance = impedance
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (!Array.isArray(impedance) || impedance.length === 0) {
|
|
277
|
+
throw new Error(`Unknown Touchstone impedance: ${impedance}`)
|
|
278
|
+
}
|
|
279
|
+
for (const element of impedance) {
|
|
280
|
+
if (typeof element !== 'number') {
|
|
281
|
+
throw new Error(`Unknown Touchstone impedance: ${impedance}`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this._impedance = impedance
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the Touchstone impedance.
|
|
289
|
+
* Default: 50Ω
|
|
290
|
+
* @returns
|
|
291
|
+
*/
|
|
292
|
+
get impedance(): TouchstoneImpedance {
|
|
293
|
+
return this._impedance
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The number of ports in the network
|
|
298
|
+
*/
|
|
299
|
+
private _nports: number | undefined
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set the ports number
|
|
303
|
+
* @param nports
|
|
304
|
+
* @returns
|
|
305
|
+
* @throws Will throw an error if the number of ports is not valid
|
|
306
|
+
*/
|
|
307
|
+
set nports(nports: number | undefined | null) {
|
|
308
|
+
if (nports === undefined || nports === null) {
|
|
309
|
+
this._nports = undefined
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
if (typeof nports !== 'number') {
|
|
313
|
+
throw new Error(`Unknown ports number: ${nports}`)
|
|
314
|
+
}
|
|
315
|
+
if (!Number.isInteger(nports)) {
|
|
316
|
+
throw new Error(`Unknown ports number: ${nports}`)
|
|
317
|
+
}
|
|
318
|
+
if (nports < 1) {
|
|
319
|
+
throw new Error(`Unknown ports number: ${nports}`)
|
|
320
|
+
}
|
|
321
|
+
this._nports = nports
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the ports number
|
|
326
|
+
*/
|
|
327
|
+
get nports() {
|
|
328
|
+
return this._nports
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Frequency points
|
|
333
|
+
*/
|
|
334
|
+
public frequency: Frequency | undefined
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 3D array to store the network parameter data
|
|
338
|
+
* - The first dimension is the exits (output) port number
|
|
339
|
+
* - The second dimension is the enters (input) port number
|
|
340
|
+
* - The third dimension is the frequency index
|
|
341
|
+
* For example, data[i][j][k] would be the parameter from j+1 port to i+1 port at frequency index k
|
|
342
|
+
*/
|
|
343
|
+
private _matrix: TouchstoneMatrix | undefined
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Sets the network parameter matrix.
|
|
347
|
+
*
|
|
348
|
+
* @param matrix - The 3D complex matrix to store, or undefined/null to clear
|
|
349
|
+
* @remarks
|
|
350
|
+
* This setter provides a way to directly assign the network parameter matrix.
|
|
351
|
+
* Setting to undefined or null will clear the existing matrix data.
|
|
352
|
+
*/
|
|
353
|
+
set matrix(matrix: TouchstoneMatrix | undefined | null) {
|
|
354
|
+
if (matrix === undefined || matrix === null) {
|
|
355
|
+
this._matrix = undefined
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
this._matrix = matrix
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Gets the current network parameter matrix (3D array).
|
|
363
|
+
* Represents the S/Y/Z/G/H-parameters of the network.
|
|
364
|
+
*
|
|
365
|
+
* @remarks
|
|
366
|
+
* Matrix Structure:
|
|
367
|
+
* - First dimension [i]: Output (exit) port number (0 to nports-1)
|
|
368
|
+
* - Second dimension [j]: Input (enter) port number (0 to nports-1)
|
|
369
|
+
* - Third dimension [k]: Frequency point index
|
|
370
|
+
*
|
|
371
|
+
* Example:
|
|
372
|
+
* - matrix[i][j][k] represents the parameter from port j+1 to port i+1 at frequency k
|
|
373
|
+
* - For S-parameters: matrix[1][0][5] is S₂₁ at the 6th frequency point
|
|
374
|
+
*
|
|
375
|
+
* Special case for 2-port networks:
|
|
376
|
+
* - Indices are swapped to match traditional Touchstone format
|
|
377
|
+
* - matrix[0][1][k] represents S₁₂ (not S₂₁)
|
|
378
|
+
*
|
|
379
|
+
* @returns The current network parameter matrix, or undefined if not set
|
|
380
|
+
*/
|
|
381
|
+
get matrix() {
|
|
382
|
+
return this._matrix
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Reads and parses a Touchstone format string into the internal data structure.
|
|
387
|
+
*
|
|
388
|
+
* @param string - The Touchstone format string to parse
|
|
389
|
+
* @param nports - Number of ports in the network
|
|
390
|
+
*
|
|
391
|
+
* @throws {Error} If the option line is missing or invalid
|
|
392
|
+
* @throws {Error} If multiple option lines are found
|
|
393
|
+
* @throws {Error} If the impedance specification is invalid
|
|
394
|
+
* @throws {Error} If the data format is invalid or incomplete
|
|
395
|
+
*
|
|
396
|
+
* @remarks
|
|
397
|
+
* The method performs the following steps:
|
|
398
|
+
* 1. Parses comments and option line
|
|
399
|
+
* 2. Extracts frequency points
|
|
400
|
+
* 3. Converts raw data into complex numbers based on format
|
|
401
|
+
* 4. Stores the results in the matrix property
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* import { Touchstone, Frequency } from 'rf-touchstone';
|
|
406
|
+
*
|
|
407
|
+
* const s1pString = `
|
|
408
|
+
* ! This is a 1-port S-parameter file
|
|
409
|
+
* # MHz S MA R 50
|
|
410
|
+
* 100 0.99 -4
|
|
411
|
+
* 200 0.80 -22
|
|
412
|
+
* 300 0.707 -45
|
|
413
|
+
* `;
|
|
414
|
+
*
|
|
415
|
+
* const touchstone = new Touchstone();
|
|
416
|
+
* touchstone.readContent(s1pString, 1);
|
|
417
|
+
*
|
|
418
|
+
* console.log(touchstone.comments); // Outputs: [ 'This is a 1-port S-parameter file' ]
|
|
419
|
+
* console.log(touchstone.format); // Outputs: 'MA'
|
|
420
|
+
* console.log(touchstone.parameter); // Outputs: 'S'
|
|
421
|
+
* console.log(touchstone.impedance); // Outputs: 50
|
|
422
|
+
* console.log(touchstone.nports); // Outputs: 1
|
|
423
|
+
* console.log(touchstone.frequency?.f_scaled); // Outputs: [ 100, 200, 300 ]
|
|
424
|
+
* console.log(touchstone.matrix); // Outputs: the parsed matrix data
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
public readContent(string: string, nports: number): void {
|
|
428
|
+
// Assign the number of ports
|
|
429
|
+
this.nports = nports
|
|
430
|
+
// Parse lines from the string
|
|
431
|
+
const lines = string
|
|
432
|
+
.split('\n')
|
|
433
|
+
.map((line) => line.trim())
|
|
434
|
+
.filter((line) => line !== '')
|
|
435
|
+
// Parse comments
|
|
436
|
+
this.comments = lines
|
|
437
|
+
.filter((line) => line.startsWith('!'))
|
|
438
|
+
.map((line) => line.slice(1).trim())
|
|
439
|
+
// Initialize frequency
|
|
440
|
+
this.frequency = new Frequency()
|
|
441
|
+
|
|
442
|
+
// Parse options
|
|
443
|
+
const options = lines.filter((line) => line.startsWith('#'))
|
|
444
|
+
if (options.length === 0) {
|
|
445
|
+
throw new Error('Unable to find the option line starting with "#"')
|
|
446
|
+
} else if (options.length > 1) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Only one option line starting with "#" is supported, but found ${options.length} lines`
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
const tokens = options[0].slice(1).trim().split(/\s+/)
|
|
452
|
+
// Frequency unit
|
|
453
|
+
this.frequency.unit = tokens[0] as FrequencyUnit
|
|
454
|
+
// Touchstone parameter
|
|
455
|
+
this.parameter = tokens[1] as TouchstoneParameter
|
|
456
|
+
// Touchstone format
|
|
457
|
+
this.format = tokens[2] as TouchstoneFormat
|
|
458
|
+
// Touchstone impedance
|
|
459
|
+
if (tokens.length >= 4) {
|
|
460
|
+
if (tokens[3].toLowerCase() !== 'r') {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Unknown Touchstone impedance: ${tokens.slice(3).join(' ')}`
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
const array = tokens.slice(4).map((d) => parseFloat(d))
|
|
466
|
+
if (array.length === 0 || array.some(Number.isNaN)) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Unknown Touchstone impedance: ${tokens.slice(3).join(' ')}`
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
if (array.length === 1) {
|
|
472
|
+
this.impedance = array[0]
|
|
473
|
+
} else if (array.length === this.nports) {
|
|
474
|
+
this.impedance = array
|
|
475
|
+
} else {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`${this.nports}-ports network, but find ${array.length} impedances: [${array}]`
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Parse frequency data
|
|
483
|
+
const content = lines
|
|
484
|
+
.filter((line) => !line.startsWith('!') && !line.startsWith('#'))
|
|
485
|
+
.map((line) => {
|
|
486
|
+
const index = line.indexOf('!')
|
|
487
|
+
if (index !== -1) {
|
|
488
|
+
// If a comment is found in the line, remove it
|
|
489
|
+
return line.substring(0, index).trim()
|
|
490
|
+
} else {
|
|
491
|
+
return line.trim()
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
.join(' ')
|
|
495
|
+
|
|
496
|
+
const data = content.split(/\s+/).map((d) => parseFloat(d))
|
|
497
|
+
// countColumn(Columns count): 1 + 2 * nports^2
|
|
498
|
+
const countColumn = 2 * Math.pow(this.nports, 2) + 1
|
|
499
|
+
if (data.length % countColumn !== 0) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Touchstone invalid data number: ${data.length}, which should be multiple of ${countColumn}`
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
const points = data.length / countColumn
|
|
505
|
+
// f[n] = TokenList[n * countColumn]
|
|
506
|
+
const rawScaled = subset(
|
|
507
|
+
data,
|
|
508
|
+
index(multiply(range(0, points), countColumn))
|
|
509
|
+
)
|
|
510
|
+
/* v8 ignore start */
|
|
511
|
+
if (Array.isArray(rawScaled)) {
|
|
512
|
+
this.frequency.f_scaled = rawScaled
|
|
513
|
+
} else if (typeof rawScaled === 'number') {
|
|
514
|
+
this.frequency.f_scaled = [rawScaled]
|
|
515
|
+
} else {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`Unknown frequency.f_scaled type: ${typeof rawScaled}, and its value: ${rawScaled}`
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
/* v8 ignore stop */
|
|
521
|
+
|
|
522
|
+
// Initialize matrix with the correct dimensions:
|
|
523
|
+
// - First dimension: output ports (nports)
|
|
524
|
+
// - Second dimension: input ports (nports)
|
|
525
|
+
// - Third dimension: frequency points (points)
|
|
526
|
+
this.matrix = new Array(nports)
|
|
527
|
+
for (let outPort = 0; outPort < nports; outPort++) {
|
|
528
|
+
this.matrix[outPort] = new Array(nports)
|
|
529
|
+
for (let inPort = 0; inPort < nports; inPort++) {
|
|
530
|
+
this.matrix[outPort][inPort] = new Array(points)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Parse matrix data: Convert raw data into complex numbers based on format
|
|
535
|
+
for (let outPort = 0; outPort < nports; outPort++) {
|
|
536
|
+
for (let inPort = 0; inPort < nports; inPort++) {
|
|
537
|
+
// A[outPort][inPort][n] = TokenList[countColumn * n + (outPort * nports + inPort) * 2 + 1]
|
|
538
|
+
const A = subset(
|
|
539
|
+
data,
|
|
540
|
+
index(
|
|
541
|
+
add(
|
|
542
|
+
multiply(range(0, points), countColumn),
|
|
543
|
+
(outPort * nports + inPort) * 2 + 1
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
// B[outPort][inPort][n] = TokenList[countColumn * n + (outPort * nports + inPort) * 2 + 2]
|
|
548
|
+
const B = subset(
|
|
549
|
+
data,
|
|
550
|
+
index(
|
|
551
|
+
add(
|
|
552
|
+
multiply(range(0, points), countColumn),
|
|
553
|
+
(outPort * nports + inPort) * 2 + 2
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
// Convert data pairs into complex numbers based on format
|
|
559
|
+
/* v8 ignore start */
|
|
560
|
+
for (let n = 0; n < points; n++) {
|
|
561
|
+
let value: Complex
|
|
562
|
+
switch (this.format) {
|
|
563
|
+
case 'RI':
|
|
564
|
+
// Real-Imaginary format: A + jB
|
|
565
|
+
value = complex(A[n], B[n])
|
|
566
|
+
break
|
|
567
|
+
case 'MA':
|
|
568
|
+
// Magnitude-Angle format: A∠B°
|
|
569
|
+
value = complex({
|
|
570
|
+
r: A[n],
|
|
571
|
+
phi: (B[n] / 180) * pi,
|
|
572
|
+
})
|
|
573
|
+
break
|
|
574
|
+
case 'DB':
|
|
575
|
+
// Decibel-Angle format: 20log₁₀(|A|)∠B°
|
|
576
|
+
value = complex({
|
|
577
|
+
r: pow(10, A[n] / 20) as number,
|
|
578
|
+
phi: (B[n] / 180) * pi,
|
|
579
|
+
})
|
|
580
|
+
break
|
|
581
|
+
default:
|
|
582
|
+
throw new Error(`Unknown Touchstone format: ${this.format}`)
|
|
583
|
+
}
|
|
584
|
+
/* v8 ignore stop */
|
|
585
|
+
|
|
586
|
+
// Store the value in the matrix
|
|
587
|
+
// Special case for 2-port networks: swap indices
|
|
588
|
+
if (nports === 2) {
|
|
589
|
+
this.matrix[inPort][outPort][n] = value
|
|
590
|
+
} else {
|
|
591
|
+
this.matrix[outPort][inPort][n] = value
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Validates the internal state of the Touchstone instance.
|
|
600
|
+
* Performs comprehensive checks on all required data and matrix dimensions.
|
|
601
|
+
*
|
|
602
|
+
* @throws {Error} If any of the following conditions are met:
|
|
603
|
+
* - Number of ports is undefined
|
|
604
|
+
* - Frequency object is not initialized
|
|
605
|
+
* - Frequency points array is empty
|
|
606
|
+
* - Network parameter type is undefined
|
|
607
|
+
* - Data format is undefined
|
|
608
|
+
* - Network parameter matrix is undefined
|
|
609
|
+
* - Matrix dimensions don't match with nports or frequency points
|
|
610
|
+
*
|
|
611
|
+
* @remarks
|
|
612
|
+
* This method performs two main validation steps:
|
|
613
|
+
* 1. Essential Data Validation:
|
|
614
|
+
* - Checks existence of all required properties
|
|
615
|
+
* - Ensures frequency points are available
|
|
616
|
+
*
|
|
617
|
+
* 2. Matrix Dimension Validation:
|
|
618
|
+
* - Verifies matrix row count matches port number
|
|
619
|
+
* - Ensures each row has correct number of columns
|
|
620
|
+
* - Validates frequency points count in each matrix element
|
|
621
|
+
*/
|
|
622
|
+
public validate(): void {
|
|
623
|
+
// Check if all required data exists
|
|
624
|
+
if (!this.nports) {
|
|
625
|
+
throw new Error('Number of ports (nports) is not defined')
|
|
626
|
+
}
|
|
627
|
+
if (!this.frequency) {
|
|
628
|
+
throw new Error('Frequency object is not defined')
|
|
629
|
+
}
|
|
630
|
+
if (this.frequency.f_scaled.length === 0) {
|
|
631
|
+
throw new Error('Frequency points array is empty')
|
|
632
|
+
}
|
|
633
|
+
if (!this.parameter) {
|
|
634
|
+
throw new Error('Network parameter type is not defined')
|
|
635
|
+
}
|
|
636
|
+
if (!this.format) {
|
|
637
|
+
throw new Error('Data format (RI/MA/DB) is not defined')
|
|
638
|
+
}
|
|
639
|
+
if (!this.matrix) {
|
|
640
|
+
throw new Error('Network parameter matrix is not defined')
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Calculate points number in the network
|
|
644
|
+
const points = this.frequency.f_scaled.length
|
|
645
|
+
// Check the matrix size
|
|
646
|
+
if (this.matrix.length !== this.nports) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Touchstone matrix has ${this.matrix.length} rows, but expected ${this.nports}`
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
for (let outPort = 0; outPort < this.nports; outPort++) {
|
|
652
|
+
if (this.matrix[outPort].length !== this.nports) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Touchstone matrix at row #${outPort} has ${this.matrix[outPort].length} columns, but expected ${this.nports}`
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
for (let inPort = 0; inPort < this.nports; inPort++) {
|
|
658
|
+
if (this.matrix[outPort][inPort].length !== points) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Touchstone matrix at row #${outPort} column #${inPort} has ${this.matrix[outPort][inPort].length} points, but expected ${points}`
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Generates a Touchstone format string from the internal data structure.
|
|
669
|
+
*
|
|
670
|
+
* @returns The generated Touchstone format string
|
|
671
|
+
*
|
|
672
|
+
* @throws {Error} If any required data is missing
|
|
673
|
+
* @throws {Error} If the matrix dimensions are invalid
|
|
674
|
+
*
|
|
675
|
+
* @remarks
|
|
676
|
+
* The generated string includes:
|
|
677
|
+
* 1. Comments (if any)
|
|
678
|
+
* 2. Option line with format, parameter type, and impedance
|
|
679
|
+
* 3. Network parameter data in the specified format
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```typescript
|
|
683
|
+
* import { Touchstone, Frequency } from 'rf-touchstone';
|
|
684
|
+
* import { complex } from 'mathjs';
|
|
685
|
+
*
|
|
686
|
+
* const touchstone = new Touchstone();
|
|
687
|
+
*
|
|
688
|
+
* // Set properties and matrix data (using simplified complex number representation for example)
|
|
689
|
+
* touchstone.comments = ['Generated by rf-touchstone'];
|
|
690
|
+
* touchstone.nports = 1;
|
|
691
|
+
* touchstone.frequency = new Frequency();
|
|
692
|
+
* touchstone.frequency.unit = 'GHz';
|
|
693
|
+
* touchstone.frequency.f_scaled = [1.0, 2.0];
|
|
694
|
+
* touchstone.parameter = 'S';
|
|
695
|
+
* touchstone.format = 'MA';
|
|
696
|
+
* touchstone.impedance = 50;
|
|
697
|
+
* touchstone.matrix = [
|
|
698
|
+
* [ // Output port 1
|
|
699
|
+
* [complex(0.5, 0.1), complex(0.4, 0.2)] // S11 at each frequency
|
|
700
|
+
* ]
|
|
701
|
+
* ];
|
|
702
|
+
*
|
|
703
|
+
* const s1pString = touchstone.writeContent();
|
|
704
|
+
* console.log(s1pString);
|
|
705
|
+
* // Expected output (approximately, due to floating point precision):
|
|
706
|
+
* // ! Generated by rf-touchstone
|
|
707
|
+
* // # GHz S MA R 50
|
|
708
|
+
* // 1 0.5099 11.3099
|
|
709
|
+
* // 2 0.4472 26.5651
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
public writeContent(): string {
|
|
713
|
+
this.validate()
|
|
714
|
+
|
|
715
|
+
// Calculate points number in the network
|
|
716
|
+
const points = this.frequency!.f_scaled.length
|
|
717
|
+
|
|
718
|
+
// Generate Touchstone content lines
|
|
719
|
+
const lines: string[] = []
|
|
720
|
+
|
|
721
|
+
// Add comments if they exist
|
|
722
|
+
if (this.comments.length > 0) {
|
|
723
|
+
lines.push(...this.comments.map((comment) => `! ${comment}`))
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Add option line
|
|
727
|
+
let optionLine = `# ${this.frequency!.unit} ${this.parameter} ${this.format}`
|
|
728
|
+
if (Array.isArray(this.impedance)) {
|
|
729
|
+
optionLine += ` R ${this.impedance.join(' ')}`
|
|
730
|
+
} else {
|
|
731
|
+
optionLine += ` R ${this.impedance}`
|
|
732
|
+
}
|
|
733
|
+
lines.push(optionLine)
|
|
734
|
+
|
|
735
|
+
// Add network data
|
|
736
|
+
for (let n = 0; n < points; n++) {
|
|
737
|
+
const dataLine: string[] = [this.frequency!.f_scaled[n].toString()]
|
|
738
|
+
|
|
739
|
+
// Add matrix data for this frequency point
|
|
740
|
+
for (let outPort = 0; outPort < this.nports!; outPort++) {
|
|
741
|
+
for (let inPort = 0; inPort < this.nports!; inPort++) {
|
|
742
|
+
const value =
|
|
743
|
+
this.nports === 2
|
|
744
|
+
? this.matrix![inPort][outPort][n]
|
|
745
|
+
: this.matrix![outPort][inPort][n]
|
|
746
|
+
|
|
747
|
+
let A: number, B: number
|
|
748
|
+
switch (this.format) {
|
|
749
|
+
case 'RI':
|
|
750
|
+
A = value.re
|
|
751
|
+
B = value.im
|
|
752
|
+
break
|
|
753
|
+
case 'MA':
|
|
754
|
+
A = abs(value) as unknown as number
|
|
755
|
+
B = (arg(value) / pi) * 180
|
|
756
|
+
break
|
|
757
|
+
case 'DB':
|
|
758
|
+
A = 20 * log10(abs(value) as unknown as number)
|
|
759
|
+
B = (arg(value) / pi) * 180
|
|
760
|
+
break
|
|
761
|
+
default:
|
|
762
|
+
throw new Error(`Unknown Touchstone format: ${this.format}`)
|
|
763
|
+
}
|
|
764
|
+
// Format numbers to avoid scientific notation and limit decimal places
|
|
765
|
+
dataLine.push(round(A, 12).toString(), round(B, 12).toString())
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
lines.push(dataLine.join(' '))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
lines.push('')
|
|
772
|
+
return lines.join('\n')
|
|
773
|
+
}
|
|
774
|
+
}
|