rf-touchstone
Version:
A Javascript/TypeScript library for reading, manipulating, and writing Touchstone files (.snp files) used in radio frequency (RF) and microwave engineering.
979 lines (920 loc) • 31.9 kB
text/typescript
import {
abs,
add,
arg,
complex,
Complex,
index,
log10,
multiply,
pi,
pow,
range,
round,
subset,
} from 'mathjs'
import type { FrequencyUnit } from './frequency'
import { Frequency } from './frequency'
/**
* Supported Touchstone data formats.
* - RI: Real and Imaginary, i.e., $A + j \cdot B$
* - MA: Magnitude and Angle (degrees), i.e., $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
* - DB: Decibel (20*log10) and Angle (degrees), i.e., $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
*/
export const TouchstoneFormats = ['RI', 'MA', 'DB'] as const
/**
* Type representing the Touchstone data format.
* - RI: Real and Imaginary, i.e., $A + j \cdot B$
* - MA: Magnitude and Angle (degrees), i.e., $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
* - DB: Decibel (20*log10) and Angle (degrees), i.e., $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
*/
export type TouchstoneFormat = (typeof TouchstoneFormats)[number]
/**
* Supported network parameter types in Touchstone files.
* - S: Scattering parameters
* - Y: Admittance parameters
* - Z: Impedance parameters
* - H: Hybrid-h parameters
* - G: Hybrid-g parameters
*/
export const TouchstoneParameters = ['S', 'Y', 'Z', 'G', 'H'] as const
/**
* Type representing the network parameter type.
* - S: Scattering
* - Y: Admittance
* - Z: Impedance
* - H: Hybrid-h
* - G: Hybrid-g
*/
export type TouchstoneParameter = (typeof TouchstoneParameters)[number]
/**
* The reference resistance(s) for the network parameters.
* The token "R" (case-insensitive) followed by one or more reference resistance values.
* Default: 50Ω
*
* For Touchstone 1.0, this is a single value for all ports.
* For Touchstone 1.1, this can be an array of values (one per port)
*/
export type TouchstoneImpedance = number | number[]
/**
* Network parameter matrix stored as complex numbers.
*
* @remarks
* The matrix is a 3D array with the following dimensions:
* - First dimension [i]: Output port index (0 to nports-1) - where the signal exits
* - Second dimension [j]: Input port index (0 to nports-1) - where the signal enters
* - Third dimension [k]: Frequency point index
*
* For example:
* - `matrix[i][j][k]` represents the parameter from port j+1 to port i+1 at frequency k
* - For S-parameters: `matrix[1][0][5]` is S₂₁ at the 6th frequency point
* (signal enters at port 1, exits at port 2)
*
* @example
* ```typescript
* // Access S21 (port 1 → port 2) at first frequency
* const s21 = touchstone.matrix[1][0][0]
*
* // Access S11 (port 1 → port 1, reflection) at first frequency
* const s11 = touchstone.matrix[0][0][0]
* ```
*/
export type TouchstoneMatrix = Complex[][][]
/**
* Represents a Touchstone file parser and generator.
* Supports reading, manipulating, and writing Touchstone (.snp) files
* following version 1.0 and 1.1 specifications.
*
* @remarks
* #### Overview
*
* The **Touchstone file format** (typically with `.snp` extensions) is an industry-standard ASCII format
* for representing n-port network parameters. It is widely used in Electronic Design Automation (EDA)
* and by measurement equipment (like VNAs) to describe the performance of RF and microwave components.
*
* ##### Key Features:
* - **File Extensions**: `.s1p`, `.s2p`, ... `.sNp` indicate a network with N ports.
* - **Case Insensitivity**: Keywords and identifiers are case-insensitive.
* - **Versions**: Full support for version 1.0 and 1.1. Version 2.0 is not currently supported.
*
* ---
*
* #### Touchstone File Structure
*
* A file consists of a header (comments and option line) followed by network data.
*
* ##### 1. Header Section
*
* - **Comment Lines**: Lines starting with `!` are stored in the `comments` array.
* - **Option Line**: The line starting with `#` sets the global context.
* Example: `# GHz S MA R 50`
* - `GHz`: Frequency unit (`Hz`, `kHz`, `MHz`, or `GHz`).
* - `S`: Parameter type (`S`, `Y`, `Z`, `H`, or `G`).
* - `MA`: Data format (`MA` for magnitude-angle, `DB` for decibel-angle, or `RI` for real-imaginary).
* - RI: $A + j \cdot B$
* - MA: $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
* - DB: $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
* - `R 50`: Reference resistance in ohms (default is 50 ohms if omitted).
*
* ##### 2. Network Data Section
*
* Data is listed frequency by frequency. For a 2-port network, the format is:
* `<frequency> <N11> <N21> <N12> <N22>` (where each NXx is a pair of values based on the format).
*
* ---
*
* #### References:
* - {@link https://github.com/scikit-rf/scikit-rf scikit-rf: Open Source RF Engineering}
* - {@link https://github.com/Nubis-Communications/SignalIntegrity SignalIntegrity: Signal and Power Integrity Tools}
* - {@link https://books.google.com/books/about/S_Parameters_for_Signal_Integrity.html?id=_dLKDwAAQBAJ S-Parameters for Signal Integrity}
* - {@link https://ibis.org/touchstone_ver2.1/touchstone_ver2_1.pdf Touchstone(R) File Format Specification (Version 2.1)}
*
* @example
* #### Parsing a local file string
* ```typescript
* import { Touchstone } from 'rf-touchstone';
*
* const content = `
* ! Simple 1-port S-parameter data
* # MHz S RI R 50
* 100 0.9 -0.1
* 200 0.8 -0.2
* `;
* const ts = Touchstone.fromText(content, 1);
* console.log(ts.parameter); // 'S'
* console.log(ts.frequency.unit); // 'MHz'
* ```
*/
export class Touchstone {
/**
* Utility to extract a filename from a URL or a file path string.
*
* @param pathOrUrl - The URL or string representation of a path.
* @returns The filename part of the string.
* @throws Error if the filename cannot be determined.
*/
public static getFilename(pathOrUrl: string): string {
let filename: string | undefined
try {
// Attempt to parse as a URL
const url = new URL(pathOrUrl)
filename = url.pathname.split(/[/\\]/).pop()
} catch {
// Fallback to simple string path logic
filename = pathOrUrl.split(/[/\\]/).pop()
}
if (!filename) {
throw new Error(`Could not determine filename from: ${pathOrUrl}`)
}
return filename
}
/**
* Extracts the basename from a filename or path by removing the file extension.
*
* @remarks
* This method intelligently handles both simple filenames and full paths.
* If a path separator is detected (/ or \), it first extracts the filename,
* then removes any file extension.
*
* @param filenameOrPath - The filename or full path to process.
* @returns The basename without extension.
*
* @example
* ```typescript
* // Works with simple filenames
* Touchstone.getBasename('myfile.s2p') // 'myfile'
* Touchstone.getBasename('data.txt') // 'data'
* Touchstone.getBasename('document.pdf') // 'document'
*
* // Also works with full paths
* Touchstone.getBasename('/path/to/network.s2p') // 'network'
* Touchstone.getBasename('C:\\data\\test.s3p') // 'test'
* Touchstone.getBasename('https://example.com/file.txt') // 'file'
*
* // Files without extension remain unchanged
* Touchstone.getBasename('noextension') // 'noextension'
* ```
*/
public static getBasename(filenameOrPath: string): string {
// If the input contains path separators, extract filename first
let filename = filenameOrPath
if (filenameOrPath.includes('/') || filenameOrPath.includes('\\')) {
filename = this.getFilename(filenameOrPath)
}
// Remove any file extension (everything after the last dot)
const lastDotIndex = filename.lastIndexOf('.')
if (lastDotIndex === -1 || lastDotIndex === 0) {
// No extension or hidden file (starts with dot)
return filename
}
return filename.substring(0, lastDotIndex)
}
/**
* Determines the number of ports based on the file extension (e.g., .s2p -> 2).
*
* @param filename - The filename or URL to inspect.
* @returns The number of ports, or null if it cannot be determined.
*/
public static parsePorts(filename: string): number | null {
const match = filename.match(/\.s(\d+)p$/i)
if (match && match[1]) {
return parseInt(match[1], 10)
}
return null
}
/**
* Creates a Touchstone instance from a raw text string.
*
* @param content - The raw text content of the Touchstone file
* @param nports - The number of ports
* @param name - Optional name for this Touchstone object (used for plotting legends and default save filename)
* @returns A new Touchstone instance
*/
public static fromText(
content: string,
nports: number,
name?: string
): Touchstone {
const ts = new Touchstone()
ts.readContent(content, nports)
ts.name = name
return ts
}
/**
* Async helper to fetch, parse, and return a Touchstone instance from a URL.
*
* @param url - The URL of the Touchstone file.
* @param nports - The expected number of ports. If null, it attempts to parse from the URL.
* @returns A promise resolving to a Touchstone instance.
* @throws Error if the fetch fails, or the number of ports cannot be determined.
*/
public static async fromUrl(
url: string,
nports?: number | null
): Promise<Touchstone> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch file: ${response.statusText}`)
}
const textContent = await response.text()
// Extract filename once for efficiency
const filename = this.getFilename(url)
let determinedNports: number | null = nports ?? null
if (determinedNports === null) {
determinedNports = this.parsePorts(filename)
}
if (determinedNports === null) {
throw new Error(
`Could not determine number of ports from URL: ${url}. Please provide nports manually.`
)
}
// Pass filename to getBasename to avoid re-extracting it
const name = this.getBasename(filename)
return this.fromText(textContent, determinedNports, name)
}
/**
* Reads a File object (typical in browser environments), parses it, and returns a Touchstone instance.
*
* @param file - The HTML5 File object to read.
* @param nports - The expected number of ports. If null, it attempts to parse from the filename.
* @returns A promise resolving to a Touchstone instance.
* @throws Error if reading fails or the number of ports cannot be determined.
*/
public static fromFile(
file: File,
nports?: number | null
): Promise<Touchstone> {
return new Promise((resolve, reject) => {
let determinedNports: number | null = nports ?? null
if (determinedNports === null) {
determinedNports = this.parsePorts(file.name)
}
if (determinedNports === null) {
return reject(
new Error(
`Could not determine number of ports from file name: ${file.name}`
)
)
}
const name = this.getBasename(file.name)
const reader = new FileReader()
reader.onload = (e) => {
try {
const textContent = e.target?.result as string
if (!textContent) {
reject(new Error('File content is empty'))
return
}
resolve(this.fromText(textContent, determinedNports, name))
} catch (err) {
reject(err)
}
}
reader.onerror = () => {
reject(new Error(`Failed to read file: ${file.name}`))
}
reader.readAsText(file)
})
}
/**
* Name of the Touchstone file (without extension).
* Used as default filename when saving and as legend label in plots.
* Automatically set when using `fromUrl()` or `fromFile()`,
* can be manually provided in `fromText()` or set directly.
*
* @example
* ```typescript
* const ts = await Touchstone.fromUrl('http://example.com/network.s2p')
* console.log(ts.name) // 'network'
* ```
*/
public name: string | undefined
/**
* Array of comment strings extracted from the Touchstone file header (lines starting with `!`).
*/
public comments: string[] = []
/**
* Touchstone format: MA, DB, and RI
*/
private _format: TouchstoneFormat | undefined
/**
* Sets the Touchstone data format.
* - RI: Real and Imaginary, i.e., $A + j \cdot B$
* - MA: Magnitude and Angle (degrees), i.e., $A \cdot e^{j \cdot {\pi \over 180} \cdot B }$
* - DB: Decibel and Angle (degrees), i.e., $10^{A \over 20} \cdot e^{j \cdot {\pi \over 180} \cdot B}$
*
* @param format - The target format (RI, MA, or DB).
* @throws Error if the format is invalid.
*/
set format(format: TouchstoneFormat | undefined | null) {
if (format === undefined || format === null) {
this._format = undefined
return
}
if (typeof format !== 'string') {
throw new Error(`Unknown Touchstone format: ${format}`)
}
switch (format.toLowerCase()) {
case 'ma':
this._format = 'MA'
break
case 'db':
this._format = 'DB'
break
case 'ri':
this._format = 'RI'
break
default:
throw new Error(`Unknown Touchstone format: ${format}`)
}
}
/**
* Get the Touchstone format
* @returns
*/
get format(): TouchstoneFormat | undefined {
return this._format
}
/**
* Type of network parameter: 'S' | 'Y' | 'Z' | 'G' | 'H'
*/
private _parameter: TouchstoneParameter | undefined
/**
* Sets the type of network parameters.
* - S: Scattering parameters
* - Y: Admittance parameters
* - Z: Impedance parameters
* - H: Hybrid-h parameters
* - G: Hybrid-g parameters
*
* @param parameter - The target parameter type (S, Y, Z, G, or H).
* @throws Error if the parameter type is invalid.
*/
set parameter(parameter: TouchstoneParameter | undefined | null) {
if (parameter === undefined || parameter === null) {
this._parameter = undefined
return
}
if (typeof parameter !== 'string') {
throw new Error(`Unknown Touchstone parameter: ${parameter}`)
}
switch (parameter.toLowerCase()) {
case 's':
this._parameter = 'S'
break
case 'y':
this._parameter = 'Y'
break
case 'z':
this._parameter = 'Z'
break
case 'g':
this._parameter = 'G'
break
case 'h':
this._parameter = 'H'
break
default:
throw new Error(`Unknown Touchstone parameter: ${parameter}`)
}
}
/**
* Get the type of network parameter
*/
get parameter() {
return this._parameter
}
/**
* Reference impedance(s) for the network parameters
* Default: 50Ω
*/
private _impedance: TouchstoneImpedance = 50
/**
* Sets the reference impedance (resistance) in Ohms.
* Default: 50Ω
*
* @param impedance - A single number for all ports, or an array of numbers (one per port).
* @throws Error if the impedance value is invalid.
*/
set impedance(impedance: TouchstoneImpedance) {
if (typeof impedance === 'number') {
this._impedance = impedance
return
}
if (!Array.isArray(impedance) || impedance.length === 0) {
throw new Error(`Unknown Touchstone impedance: ${impedance}`)
}
for (const element of impedance) {
if (typeof element !== 'number') {
throw new Error(`Unknown Touchstone impedance: ${impedance}`)
}
}
this._impedance = impedance
}
/**
* Get the Touchstone impedance.
* Default: 50Ω
* @returns
*/
get impedance(): TouchstoneImpedance {
return this._impedance
}
/**
* The number of ports in the network
*/
private _nports: number | undefined
/**
* Sets the number of ports for the network.
*
* @param nports - The integer number of ports (must be >= 1).
* @throws Error if the value is not a positive integer.
*/
set nports(nports: number | undefined | null) {
if (nports === undefined || nports === null) {
this._nports = undefined
return
}
if (typeof nports !== 'number') {
throw new Error(`Unknown ports number: ${nports}`)
}
if (!Number.isInteger(nports)) {
throw new Error(`Unknown ports number: ${nports}`)
}
if (nports < 1) {
throw new Error(`Unknown ports number: ${nports}`)
}
this._nports = nports
}
/**
* The number of ports in the network.
*/
get nports() {
return this._nports
}
/**
* Frequency metadata and point array.
*/
public frequency: Frequency | undefined
/**
* 3D array to store the network parameter data
* - First dimension [i]: Output port index (0 to nports-1) - where signal exits
* - Second dimension [j]: Input port index (0 to nports-1) - where signal enters
* - Third dimension [k]: Frequency index
*
* For example: matrix[i][j][k] is the parameter from port j+1 to port i+1 at frequency k
*/
private _matrix: TouchstoneMatrix | undefined
/**
* Directly sets the network parameter matrix.
*
* @param matrix - The 3D complex matrix to assign.
*/
set matrix(matrix: TouchstoneMatrix | undefined | null) {
if (matrix === undefined || matrix === null) {
this._matrix = undefined
return
}
this._matrix = matrix
}
/**
* Gets the current network parameter matrix (3D array).
* Represents the S/Y/Z/G/H-parameters of the network.
*
* @remarks
* Matrix Structure:
* - First dimension [i]: Output port index (0 to nports-1) - where the signal exits
* - Second dimension [j]: Input port index (0 to nports-1) - where the signal enters
* - Third dimension [k]: Frequency point index
*
* @example
* ```typescript
* // For any N-port network (2-port, 4-port, etc.):
* const s11 = touchstone.matrix[0][0][freqIdx] // S11: port 1 → port 1
* const s21 = touchstone.matrix[1][0][freqIdx] // S21: port 1 → port 2
* const s12 = touchstone.matrix[0][1][freqIdx] // S12: port 2 → port 1
* const s22 = touchstone.matrix[1][1][freqIdx] // S22: port 2 → port 2
*
* // General pattern: Sij = matrix[i-1][j-1][freqIdx]
* // where i is the output port number, j is the input port number
* ```
*
* @returns The current network parameter matrix, or undefined if not set
*/
get matrix() {
return this._matrix
}
/**
* Reads and parses a Touchstone format string into the internal data structure.
*
* @param string - The Touchstone format string to parse
* @param nports - Number of ports in the network
*
* @throws {Error} If the option line is missing or invalid
* @throws {Error} If multiple option lines are found
* @throws {Error} If the impedance specification is invalid
* @throws {Error} If the data format is invalid or incomplete
*
* @remarks
* The method performs the following steps:
* 1. Parses comments and option line
* 2. Extracts frequency points
* 3. Converts raw data into complex numbers based on format
* 4. Stores the results in the matrix property
*
* @example
* ```typescript
* import { Touchstone } from 'rf-touchstone';
*
* const s1pString = `
* ! This is a 1-port S-parameter file
* # MHz S MA R 50
* 100 0.99 -4
* 200 0.80 -22
* 300 0.707 -45
* `;
*
* const touchstone = Touchstone.fromText(s1pString, 1);
*
* console.log(touchstone.comments); // Outputs: [ 'This is a 1-port S-parameter file' ]
* console.log(touchstone.format); // Outputs: 'MA'
* console.log(touchstone.parameter); // Outputs: 'S'
* console.log(touchstone.impedance); // Outputs: 50
* console.log(touchstone.nports); // Outputs: 1
* console.log(touchstone.frequency?.f_scaled); // Outputs: [ 100, 200, 300 ]
* console.log(touchstone.matrix); // Outputs: the parsed matrix data
* ```
*/
public readContent(string: string, nports: number): void {
// Assign the number of ports
this.nports = nports
// Parse lines from the string
const lines = string
.split('\n')
.map((line) => line.trim())
.filter((line) => line !== '')
// Parse comments
this.comments = lines
.filter((line) => line.startsWith('!'))
.map((line) => line.slice(1).trim())
// Initialize frequency
this.frequency = new Frequency()
// Parse options
const options = lines.filter((line) => line.startsWith('#'))
if (options.length === 0) {
throw new Error('Unable to find the option line starting with "#"')
} else if (options.length > 1) {
throw new Error(
`Only one option line starting with "#" is supported, but found ${options.length} lines`
)
}
const tokens = options[0].slice(1).trim().split(/\s+/)
// Frequency unit
this.frequency.unit = tokens[0] as FrequencyUnit
// Touchstone parameter
this.parameter = tokens[1] as TouchstoneParameter
// Touchstone format
this.format = tokens[2] as TouchstoneFormat
// Touchstone impedance
if (tokens.length >= 4) {
if (tokens[3].toLowerCase() !== 'r') {
throw new Error(
`Unknown Touchstone impedance: ${tokens.slice(3).join(' ')}`
)
}
const array = tokens.slice(4).map((d) => parseFloat(d))
if (array.length === 0 || array.some(Number.isNaN)) {
throw new Error(
`Unknown Touchstone impedance: ${tokens.slice(3).join(' ')}`
)
}
if (array.length === 1) {
this.impedance = array[0]
} else if (array.length === this.nports) {
this.impedance = array
} else {
throw new Error(
`${this.nports}-ports network, but find ${array.length} impedances: [${array}]`
)
}
}
// Parse frequency data
const dataString = lines
.filter((line) => !line.startsWith('!') && !line.startsWith('#'))
.map((line) => {
const index = line.indexOf('!')
if (index !== -1) {
// Remove inline comments
return line.substring(0, index).trim()
} else {
return line.trim()
}
})
.join(' ')
// Split valid data tokens and parse as numbers
const data = dataString.split(/\s+/).map((d) => parseFloat(d))
// countColumn(Columns count): 1 + 2 * nports^2
// The expected number of values in each frequency data row
const countColumn = 2 * Math.pow(this.nports, 2) + 1
if (data.length % countColumn !== 0) {
throw new Error(
`Touchstone invalid data number: ${data.length}, which should be multiple of ${countColumn}`
)
}
const points = data.length / countColumn
// f[n] = TokenList[n * countColumn]
// Extract the scaled frequency values (f[n] = TokenList[n * countColumn])
const rawScaled = subset(
data,
index(multiply(range(0, points), countColumn))
)
/* v8 ignore start */
if (Array.isArray(rawScaled)) {
this.frequency.f_scaled = rawScaled
} else if (typeof rawScaled === 'number') {
this.frequency.f_scaled = [rawScaled]
} else {
throw new Error(
`Unexpected frequency f_scaled type: ${typeof rawScaled}, and its value: ${rawScaled}`
)
}
/* v8 ignore stop */
// Initialize matrix with the correct dimensions:
// - First dimension: output ports (nports)
// - Second dimension: input ports (nports)
// - Third dimension: frequency points (points)
this.matrix = Array.from<Complex[][]>({ length: nports })
for (let outPort = 0; outPort < nports; outPort++) {
this.matrix[outPort] = Array.from<Complex[]>({ length: nports })
for (let inPort = 0; inPort < nports; inPort++) {
this.matrix[outPort][inPort] = Array.from<Complex>({ length: points })
}
}
// Parse matrix data: Convert raw data into complex numbers based on format
for (let outPort = 0; outPort < nports; outPort++) {
for (let inPort = 0; inPort < nports; inPort++) {
// A[outPort][inPort][n] = TokenList[countColumn * n + (outPort * nports + inPort) * 2 + 1]
const A = subset(
data,
index(
add(
multiply(range(0, points), countColumn),
(outPort * nports + inPort) * 2 + 1
)
)
)
// B[outPort][inPort][n] = TokenList[countColumn * n + (outPort * nports + inPort) * 2 + 2]
const B = subset(
data,
index(
add(
multiply(range(0, points), countColumn),
(outPort * nports + inPort) * 2 + 2
)
)
)
// Convert data pairs into complex numbers based on format
/* v8 ignore start */
for (let n = 0; n < points; n++) {
let value: Complex
switch (this.format) {
case 'RI':
// Real-Imaginary format: A + jB
value = complex(A[n], B[n])
break
case 'MA':
// Magnitude-Angle format: A∠B°
value = complex({
r: A[n],
phi: (B[n] / 180) * pi,
})
break
case 'DB':
// Decibel-Angle format: 20log₁₀(|A|)∠B°
value = complex({
r: pow(10, A[n] / 20) as number,
phi: (B[n] / 180) * pi,
})
break
default:
throw new Error(`Unknown Touchstone format: ${this.format}`)
}
/* v8 ignore stop */
// Store the value in the matrix
// Special case for 2-port networks: swap indices
if (nports === 2) {
this.matrix[inPort][outPort][n] = value
} else {
this.matrix[outPort][inPort][n] = value
}
}
}
}
}
/**
* Validates the internal state of the Touchstone instance.
* Performs comprehensive checks on all required data and matrix dimensions.
*
* @throws {Error} If any of the following conditions are met:
* - Number of ports is undefined
* - Frequency object is not initialized
* - Frequency points array is empty
* - Network parameter type is undefined
* - Data format is undefined
* - Network parameter matrix is undefined
* - Matrix dimensions don't match with nports or frequency points
*
* @remarks
* This method performs two main validation steps:
* 1. Essential Data Validation:
* - Checks existence of all required properties
* - Ensures frequency points are available
*
* 2. Matrix Dimension Validation:
* - Verifies matrix row count matches port number
* - Ensures each row has correct number of columns
* - Validates frequency points count in each matrix element
*/
public validate(): void {
// Check if all required data exists
if (!this.nports) {
throw new Error('Number of ports (nports) is not defined')
}
if (!this.frequency) {
throw new Error('Frequency object is not defined')
}
if (this.frequency.f_scaled.length === 0) {
throw new Error('Frequency points array is empty')
}
if (!this.parameter) {
throw new Error('Network parameter type is not defined')
}
if (!this.format) {
throw new Error('Data format (RI/MA/DB) is not defined')
}
if (!this.matrix) {
throw new Error('Network parameter matrix is not defined')
}
// Get the number of frequency points
const points = this.frequency.f_scaled.length
// Validate matrix dimensions against nports and frequency points
if (this.matrix.length !== this.nports) {
throw new Error(
`Touchstone matrix has ${this.matrix.length} rows, but expected ${this.nports}`
)
}
for (let outPort = 0; outPort < this.nports; outPort++) {
if (this.matrix[outPort].length !== this.nports) {
throw new Error(
`Touchstone matrix at row index ${outPort} has ${this.matrix[outPort].length} columns, but expected ${this.nports}`
)
}
for (let inPort = 0; inPort < this.nports; inPort++) {
if (this.matrix[outPort][inPort].length !== points) {
throw new Error(
`Touchstone matrix at row ${outPort}, column ${inPort} has ${this.matrix[outPort][inPort].length} points, but expected ${points}`
)
}
}
}
}
/**
* Generates a Touchstone format string from the internal data structure.
*
* @returns The generated Touchstone format string
*
* @throws {Error} If any required data is missing
* @throws {Error} If the matrix dimensions are invalid
*
* @remarks
* The generated string includes:
* 1. Comments (if any)
* 2. Option line with format, parameter type, and impedance
* 3. Network parameter data in the specified format
*
* @example
* ```typescript
* import { Touchstone, Frequency } from 'rf-touchstone';
* import { complex } from 'mathjs';
*
* const touchstone = new Touchstone();
*
* // Set properties and matrix data
* touchstone.comments = ['Generated by rf-touchstone'];
* touchstone.nports = 1;
* touchstone.frequency = new Frequency();
* touchstone.frequency.unit = 'GHz';
* touchstone.frequency.f_scaled = [1.0, 2.0];
* touchstone.parameter = 'S';
* touchstone.format = 'MA';
* touchstone.impedance = 50;
* touchstone.matrix = [
* [ // Output port 1
* [complex(0.5, 0.1), complex(0.4, 0.2)] // S11 at each frequency
* ]
* ];
*
* const s1pString = touchstone.writeContent();
* console.log(s1pString);
* // Expected output (approximately, due to floating point precision):
* // ! Generated by rf-touchstone
* // # GHz S MA R 50
* // 1 0.5099 11.3099
* // 2 0.4472 26.5651
* ```
*/
public writeContent(): string {
this.validate()
// Calculate points number in the network
const points = this.frequency!.f_scaled.length
// Generate Touchstone content lines
const lines: string[] = []
// 1. Add comment lines
if (this.comments.length > 0) {
lines.push(...this.comments.map((comment) => `! ${comment}`))
}
// 2. Add the option line (# ...)
let optionLine = `# ${this.frequency!.unit} ${this.parameter} ${this.format}`
if (Array.isArray(this.impedance)) {
optionLine += ` R ${this.impedance.join(' ')}`
} else {
optionLine += ` R ${this.impedance}`
}
lines.push(optionLine)
// 3. Add network parameter data
for (let n = 0; n < points; n++) {
const dataLine: string[] = [this.frequency!.f_scaled[n].toString()]
// Add matrix data for this frequency point
for (let outPort = 0; outPort < this.nports!; outPort++) {
for (let inPort = 0; inPort < this.nports!; inPort++) {
// Special indexing for 2-port networks to match S11 S21 S12 S22 order
const value =
this.nports === 2
? this.matrix![inPort][outPort][n]
: this.matrix![outPort][inPort][n]
let A: number, B: number
switch (this.format) {
case 'RI':
A = value.re
B = value.im
break
case 'MA':
A = abs(value) as unknown as number
B = (arg(value) / pi) * 180
break
case 'DB':
A = 20 * log10(abs(value) as unknown as number)
B = (arg(value) / pi) * 180
break
default:
throw new Error(`Unknown Touchstone format: ${this.format}`)
}
// Limit precision and avoid scientific notation for better parser compatibility
dataLine.push(round(A, 12).toString(), round(B, 12).toString())
}
}
lines.push(dataLine.join(' '))
}
lines.push('')
return lines.join('\n')
}
}