Skip to content

Conversation

George-Guryev-flxcmp
Copy link
Contributor

@George-Guryev-flxcmp George-Guryev-flxcmp commented Sep 30, 2025

The main changes include:

  • Implementation of the change_port_reference_impedance method for artificially translating port reference planes by updating S-parameters.
  • Ensure consistent de-embedding behaviour when refernce planes are moved.

This enables more flexible S-parameter analysis and supports use cases where ports need to be virtually shifted without rerunning full simulation.

Greptile Overview

Updated On: 2025-09-30 23:17:37 UTC

Summary

This PR introduces S-parameter de-embedding functionality to the Tidy3D framework, specifically adding the `change_port_reference_planes` method to the `TerminalComponentModelerData` class. S-parameter de-embedding is a critical technique in RF/microwave engineering that allows users to artificially shift port reference planes without re-running expensive full-wave simulations.

The implementation extracts mode data from simulation results to compute propagation constants, then applies phase corrections using diagonal transformation matrices - a standard approach in microwave theory. The method takes an array of port shifts as input and returns updated S-parameters that reflect the virtually moved reference planes.

This feature integrates well with the existing S-matrix plugin architecture in Tidy3D, leveraging the established ModeData infrastructure and terminal component modeling capabilities. It supports common workflows in component characterization where reference plane positioning needs adjustment during post-processing analysis.

PR Description Notes:

  • Minor typo: "refernce" should be "reference"
  • Missing method name after "Implementation of the" in the description

Important Files Changed

Changed Files
Filename Score Overview
CHANGELOG.md 5/5 Added changelog entry documenting new S-parameter de-embedding feature
tidy3d/plugins/smatrix/data/terminal.py 4/5 Implemented core de-embedding method with mode data extraction and phase corrections
tests/test_plugins/smatrix/test_terminal_component_modeler.py 4/5 Added comprehensive test coverage for the new de-embedding functionality

Confidence score: 4/5

  • This PR is generally safe to merge with good implementation quality and thorough testing
  • Score reflects solid engineering practices but lacks detailed validation of complex microwave calculations
  • Pay close attention to the mathematical correctness of propagation constant calculations and phase transformations

Sequence Diagram

sequenceDiagram
    participant User
    participant TerminalComponentModelerData as "TerminalComponentModelerData"
    participant MicrowaveSMatrixData as "MicrowaveSMatrixData"
    participant PortDataArray as "PortDataArray"
    participant ModeData as "ModeData"
    
    User->>TerminalComponentModelerData: "change_port_reference_planes(smatrix, port_shifts)"
    
    Note over TerminalComponentModelerData: "Validate port_shifts dimensions"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Check len(port_shifts) == N_ports"
    
    Note over TerminalComponentModelerData: "Extract port directions and shifts"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Get directions from modeler.ports"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Flatten port_shifts array"
    
    Note over TerminalComponentModelerData: "Initialize propagation constants matrix"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Allocate kvecs[N_ports, N_freq]"
    
    Note over TerminalComponentModelerData: "Extract mode data from simulation"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Get first data key"
    TerminalComponentModelerData->>ModeData: "Filter ModeData instances"
    
    loop "For each mode data"
        ModeData->>TerminalComponentModelerData: "Return n_complex data"
        TerminalComponentModelerData->>TerminalComponentModelerData: "Calculate kvecs[i,:] = 2π * n_complex * f / C_0"
    end
    
    Note over TerminalComponentModelerData: "De-embed S-parameters"
    TerminalComponentModelerData->>MicrowaveSMatrixData: "Get S_matrix.values"
    TerminalComponentModelerData->>TerminalComponentModelerData: "Initialize S_new matrix"
    
    loop "For each frequency"
        TerminalComponentModelerData->>TerminalComponentModelerData: "Calculate phase = kvecs * port_shifts * directions"
        TerminalComponentModelerData->>TerminalComponentModelerData: "Create P_inv diagonal matrix with exp(-1j * phase)"
        TerminalComponentModelerData->>TerminalComponentModelerData: "Compute S_new[i] = P_inv @ S_matrix[i] @ P_inv"
    end
    
    Note over TerminalComponentModelerData: "Create updated S-matrix data"
    TerminalComponentModelerData->>PortDataArray: "Create TerminalPortDataArray(S_new, coords)"
    PortDataArray->>TerminalComponentModelerData: "Return new smat_data"
    
    TerminalComponentModelerData->>MicrowaveSMatrixData: "updated_copy(data=smat_data)"
    MicrowaveSMatrixData->>User: "Return de-embedded S-parameters"
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

github-actions bot commented Sep 30, 2025

Diff Coverage

Diff: origin/develop...HEAD, staged and unstaged changes

  • tidy3d/plugins/smatrix/data/data_array.py (100%)
  • tidy3d/plugins/smatrix/data/terminal.py (100%)

Summary

  • Total: 41 lines
  • Missing: 0 lines
  • Coverage: 100%

Copy link
Contributor

@dmarek-flex dmarek-flex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @George-Guryev-flxcmp for a long awaited feature! Can you clarify in the method docstring what a positive port shift and negative port shift mean? Positive moves closer to the device (inwards)?

Also you will need to handle the case of TCM with a mix of lumped and wave ports. I am guessing the lumped ports will have a 1.0 at their row/column position and cannot be de-embedded.

Hmm, and now that I think about it we might need something more specific than a numpy array for the shifts, because of the presence of both types of ports.

@dbochkov-flexcompute
Copy link
Contributor

would it make sense to add a convenience function .smatrix_deembeded(port_shifts=...) as a shortcut for tcm_data.change_port_reference_planes(tcm_data.smatrix(), port_shifts=...)? Btw, in the current form it can only be applied to pseudo wave smatrix, right?

@dmarek-flex
Copy link
Contributor

Btw, in the current form it can only be applied to pseudo wave smatrix, right?

@George-Guryev-flxcmp I think @dbochkov-flexcompute is correct here unfortunately. It might not be so simple as multiplying by a diagonal matrix for power waves.

@George-Guryev-flxcmp George-Guryev-flxcmp force-pushed the george/de-embedding branch 2 times, most recently from 64dffc1 to 14de002 Compare October 7, 2025 20:30
@George-Guryev-flxcmp George-Guryev-flxcmp changed the title support S-parameter de-embedding with reference plane shift. FXC-1636 support S-parameter de-embedding with reference plane shift. Oct 8, 2025
Copy link
Contributor

@dmarek-flex dmarek-flex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for handling the lumped port case (and confirming power waves undergo the same transformation)! I have a couple of nitpicks, and I think there is a problem when there are multiple modes.

Comment on lines +89 to +93
class PortShiftsDataArray(DataArray):
"""Port shifts Data Array"""

__slots__ = ()
_dims = "port"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shame PortDataArray is already taken. It might make sense to make this a bit more general in case in the future we need an array wrt port names for some other task. I suggest PortNameDataArray. I think port_name makes sense so that we can identify any lumped or wave port without choosing a mode index. The nomenclature is getting a bit confusing, but I am trying to split apart the notion of a Simulation Port and a microwave network port (row/col in the smatrix).

class PortShiftsDataArray(DataArray):
    """Port shifts Data Array"""

    __slots__ = ()
    _dims = "port_name"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also please add a docstring example:

    Example
    -------
    >>> import numpy as np
    >>> port_names = ["port1", "port2"]
    >>> coords = dict(port_name=port_names)
    >>> data = (1+1j) * np.random.random((2,))
    >>> port_data = PortNameDataArray(data, coords=coords)

Comment on lines +153 to +155
kvecs = np.zeros((N_ports, N_freq), dtype=complex)
shifts_vec = np.zeros((N_ports, 1))
directions_vec = np.ones((N_ports, 1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you put freq dimension first, it might be easy to make use of broadcasting instead of the loops at the bottom

# if de-embedding is requested for lumped port
if isinstance(ports[index], LumpedPortType):
raise ValueError(
"De-embedding currently supports only `WavePort` instances. "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a console message like this we use 'WavePort', single quotes. I find it hard to keep track of what type of punctuation mark I need to use, here is a summary, which is sort of up to date.

When I am in doubt I search the code base to see what other code looks like.

Comment on lines +193 to +203
mode_map = {mode_data.monitor.name: mode_data for mode_data in data}

# Collect relevant mode data
modes_data = tuple(
mode_map[port._mode_monitor_name] for port in ports if isinstance(port, WavePort)
)

# infer propagation constants from modal data
for i, mode_data in enumerate(modes_data):
n_complex = mode_data.n_complex
kvecs[i, :] = (2 * np.pi * n_complex.f * n_complex / C_0).squeeze()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this won't work when a WavePort is setup with a ModeSpec that calculates more than 1 mode. mode_data.n_complex will have dimensions of mode_index and frequency. For now you can use, waveport.mode_index to select the correct mode.

I'll need to change this when multimodal waveports are in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants