Skip to content

merge IIC-JKU new passive rf components#184

Merged
joamatab merged 156 commits into
gdsfactory:mainfrom
mdmaas:experiment/massive-pr-attempt
Jun 4, 2026
Merged

merge IIC-JKU new passive rf components#184
joamatab merged 156 commits into
gdsfactory:mainfrom
mdmaas:experiment/massive-pr-attempt

Conversation

@mdmaas

@mdmaas mdmaas commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This branch started from a fork state updated-IHP that was significantly diverged from upstream: updated-IHP/main was 382 commits behind and 144 commits ahead of gdsfactory/IHP main.

Given that divergence, this PR intentionally did not adopt the fork core wholesale. Instead, it applied a core-first integration strategy:

  • Kept upstream gdsfactory/IHP core implementations as the baseline.
  • Resolved merge conflicts in favor of upstream for foundational/core files.
  • Brought in the new passive RF functionality from the IIC-JKU side.
  • Adapted imported code so it runs on upstream core conventions (cross-sections, helper compatibility, and API glue).
  • Excluded pycell-path-dependent wrappers that rely on local sys.path / sg13g2_pycell_lib / cni runtime assumptions.

What this PR adds

  • New RF passive device module:
    • ihp/cells/rf_devices.py
  • Cell export wiring:
    • ihp/cells/__init__.py
  • Transmission-line helper and primitive updates used by the RF passive module:
    • ihp/cells/waveguides.py

Validation performed

  • Verified there are no remaining merge conflict markers.
  • Verified no static diagnostics in the touched Python files.
  • Runtime sanity check by instantiating:
    • tline
    • branch_line_coupler
    • wilkinson_power_divider
    • directional_coupler
    • quarter_wave_transformer

Notes

  • This PR focuses on new passive RF components and required helpers.
  • It intentionally avoids replacing upstream transistor/passive/bondpad core implementations with pycell-path-dependent variants.

Summary by Sourcery

Add new RF passive transmission-line primitives and higher-level RF components for the IHP PDK.

New Features:

  • Introduce coplanar and stripline transmission-line components with impedance-driven sizing, including straight segments, bends, corners, and coupled lines.
  • Add a comprehensive rf_devices module implementing branch-line couplers, Wilkinson power dividers, directional couplers, quarter-wave transformers, and bandpass filters (including coupled-line and hairpin topologies) for RF design in the IHP PDK.

Enhancements:

  • Extend waveguide utilities with stack-based geometry and dielectric constant helpers to support RF impedance calculations.

As stated in the docs is should be ", 0)"
changed to use the layer definitons from tech.py and also moved the design rules to tech.py to have all definitions in one place
Copy Paste IHP PCell code and rework so it works
in the process the via array function was changed so via stack wont work as intended
@sourcery-ai

sourcery-ai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a new RF passive device module and extends waveguide primitives to support impedance-aware transmission line synthesis (including bends, couplers, and filters) while wiring these new cells into the IHP PDK, all built on top of the upstream gdsfactory/IHP core without bringing in pycell-dependent code.

Flow diagram for tline width/Z0 resolution and layout

flowchart TD
  A[tline called] --> B{width provided?}
  B -- yes --> C1[_calculate_Z0_from_width]
  B -- no --> C2{Z0 provided?}
  C2 -- no --> E[raise ValueError: Provide either width or Z0]
  C2 -- yes --> D[_calculate_width_from_Z0]
  C1 --> F[build signal straight gf.c.straight]
  D --> F
  F --> G{ground_cross_section is list?}
  G -- yes --> H[build ground_low & ground_high gf.c.straight]
  G -- no --> I[build single ground gf.c.straight]
  H --> J[add ports from signal]
  I --> J
  J --> K[return Component]
Loading

File-Level Changes

Change Details Files
Add impedance- and stack-aware transmission line primitives and couplers to the waveguide library, used as RF building blocks.
  • Introduce helper functions to derive stack height, metal thickness, and effective dielectric constant from the active layer stack and cross-section specs.
  • Implement closed-form conversions between line width and characteristic impedance for microstrip/stripline-like geometries, including fallbacks and grid snapping using tech.nm.
  • Add parametric coplanar/stripline components (straight, circular/Euler/S-bend, corner, and coupled-line) that accept either width or Z0 and generate matching ground planes with consistent ports.
ihp/cells/waveguides.py
Introduce a new RF device library implementing standard microwave passive structures using the new transmission-line primitives.
  • Add synthesis of classic RF components (branch-line coupler, Wilkinson divider with configurable topology, directional coupler, quarter-wave transformer) parameterized by frequency, impedances, and coupling.
  • Implement lowpass prototypes (Butterworth/Chebyshev) and mapping to coupled-line even/odd-mode impedances to build straight and hairpin coupled-line bandpass filters.
  • Provide geometric utilities (corner rectangles, impedance-based line sizing, length estimation for resistive elements) and ensure all new RF cells expose clean electrical ports suitable for PDK export.
ihp/cells/rf_devices.py
Export the new RF passive cells through the IHP cells package for use by downstream designs.
  • Update the cells package init to import and expose the RF device cells and any new transmission-line primitives required by them.
ihp/cells/__init__.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • Several helpers (e.g. _get_stack_geometry, _calculate_effective_dielectric_constant, _calculate_width_from_Z0, _calculate_Z0_from_width) assume CrossSectionSpec is a string with a specific naming convention and use split('_')[0], which will break for non-string specs or different naming; consider normalizing via gf.get_cross_section(...) and layer metadata instead of relying on string parsing and implicit ordering of layer_stack().layers.
  • The width/Z0 argument validation and conversion logic is duplicated across multiple tline-related cells (tline, tline_bend_*, tline_bend_s, tline_corner); factoring this into a shared helper (e.g. resolve_width_and_Z0(...)) would reduce repetition and keep behavior consistent when updating the impedance/width formulas.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several helpers (e.g. `_get_stack_geometry`, `_calculate_effective_dielectric_constant`, `_calculate_width_from_Z0`, `_calculate_Z0_from_width`) assume `CrossSectionSpec` is a string with a specific naming convention and use `split('_')[0]`, which will break for non-string specs or different naming; consider normalizing via `gf.get_cross_section(...)` and layer metadata instead of relying on string parsing and implicit ordering of `layer_stack().layers`.
- The width/Z0 argument validation and conversion logic is duplicated across multiple tline-related cells (`tline`, `tline_bend_*`, `tline_bend_s`, `tline_corner`); factoring this into a shared helper (e.g. `resolve_width_and_Z0(...)`) would reduce repetition and keep behavior consistent when updating the impedance/width formulas.

## Individual Comments

### Comment 1
<location path="ihp/cells/waveguides.py" line_range="85" />
<code_context>
+            for i in range(signal_idx + 1, len(keys))
+        )
+    
+    e_eff = e_r * (1-exp(-1.55*(h_p+t+h_above)/h_p))
+    
+    return e_eff
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against division by zero when stack_height h_p is zero in effective dielectric calculation.

`_get_stack_geometry` can return `h_p == 0` when the signal and ground layers are adjacent (or if layer ordering changes), which will raise a `ZeroDivisionError` in `(h_p + t + h_above) / h_p`. Please handle `h_p <= 0` explicitly (e.g., fallback to `e_r` or use a small epsilon) before using it in this expression.
</issue_to_address>

### Comment 2
<location path="ihp/cells/rf_devices.py" line_range="805-811" />
<code_context>
+    return c
+
+
+def _butterworth_prototype(N: int) -> list[float]:
+    """Return Butterworth lowpass prototype element values g_0 … g_{N+1}."""
+    # g = [1.0]
+    g = []
+    for k in range(1, N + 1):
+        g.append(2 * sin((2 * k - 1) * pi / (2 * N)))
+    g.append(1.0)
+    return g
+
</code_context>
<issue_to_address>
**suggestion:** Butterworth prototype helper’s contract/comments don’t clearly match how g-values are later used.

The docstring claims this returns `g_0 … g_{N+1}`, but the function actually returns `[g1, …, gN, 1.0]` with no leading `g0`. Callers like `coupled_line_bandpass_filter` then treat this as `[g1…gN, g_{N+1}]` and prepend `1.0` for `g0`. This contract mismatch makes future misuse (e.g., double‑prepending `g0`) likely. Consider either returning `[1.0, g1, …, gN, 1.0]` and updating callers, or revising the docstring to document the current `[g1…gN, g_{N+1}]` contract and that `g0` is added by the caller.

Suggested implementation:

```python
def _estimate_rppd_length(resistance_ohm: float, width_um: float, sheet_res_ohm_per_sq: float = 300.0) -> float:
    """Estimate rppd length from target resistance using upstream resistor model constants."""


def _butterworth_prototype(N: int) -> list[float]:
    """Return Butterworth lowpass prototype element values g_0 … g_{N+1}."""
    g: list[float] = [1.0]  # g0
    for k in range(1, N + 1):
        g.append(2 * sin((2 * k - 1) * pi / (2 * N)))
    g.append(1.0)  # g_{N+1}
    return g

```

The updated helper now returns `[g0, g1, …, gN, g_{N+1}]`. Anywhere `_butterworth_prototype` is used and `1.0` is manually prepended (e.g., patterns like `[1.0] + _butterworth_prototype(N)` or `g = [1.0, * _butterworth_prototype(N)]` inside functions such as `coupled_line_bandpass_filter`), those manual `g0` prepends must be removed. Callers should instead use the list returned by `_butterworth_prototype(N)` directly as `[g0, …, g_{N+1}]`.
</issue_to_address>

### Comment 3
<location path="ihp/cells/rf_devices.py" line_range="42" />
<code_context>
+        Z0: Target characteristic impedance (ohms).
+        e_r: Relative permittivity of the substrate. Defaults to 4.1 for silicon dioxide.
+    """
+    wave_length = 3e8 / frequency * 1e6  
+    
+    c = gf.Component()
</code_context>
<issue_to_address>
**suggestion:** Use a consistent speed-of-light source and units across RF building blocks.

This function hardcodes `c = 3e8`, while others (e.g. `directional_coupler`, `quarter_wave_transformer`, filters) use `scipy.constants.c`. Please switch to `scipy.constants.c` here as well so wavelength/center‑frequency calculations remain consistent and easier to verify across the codebase.

Suggested implementation:

```python
import gdsfactory as gf
from scipy import constants

```

```python
    wave_length = constants.c / frequency * 1e6  

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread ihp/cells/waveguides.py Outdated
for i in range(signal_idx + 1, len(keys))
)

e_eff = e_r * (1-exp(-1.55*(h_p+t+h_above)/h_p))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against division by zero when stack_height h_p is zero in effective dielectric calculation.

_get_stack_geometry can return h_p == 0 when the signal and ground layers are adjacent (or if layer ordering changes), which will raise a ZeroDivisionError in (h_p + t + h_above) / h_p. Please handle h_p <= 0 explicitly (e.g., fallback to e_r or use a small epsilon) before using it in this expression.

Comment thread ihp/cells/rf_devices.py
Comment on lines +805 to +811
def _butterworth_prototype(N: int) -> list[float]:
"""Return Butterworth lowpass prototype element values g_0 … g_{N+1}."""
# g = [1.0]
g = []
for k in range(1, N + 1):
g.append(2 * sin((2 * k - 1) * pi / (2 * N)))
g.append(1.0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Butterworth prototype helper’s contract/comments don’t clearly match how g-values are later used.

The docstring claims this returns g_0 … g_{N+1}, but the function actually returns [g1, …, gN, 1.0] with no leading g0. Callers like coupled_line_bandpass_filter then treat this as [g1…gN, g_{N+1}] and prepend 1.0 for g0. This contract mismatch makes future misuse (e.g., double‑prepending g0) likely. Consider either returning [1.0, g1, …, gN, 1.0] and updating callers, or revising the docstring to document the current [g1…gN, g_{N+1}] contract and that g0 is added by the caller.

Suggested implementation:

def _estimate_rppd_length(resistance_ohm: float, width_um: float, sheet_res_ohm_per_sq: float = 300.0) -> float:
    """Estimate rppd length from target resistance using upstream resistor model constants."""


def _butterworth_prototype(N: int) -> list[float]:
    """Return Butterworth lowpass prototype element values g_0 … g_{N+1}."""
    g: list[float] = [1.0]  # g0
    for k in range(1, N + 1):
        g.append(2 * sin((2 * k - 1) * pi / (2 * N)))
    g.append(1.0)  # g_{N+1}
    return g

The updated helper now returns [g0, g1, …, gN, g_{N+1}]. Anywhere _butterworth_prototype is used and 1.0 is manually prepended (e.g., patterns like [1.0] + _butterworth_prototype(N) or g = [1.0, * _butterworth_prototype(N)] inside functions such as coupled_line_bandpass_filter), those manual g0 prepends must be removed. Callers should instead use the list returned by _butterworth_prototype(N) directly as [g0, …, g_{N+1}].

Comment thread ihp/cells/rf_devices.py Outdated
Z0: Target characteristic impedance (ohms).
e_r: Relative permittivity of the substrate. Defaults to 4.1 for silicon dioxide.
"""
wave_length = 3e8 / frequency * 1e6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Use a consistent speed-of-light source and units across RF building blocks.

This function hardcodes c = 3e8, while others (e.g. directional_coupler, quarter_wave_transformer, filters) use scipy.constants.c. Please switch to scipy.constants.c here as well so wavelength/center‑frequency calculations remain consistent and easier to verify across the codebase.

Suggested implementation:

import gdsfactory as gf
from scipy import constants
    wave_length = constants.c / frequency * 1e6  

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new rf_devices.py module containing various RF components (such as couplers, power dividers, and filters) and extends waveguides.py with transmission line primitives and analytical impedance calculations. The review feedback identifies several critical layout and routing issues, including short-circuits in the directional coupler, asymmetry in the Wilkinson power divider, and an incorrect circumference calculation due to a commented-out resistor. Additionally, there are potential runtime errors from unsafe log arguments and scipy.constants namespace access, as well as a violation of gdsfactory best practices regarding the use of .copy() on cell components.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread ihp/cells/rf_devices.py Outdated
Comment on lines +695 to +724
corner_left = c.add_ref(tline_corner(
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
Z0=Z0,
))

corner_left.connect("e1", coupled_lines.ports["e4"])
connection_port4 = c.add_ref(tline(
length=connection_length,
Z0=Z0,
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
))

corner_right = c.add_ref(tline_corner(
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
Z0=Z0,
))
connection_port4.connect("e1", corner_left.ports["e2"])

corner_right.connect("e1", coupled_lines.ports["e3"])
connection_port3 = c.add_ref(tline(
length=connection_length,
Z0=Z0,
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
))
connection_port3.connect("e1", corner_right.ports["e4"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

In directional_coupler, the coupled ports (ports 3 and 4) are routed upwards (towards positive Y) because the corners (corner_left and corner_right) are not properly mirrored/oriented. This causes connection_port3 and connection_port4 to cross and short-circuit with the main transmission lines at the top. The corners must be oriented so that the coupled ports point downwards (away from the main line).

Comment thread ihp/cells/rf_devices.py Outdated
Comment on lines +456 to +497
corner_output_p2 = c.add_ref(tline_corner(
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
Z0=Z0*sqrt(2)
))
corner_output_p2.connect(
"e1", branch_right_down.ports["e2"]
)

corner_output_p3 = c.add_ref(tline_corner(
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
Z0=Z0*sqrt(2)
))
corner_output_p3.connect(
"e2", branch_right_up.ports["e2"]
)

connection_out_p2 = c.add_ref(tline(
length=connection_length,
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
width=width_Z0,
))

connection_out_p2.connect(
"e1", corner_output_p2.ports["e2"], allow_width_mismatch=True
)
connection_out_p2.movey(width_Z0/2 - width_Z0_sqrt2/2)

connection_out_p3 = c.add_ref(tline(
length=connection_length,
signal_cross_section=signal_cross_section,
ground_cross_section=ground_cross_section,
width=width_Z0,
))

connection_out_p3.connect(
"e1", corner_output_p3.ports["e1"], allow_width_mismatch=True
)
connection_out_p3.movey(-(width_Z0/2 - width_Z0_sqrt2/2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

The Wilkinson power divider layout is completely asymmetric because the output corners (corner_output_p2 and corner_output_p3) are not mirrored/rotated symmetrically. This causes the output ports to point in different directions (one up, one left) and results in an electrically unbalanced design. Additionally, movey is used on connection_out_p2 which is oriented along Y, causing a gap or overlap instead of a lateral edge alignment.

Comment thread ihp/cells/rf_devices.py
Comment thread ihp/cells/rf_devices.py
Comment thread ihp/cells/rf_devices.py Outdated
if shape == "C":

# Calculate the circumference of the square
circumference = quater_wave_length * 2 + length_R

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The isolation resistor rppd is currently commented out (lines 616-622), but its estimated length length_R is still included in the circumference calculation. This artificially increases the branch lengths of the Wilkinson divider, shifting its operating frequency lower than the target frequency. If the resistor is not placed, length_R should be excluded from the circumference.

Suggested change
circumference = quater_wave_length * 2 + length_R
circumference = quater_wave_length * 2

Comment thread ihp/cells/rf_devices.py Outdated
Comment thread ihp/cells/rf_devices.py Outdated
Comment thread ihp/cells/rf_devices.py
Comment thread ihp/cells/rf_devices.py
Comment thread ihp/cells/rf_devices.py Outdated
mdmaas and others added 11 commits June 4, 2026 18:46
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…maas/IHP into experiment/massive-pr-attempt

# Conflicts:
#	ihp/cells/rf_devices.py

@joamatab joamatab left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks great!

@joamatab joamatab merged commit cd171a8 into gdsfactory:main Jun 4, 2026
12 checks passed
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.

4 participants