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.
@@ -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
+ }