Skip to content

Palace guide

Use gsim.palace for driven RF and microwave simulations, eigenmodes, electrostatics, and boundary modes. This guide covers solver configuration, meshing, and result processing. Class and function links open the API reference.

Solver settings

Numerical and problem-specific settings live under sim.solver. Each simulation exposes its applicable problem group alongside the common controls:

import gsim.palace as pa

sim = pa.EigenmodeSim()
sim.solver.order = 1
sim.solver.linear.tolerance = 1e-9
sim.solver.linear.max_iterations = 400
sim.solver.eigenmode.num_modes = 2
sim.solver.eigenmode.target = 4e9  # Hz; required before meshing/exporting
sim.solver.eigenmode.tolerance = 1e-9
sim.solver.eigenmode.save = 2

The linear tolerance controls each linear solve. The eigenmode tolerance controls eigenvalue convergence. They are independent. All Palace simulation types default to GMRES with complex MUMPS multigrid, field order 2, 400 maximum linear iterations, and "CPU". Linear tolerance defaults to 1e-6, except for EigenmodeSim, whose linear and eigenvalue tolerances both default to 1e-9. An omitted linear tolerance uses the simulation-specific default through grouped dictionaries, settings models, and legacy numerical constructors or assignments. An explicitly supplied tolerance, including 1e-6, is preserved.

The configurable multigrid controls live under sim.solver.linear:

Setting Default Meaning
solver_type "MUMPS" Coarse solver backend; "Default" asks Palace to choose.
mg_max_levels 100 Maximum multigrid levels; Palace uses the available hierarchy. Set 1 for a full-system solve.
pc_mat_real False Set True to use a real approximation of the preconditioner matrix.
complex_coarse_solve True Use a complex coarse solve for complex-valued problems.

Real-only problems such as electrostatics still solve real equations. The complex controls do not turn materials or fields into complex quantities in those problems. MUMPS no longer forces one Krylov iteration, disables multigrid, or disables the mesh error estimator or divergence-free projection. Explicit settings are exported without backend-specific overrides.

sim.solver.linear.pc_mat_real = True
sim.solver.linear.complex_coarse_solve = False  # lower-memory real approximation
sim.solver.linear.mg_max_levels = 1  # full-system solve; can need much more RAM

solver_type="SuperLU", "STRUMPACK", and "Default" remain available. preconditioner="AMS" or "BoomerAMG" applies when solver_type="Default". Use pc_mat_real=True and complex_coarse_solve=False to request a real preconditioner with another backend. These settings can also be supplied to set_solver(...) or in the constructor's solver={"linear": ...} group.

The defaults are a starting point. Check convergence and mesh resolution for each problem, especially when extracting quality factors. Driven adaptive-sweep controls are separate and retain their existing defaults.

You can also supply grouped settings at construction:

sim = pa.EigenmodeSim(
    solver={
        "order": 1,
        "linear": {"tolerance": 1e-6, "max_iterations": 400},
        "eigenmode": {"num_modes": 2, "target": 4e9, "tolerance": 1e-8, "save": 2},
    }
)

Other simulation types expose their own problem group:

driven = pa.DrivenSim(solver={"driven": {"fmin": 1e9, "fmax": 10e9, "num_points": 40}})
driven.solver.driven.adaptive_tol = 0.01

electrostatic = pa.ElectrostaticSim()
electrostatic.solver.electrostatic.save_fields = 2

boundary = pa.BoundaryModeSim()
boundary.solver.boundary_mode.freq = 5e9
boundary.solver.boundary_mode.num_modes = 2

All four share nine controls: order, device, and linear.{solver_type, preconditioner, tolerance, max_iterations, mg_max_levels, pc_mat_real, complex_coarse_solve}.

Simulation Problem group Problem settings Total settings
DrivenSim solver.driven 13 22
EigenmodeSim solver.eigenmode 8 17
ElectrostaticSim solver.electrostatic 1 10
BoundaryModeSim solver.boundary_mode 7 16

sim.set_solver(...) replaces the common controls using the same defaults as the simulation constructor, including its linear tolerance, while preserving the problem-specific settings. Assign individual attributes to retain other common values. The generated Palace JSON continues to use Solver.Order, Solver.Device, Solver.Linear, and the applicable problem block.

Existing code

sim.numerical, NumericalConfig, set_numerical(...), the top-level problem attributes, and legacy constructor arguments remain supported and emit DeprecationWarning messages pointing to the grouped API. Problem-specific setters such as set_eigenmode(...) remain available as convenience methods. Old serialized inputs still load; simulation serialization uses the grouped solver field. NumericalConfig retains its flat serialization format. Migrate direct access as follows:

Previous access Grouped access
sim.numerical.order sim.solver.order
sim.numerical.tolerance sim.solver.linear.tolerance
sim.eigenmode.target sim.solver.eigenmode.target
sim.driven.fmin sim.solver.driven.fmin
sim.electrostatic.save_fields sim.solver.electrostatic.save_fields
sim.boundary_mode.freq sim.solver.boundary_mode.freq

MUMPS behavior change

Reloading a saved gsim settings model with solver_type="MUMPS" now uses the saved iteration limit and the new multigrid defaults when multigrid controls are absent. Previously, gsim forced MaxIts=1 and MGMaxLevels=1, regardless of the saved iteration limit. Old settings still load, but their generated Palace configuration and solve behavior can change. Explicit saved tolerances are retained.

For a full-system solve, set mg_max_levels=1; set max_iterations=1 to also request the former iteration limit. This does not reproduce the entire old block: gsim no longer writes the forced EstimatorMaxIts=0, EstimatorTol=1e-6, DivFreeMaxIts=0, or DivFreeTol=1e-6. Those auxiliary controls are not currently exposed by the settings API, so Palace applies its defaults. Exposing them is deferred; there is no automatic legacy-mode fallback.

Calling set_numerical() without an explicit order now uses 2, matching the constructor and set_solver(). Pass order=1 to retain the previous setter behavior. Such calls emit a temporary FutureWarning explaining this change. Explicit-order calls emit the usual API deprecation warning. The new grouped API emits neither.

Update individual controls directly to preserve other settings:

sim.solver.order = 1
sim.solver.linear.tolerance = 1e-8

sim.set_solver(...) replaces the common controls with the supplied values and their defaults, preserving the existing problem group, such as the eigenmode target. Assigning sim.solver = ... replaces the entire solver configuration, including its problem group; a new eigenmode group defaults to target=None.

sim.numerical is a deprecated alias for sim.solver, so its model_dump() uses the grouped format. NumericalConfig(**sim.numerical.model_dump()) accepts that format and copies the nine common numerical controls into a flat legacy model. For new code, use sim.solver.model_copy(deep=True) to copy all solver settings.

See result validation for independent solver, adaptive sampling, mode identity, wave-port mode continuity, and provenance checks.

Numeric wave-port boundaries

The default waveport_boundary="pec" approximates finite conductivity, impedance and absorbing boundaries as PEC inside the port eigenproblem. Choose "inherit" to retain those physical conditions when solving the port mode:

from gsim.palace import DrivenSim

sim = DrivenSim()
sim.set_driven(f=50e9, waveport_boundary="inherit")
# After geometry/ports are configured and the mesh is generated:
# config = json.loads(sim.write_config().read_text())
# "WavePortPEC" not in config["Boundaries"]

Only the generated Boundaries.WavePortPEC override changes. The physical 3D conductivity, impedance and absorbing boundaries remain identical. Lumped ports are unaffected. This controls a modeling approximation; it does not fix Palace mode selection or establish lossy S-parameter normalization accuracy. Check the port fields and solver convergence for the chosen Palace runtime.

The policies differ only when absorbing, conductivity or impedance boundaries exist; without these Robin terms, both omit WavePortPEC. A later set_driven() call rebuilds the driven settings, so repeat waveport_boundary="inherit" when updating other sweep options if you want to retain it.

Material override precedence is separate; see PR #277.

Result validation

Simulation Classes

Floquet eigenmodes

Set the signed cell phase in radians. The wave vector uses the measured donor-to-receiver mesh translation; the eigenvalue search frequency does not determine the cell length. An optional periodic_length in mesh units (micrometers for generated meshes) checks that the periodic faces have the expected separation:

from gsim.palace import EigenmodeSim

sim = EigenmodeSim()
sim.set_eigenmode(target=40e9, floquet=True, phi_target=-0.4, periodic_length=100.0)
sim.solver.eigenmode.compute_floquet_wave_vector(periodic_axis="x")
# [-0.004, 0.0, 0.0] rad/um

After setting geometry and stack, sim.mesh(periodic_axis="x") records the actual translation. sim.write_config() writes it as Boundaries.Periodic.BoundaryPairs[0].Translation, and computes FloquetWaveVector from that measured length. A length mismatch raises ValueError; omit periodic_length to use the measured value without an expected-length check. Domain padding can change the separation of periodic faces. Generated translations use the CAD face planes rather than the tolerance-padded OCC bounding boxes.

Zero, negative phases, and the Brillouin-zone endpoints +/-pi are supported without wrapping. With Palace's phase convention, the receiver field is exp(-1j * phi_target) times the donor field. The GDS mesher supports periodic axes x and y; the wave-vector helper and config generation also support z for supplied mesh metadata.

n_eff_guess is retained as a deprecated compatibility argument and no longer affects the result. Passing it explicitly to set_eigenmode() emits a DeprecationWarning. Direct calls to compute_floquet_wave_vector() now require an explicit length on the config or as a periodic_length argument; the old l0 argument and frequency-based length estimate are removed. Old mesh results without translation metadata must be regenerated.

Electrostatic terminals

An ElectrostaticSim terminal selects conductors by connected net, by region or by layer. A net is the metal that touches on one layer plus the metal that vias join on other layers, so two electrodes on one layer are two nets (see sim.nets()).

sim.add_terminal("TOP", net="PLUS")  # net by port name, or by its name in sim.nets()
sim.add_terminal("LEFT", region=(0, 0, 20, 20), layer="m1")  # nets with an m1 shape in the box
sim.add_terminal("SHIELD", net="net3", floating=True)  # solved, then eliminated
sim.add_terminal("T1", layer="metal5")  # a whole layer, as before
sim.unassigned_conductors = "ground"  # the default; or "floating"
print(sim.resolve_terminals())  # check the selection before meshing
cap = sim.load_capacitance(sim.run())  # floating terminals are eliminated
cap.between("TOP", "LEFT")
cap.to_ground("LEFT")
cap.pairs()  # every pair and every terminal-to-ground value, as a table
  • Net names are the names of the ports on the net joined by +, or net<k> (k counts from 1 in layout order). net= matches the whole name or any one port name; if several nets carry the same port name they all go into that terminal. net<k> changes when the layout is edited, so prefer port names or region=. GDS text labels are not used.
  • region= is a box (xmin, ymin, xmax, ymax) or a list of at least three (x, y) points, in um. It selects every net with a conductor or via shape that overlaps the region, only on layer when that is given. It always selects whole nets, because part of a conductor cannot be at another potential.
  • layer= alone keeps the old behaviour while no terminal uses net, region or floating: every shape on that layer, and vias by z-overlap. Once any terminal uses nets, layer= means every net with a shape on that layer, including the metal that vias join to it on other layers. net= cannot be combined with region= or layer=.
  • Conductors in no terminal are grounded, as before. With unassigned_conductors="floating" each becomes an extra terminal named after its net. PEC blocks are always grounded. A net in two terminals is an error.
  • Floating conductors are solved as terminals, one extra Palace excitation each, and load_capacitance removes them with a Schur complement of the Maxwell matrix, C_tt - C_tf C_ff^-1 C_ft, which is exact for electrostatics. Pass eliminate_floating=False to keep them. The rows are the terminals in the order they were added, then the automatic floating terminals in net order.
  • Vias and merging. mesh() merges vias closer than merge_via_distance (2 um by default). If that would join the vias of two nets it raises a ValueError naming the via layer and the nets; set merge_via_distance below the gap between them. Merging also fills the gaps between vias, so their area grows; a warning names the layer and the area ratio when it grows by more than 5%. Pass merge_via_distance=0 to keep every via as drawn.
  • Changing the selection style. A mesh made for layer terminals cannot serve net terminals and the other way round; write_config() raises and asks you to call mesh() again. In net mode write_config() also writes terminals.json, which load_capacitance reads when the simulation object is new.
  • Reading the results. cap.between(a, b) is the capacitance between two electrodes and cap.to_ground(a) the one to ground; cap.pairs() lists both kinds apart. If no conductor is grounded, to_ground comes out near zero.

Capacitance

Saved fields and S-parameters

load_fields accepts a simulation directory, Palace output directory, or results dictionary. Driven, BoundaryMode, and Eigenmode volume and boundary outputs are supported. Enable field saving with save_step >= 1 for driven simulations or save >= 1 for eigenmode simulations.

from gsim.palace.results import load_fields

fields = load_fields("eigenmode_sim/output/palace", mode=1)
electric = fields["E_real"] + 1j * fields["E_imag"]
assert electric.shape == (fields.n_points, 3)
surface = load_fields("eigenmode_sim/output/palace", mode=1, boundary=True)

mode=1 selects Cycle000001 from Eigenmode or BoundaryMode output; a missing mode or a mode containing only mesh metadata raises an error. Use cycle=N to select an exact ParaView cycle instead, including metadata cycles. Omit both selectors to load the last cycle containing solution fields, skipping final Indicator, Rank, or attribute arrays. mode and cycle are mutually exclusive. For driven output, excitation=N selects the excitation directory. A missing excitation now raises FileNotFoundError instead of selecting another excitation. Metadata-only output raises ValueError by default; explicit cycle=N still permits inspecting that metadata. Mode/cycle selection was also checked against saved Palace Floquet run palace-552ac5e8, in addition to synthetic tests.

load_sparams converts frequency, dB magnitude, and degree phase columns to floating-point arrays. A whitespace-padded -inf dB value remains negative infinity and converts to exact zero, without a magnitude floor:

from pathlib import Path
from tempfile import TemporaryDirectory

import numpy as np

from gsim.palace.results import load_sparams

with TemporaryDirectory() as directory:
    output = Path(directory)
    (output / "port-S.csv").write_text(
        "f (GHz), |S[2][1]| (dB), arg(S[2][1]) (deg.)\n"
        "1.0,   -inf   , 0.0\n"
    )
    parameters = load_sparams(output)
    assert np.isneginf(parameters.s21.db[0])
    assert parameters.s21.complex[0] == 0j
    assert parameters.to_skrf().s[0, 1, 0] == 0j

Malformed numeric cells reject the entire CSV, including otherwise valid rows. Errors report the path, column and one-based data row, excluding the header and blank lines. NaN, positive infinity and empty numeric cells raise ValueError; phase and frequency must be finite, while dB magnitudes permit negative infinity.

Mesh

Presets select mesh-size targets near conductors and in the bulk. Explicit refined_mesh_size and max_mesh_size arguments override those targets. auto_size=True only scales a preset when refined_mesh_size is omitted; an explicit size is always preserved, even with auto_size=True.

Feature detection merges touching polygons on each conductor layer before checking trace/neck widths and gaps, including notches and holes. Distances below two layout database units are ignored as grid artifacts. Gap searches for sizing stop at the smallest width already found (and at 500 um); larger gaps cannot change the size. Cross-layer spacing and arbitrary tapered or curved widths are not measured.

Resolution warnings compare the effective size with the detected conductor feature divided by cells_per_feature (2 by default). They suggest a smaller explicit size when an override is present. This feature-based rule is a heuristic; mesh validation and a lack of warnings do not establish convergence of resonance frequencies, Q, or S-parameters. Compare results across mesh refinements.

sim.mesh() also reports Estimated Field DOFs for tetrahedral 3D driven and eigenmode problems, using the configured field order. For order 2, the estimate is 2 * unique_edges + 2 * unique_triangular_faces, counting interior and shared entities once. Geometry order and field order are separate. This is the input mesh count before Palace splits interior boundaries, applies periodic constraints or refines the mesh; it is not a bound on the final solve size. Other problem types and mixed/non-tetrahedral meshes have a null estimate.

Mesh results expose a JSON-compatible result.metadata dictionary, also written to metadata.json beside the mesh and included in cloud input uploads:

result = sim.mesh()
metadata = result.metadata
metadata["schema_version"]  # 1
metadata["mesh"]["topology"]  # edges and triangular_faces
metadata["mesh"]["field_dofs"]["estimated_field_dofs"]
metadata["mesh"]["kappa"]["max"]  # numerical distortion, not formatted text

sim.write_config() refreshes the file using the effective config's field order, problem type, solver settings and refinement controls. The result object is a snapshot from meshing. Consumers should accept additional metadata keys so new measurements can be added. Allocation rules remain in DataLab; these are sizing hints. gsim sends supported tetrahedron and field-DOF counts to DataLab before upload, including with older SDK versions. See cloud sizing metadata for the calibration assumptions. The root-level metadata.json name is reserved for these diagnostics. It is excluded from the Palace result-cache key so measurements can evolve without rerunning identical mesh/config inputs. The full compute_dir_digest() includes the metadata by default; the cache key is not a complete bundle-integrity hash.

sim.mesh() reports Worst element distortion, κ in its summary. The value is also available in result.mesh_stats["kappa"]["max"] and sim.print_mesh_stats(). It uses Palace/MFEM's normalized Jacobian condition number: 1 is an ideal equilateral tetrahedron; larger values indicate greater distortion. Curved elements are sampled at their centers, so this is not a bound over their interiors. Use it alongside SICN's signed validity check and solution convergence; κ alone does not measure simulation accuracy. A numerically singular center is displayed as infinite and stored as max=None with a nonzero singular_elements count.

Feature-based sizing

Automatic sizing uses a same-layer geometry heuristic. Touching and overlapping conductor polygons are merged before measuring widths and spacing. Nearly parallel opposing edges reveal narrow traces and necks; spacing checks also include notches and holes within one connected conductor. Polygon bounding boxes provide a fallback for shapes without suitable opposing edges.

Zero distances and distances below two layout database units are ignored. Gap searches are limited to 500 um; cross-layer gaps and arbitrary tapered or curved widths are not measured. These limitations make the estimate useful for choosing an initial mesh, rather than establishing simulation accuracy. Compare results across refinements before relying on resonance frequencies, Q, or S-parameters.

Nets

Stack

Circuit Synthesis

Fitting

Parameter Conversions

Transmission-line analysis

See Transmission-line analysis for using load_sparams(...).to_skrf() with scikit-rf's multiline TRL calibration, physical impedance normalization and independent-length checks.

Terminal S-parameters

See Terminal (multi-pin) Wave Ports for the conventions and a worked example.

terminal

Modal to terminal S-parameters for multi-conductor (multi-pin) ports.

A field solver returns modal S-parameters: one port per propagating mode. Circuit work (differential/common-mode analysis, bias lines, SPICE export) needs terminal S-parameters: one port per conductor, referenced to a common ground and a chosen reference impedance. This module converts one into the other using, for every physical port, the matrix of terminal voltages T_V and terminal currents T_I of its modes.

Conventions

Time convention exp(+j w t), as in the rest of gsim. A physical port p has n_p terminals and n_p modes kept.

  • Column k of T_V[p] holds the terminal voltages (terminal minus the reference conductor) of mode k for a unit forward modal wave. Column k of T_I[p] holds the terminal currents flowing into the network for that wave.
  • Modal waves: V = T_V (a + b) and I = T_I (a - b).
  • Terminal power waves for a real reference impedance Zr: a_t = F (V + Zr I) and b_t = F (V - Zr I) with F = diag(1 / (2 sqrt(Zr))). For real Zr these equal pseudo waves.
  • With block-diagonal T = diag(T_V[p]) and W = diag(T_I[p]), and A = F (T + Zr W), B = F (T - Zr W)::

    S_t = (B + A S_m) (A + B S_m)^-1 S_m = (A - S_t B)^-1 (S_t A - B)

  • Port ordering: physical port by physical port, terminals (or modes) in column order inside each port. A two-end line is ordered (A1, B1, A2, B2) and its modal S is (end 1 mode 1, end 1 mode 2, end 2 mode 1, end 2 mode 2).

Properties

  • Joint per-mode scaling. Scaling column k of T_V and T_I together by any complex constant leaves S_t unchanged when S_m has no cross-mode terms. The solver's modal normalisation therefore cancels; only the voltage/current shape of each mode matters.
  • Degenerate modes. If several modes share the same propagation constant, any basis of their subspace gives the same S_t. Mode identification can be unstable there; the terminal S is not.
  • Currents. For a reciprocal line T_I^T T_V is diagonal with entries r_k = V_k^T I_k (the unconjugated reaction), so T_I = T_V^{-T} diag(r), see :func:currents_from_reaction. Replacing r_k by the real power 2 P_k is only an approximation when the line is lossy.
  • Uniform line. The modal S of a uniform line is [[0, E], [E, 0]] with E = diag(exp(-gamma_k * length)), see :func:uniform_line_modal_s.

Example::

import numpy as np
from gsim.palace.terminal import modal_to_terminal_s, uniform_line_modal_s

# t_v, t_i: (nf, 2, 2) for each end; gamma: (nf, 2) in 1/m
s_modal = uniform_line_modal_s(gamma, length=700e-6)
s_term = modal_to_terminal_s(s_modal, [t_v, t_v], [t_i, t_i], z_ref=50.0)
# s_term is ordered (A1, B1, A2, B2)

Limits: real positive reference impedances only; cross-mode terms in the modal S must come from the solver (a multi-mode port solve).

Functions:

currents_from_reaction

currents_from_reaction(t_v: ArrayLike, reaction: ArrayLike) -> NDArray[complex128]

Terminal currents from terminal voltages and the modal reaction.

The reaction matrix is R = T_V^T T_I (unconjugated), so T_I = T_V^{-T} R. Pass the full matrix R whenever the modes may be mixed: within a degenerate group any basis is valid, and a mixed basis has nonzero off-diagonal reactions.

A vector r is read as R = diag(r) with r_k = V_k^T I_k. This holds only for a reaction-orthogonal basis (V_j^T I_k = 0 for j != k): distinct modes of a reciprocal line, or degenerate modes that were orthogonalised. Using the real power 2 P_k for r_k is exact only without loss.

Parameters:

  • t_v (ArrayLike) –

    (n, n) or (nf, n, n) terminal voltages (column = mode).

  • reaction (ArrayLike) –

    the full matrix R, (n, n) for a 2-D t_v or (nf, n, n); or the diagonal only, (n,) or (nf, n).

Returns:

  • NDArray[complex128] –

    T_I with the shape of t_v.

Raises:

  • ValueError –

    On shape mismatch, ill-conditioned t_v or non-finite input.

degenerate_mode_groups

degenerate_mode_groups(
    n_eff: ArrayLike, *, rtol: float = 1e-06
) -> list[tuple[int, ...]]

Group modes whose propagation constants agree.

Modes i and j are linked when |n_i - n_j| <= rtol * max(|n_i|, |n_j|); groups are the connected components (single linkage). Within a group the terminal S does not depend on the basis chosen for the modes.

Parameters:

  • n_eff (ArrayLike) –

    Effective indices (or propagation constants) at one frequency, shape (n,).

  • rtol (float, default: 1e-06 ) –

    Relative tolerance.

Returns:

  • list[tuple[int, ...]] –

    Groups of mode indices, each sorted, ordered by smallest index.

Raises:

  • ValueError –

    If n_eff is not 1-D or not finite.

modal_to_terminal_s

modal_to_terminal_s(
    s_modal: ArrayLike,
    t_v: Sequence[ArrayLike],
    t_i: Sequence[ArrayLike],
    *,
    z_ref: ArrayLike = 50.0,
) -> NDArray[complex128]

Convert modal S-parameters to terminal (power-wave) S-parameters.

Parameters:

  • s_modal (ArrayLike) –

    Modal S-parameters, shape (nf, M, M).

  • t_v (Sequence[ArrayLike]) –

    One terminal-voltage matrix per physical port, each (n_p, n_p) or (nf, n_p, n_p). Column k is mode k.

  • t_i (Sequence[ArrayLike]) –

    Terminal currents flowing into the network, same layout as t_v.

  • z_ref (ArrayLike, default: 50.0 ) –

    Real positive reference impedance in ohm: a scalar, one value per terminal (Nt,) or one per frequency and terminal (nf, Nt).

Returns:

  • NDArray[complex128] –

    Terminal S-parameters, shape (nf, Nt, Nt) with Nt = M. For real

  • NDArray[complex128] –

    z_ref power waves and pseudo waves coincide.

Raises:

  • ValueError –

    On shape or port-count mismatch, non-square or ill-conditioned t_v/t_i blocks (cond > 1e12, the modes do not span the terminals), non-positive or complex z_ref, or non-finite input.

terminal_to_modal_s

terminal_to_modal_s(
    s_terminal: ArrayLike,
    t_v: Sequence[ArrayLike],
    t_i: Sequence[ArrayLike],
    *,
    z_ref: ArrayLike = 50.0,
) -> NDArray[complex128]

Convert terminal S-parameters back to modal S-parameters.

Inverse of :func:modal_to_terminal_s; same arguments, conventions and errors, with s_terminal of shape (nf, Nt, Nt).

uniform_line_modal_s

uniform_line_modal_s(gamma: ArrayLike, length: float) -> NDArray[complex128]

Modal S-parameters of a uniform multi-mode line, no cross-mode terms.

S = [[0, E], [E, 0]] with E = diag(exp(-gamma_k * length)).

Parameters:

  • gamma (ArrayLike) –

    Propagation constants in 1/m, shape (nf, n) (or (n,)).

  • length (float) –

    Line length in m.

Returns:

  • NDArray[complex128] –

    (nf, 2n, 2n) ordered (end 1 modes, end 2 modes).

Raises:

  • ValueError –

    If Re(gamma) < 0 beyond round-off (a mode that grows along the line), if gamma is not finite, or if length is negative or not finite.