Custom Classes

September 16, 2026 · View on GitHub

If you want to simulate and predict your own geometry, you need to create three files:

  • A python file extending the BaseGeometry class and implementing the required methods.
  • A stackup XML file defining the layers of your geometry.
  • A simulation configuration file defining the simulation parameters for Palace.

Once you have these three files, you can head over to Running ORCA to see how to run ORCA with these files.

Python Class

The Python class should be a @dataclass extending orca.BaseGeometry and must implement the following:

Required dataclass fields:

  • name: str — Unique identifier for the geometry (used as directory name and file prefix).
  • stackup_xml: str — Path to the stackup XML file describing the physical layer stack.
  • simconfig_filename: str — Path to the Palace simulation configuration file (.simcfg).
  • input_parameter_iterator: InputParameterIterator — Defines geometry parameters and their sampling ranges.

!!! warning "Give the object-valued field a default_factory"

`input_parameter_iterator` must be declared with `field(default_factory=...)`,
as in the example below. Writing `= InputParameterIterator(...)` instead builds
**one** object shared by every instance of the class.

!!! tip "Choosing an output representation"

The `codec` passed to the dataset decides what the model regresses. `FlatReImCodec`
predicts all `N x N` entries; `UpperTriangleReImCodec` predicts only the upper
triangle and mirrors it on decode, which halves the output dimension and makes
`S = S^T` structural. Use it for reciprocal passives (any ordinary inductor or
transformer), and `FlatReImCodec` for anything non-reciprocal. Note that this
changes the ONNX output names. See `src/orca/training/README.md`.

!!! note "Basis expansions are not set on the geometry"

Engineered inputs such as a Chebyshev expansion of frequency belong to the
model, not the geometry: pass `orca.ModelTrainer(model=..., basis="chebyshev")`.
That way the expansion is tuned with the model and traced into the exported ONNX
graph. See `src/orca/training/README.md`.

Required abstract methods:

  • create_gds_file(name, output_path, params) -> str — Generates a GDS layout file from geometry parameters. Returns the path to the created file.
  • create_dataset() -> BaseDataset — Builds the dataset (e.g. GeoToSParamDatasetSingleFrequency) with its output codec and normalizers, used for training. It is called once per geometry instance, the first time geometry.dataset is read, so each instance gets its own dataset and normalizer statistics.

!!! tip "Keep the training imports inside create_dataset()"

The dataset classes and normalizers need PyTorch, which is an optional
dependency of ORCA (the `train` extra). Import them inside `create_dataset()`,
as in the example below, and your geometry can still generate layouts and run
Palace simulations in an environment without PyTorch (e.g. an HPC cluster).

The model architecture and its hyperparameters are not part of the geometry: pass them to the training stage with orca.ModelTrainer(model=..., hyperparameters=...), or leave hyperparameters out to let ModelTrainer tune them with Optuna over the model's own search space.

Example:

# Builds a fresh iterator per geometry instance. See the warning above:
# a bare `= InputParameterIterator(...)` default would be shared by every instance.
def _input_parameters() -> InputParameterIterator:
    return InputParameterIterator(
        picking_strategy="random",
        frequency=[1e8, 500e8],  # 1 GHz to 500 GHz
        bottom_winding_diameter=[x / 10 for x in range(200, 1201, 1)],  # 20.0 to 120.0 in 0.1 steps
        top_winding_diameter=[x / 10 for x in range(200, 1201, 1)],  # 20.0 to 120.0 in 0.1 steps
        center_displacement=[x / 10 for x in range(0, 151, 1)],   # 0.0 to 15.0 in 0.1 steps
        bottom_linewidth=[x / 10 for x in range(20, 121, 1)],     # 2.0 to 12.0 in 0.1 steps
        top_linewidth=[x / 10 for x in range(20, 121, 1)],        # 2.0 to 12.0 in 0.1 steps
    )


@dataclass
class TransformerOcta(BaseGeometry):
    """
    Represents a transformer geometry with octagonal shape.
    """

    name: str = "tf_octa_c_ports"
    stackup_xml: str = os.path.join(os.path.dirname(__file__), "SG13G2_nosub.xml")
    simconfig_filename: str = os.path.join(os.path.dirname(__file__), "tf_octa_c_ports.simcfg")
    input_parameter_iterator: InputParameterIterator = field(default_factory=_input_parameters)

    def create_dataset(self) -> "BaseDataset":
        # Imported here so the geometry works without PyTorch (see the tip above).
        from orca.training.codecs import FlatReImCodec
        from orca.training.datasets.geo_to_s_param_single_f import GeoToSParamDatasetSingleFrequency
        from orca.training.normalize import MinMaxNormalizer, StandardNormalizer

        return GeoToSParamDatasetSingleFrequency(
            codec=FlatReImCodec(n_ports=6),
            # Scale inputs with the declared parameter ranges rather than the
            # min/max of whatever subset happens to be trained on.
            input_normalizer=MinMaxNormalizer(self.input_parameter_iterator),
            output_normalizer=StandardNormalizer(),
        )

    @staticmethod
    def create_gds_file(name: str, output_path: str, params: dict[str, Any]) -> str:
        # 
        # < Put your actual GDS generation code here >
        #
        c.write_gds(output_path, with_metadata=False)
        return output_path

Reference: InputParameterIterator

InputParameterIterator defines the set of geometry parameters and how they are sampled during GDS generation.

InputParameterIterator(
    picking_strategy="random",  # "grid" / "uniform_grid", "step_grid", or "random"
    frequency=[1e8, 500e8],     # Optional: frequency range included for normalisation (not iterated)
    param_a=[...],              # List/range of possible values for each geometry parameter
    param_b=[...],
)

Picking strategies:

StrategyBehaviour
"grid" / "uniform_grid"Uniform grid across all parameter combinations
"step_grid"Grid using explicit step sizes
"random"Random sampling without replacement

Reference: Dataset Types

ClassDescription
GeoToSParamDatasetSingleFrequencyOne training sample per frequency point per geometry (recommended)
GeoToSParamDatasetOne training sample per geometry (full frequency sweep as a vector)

Reference: Basis Expansions

Basis expansions widen the model's input before its first layer by appending fixed basis functions of it. Nothing about them is learned. They belong to the model, not the geometry, so they are chosen on the training stage — orca.ModelTrainer(model="mlp", basis="chebyshev") — and work with any architecture. Because the expansion is part of the model, it is tuned with it and traced into the exported ONNX graph; the ONNX input names stay the same.

ClassNameDescription
IdentityBasis"identity"Passes inputs through unchanged (the default)
ChebyshevBasis"chebyshev"Appends degree Chebyshev polynomials of one column, frequency by default

ChebyshevBasis tunes basis_degree automatically when no hyperparameters are supplied. It sees normalized inputs and clamps to [-1, 1], so a frequency outside the training range saturates rather than diverging.

Reference: Normalizers

Input normalizers (passed as input_normalizer to the dataset):

ClassDescription
MinMaxNormalizer(input_parameter_iterator)Min-max normalisation with the declared parameter ranges (recommended: independent of which samples are trained on, and matches the input_parameter_ranges metadata of the exported ONNX model)
OutputMinMaxNormalizerMin-max normalisation fitted to the min/max of the training split

Output normalizers (passed as output_normalizer to the dataset):

ClassDescription
StandardNormalizerZ-score normalisation (zero mean, unit variance)
OutputMinMaxNormalizerMin-max normalisation

Stackup XML File

There are multiple examples of stackup XML files in the src/orca/geometry/examples/ directory. Often these are sufficient to get started. You can also create your own stackup XML file or adjust one of the examples here to fit your needs.

Example:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
  <Stackup schemaVersion="2.0">
    <Materials>
      <Material Name="Activ" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="357141.0" Color="00ff00"/>
      <Material Name="Metal1" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="21640000.0" Color="39bfff"/>
      <Material Name="Metal2" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="23190000.0" Color="ccccd9"/>
      <Material Name="TopMetal1" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="27800000.0" Color="ffe6bf"/>
      <Material Name="TopMetal2" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="30300000.0" Color="ff8000"/>
      <Material Name="TopVia1" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="2191000.0" Color="ffe6bf"/>
      <Material Name="Via2" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="1660000.0" Color="ff3736"/>
      <Material Name="Via1" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="1660000.0" Color="ccccff"/>
      <Material Name="Cont" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="2390000.0" Color="00ffff"/>
      <Material Name="Passive" Type="Dielectric" Permittivity="6.6" DielectricLossTangent="0.0" Conductivity="0" Color="a0a0f0"/>
      <Material Name="SiO2" Type="Dielectric" Permittivity="4.1" DielectricLossTangent="0.0" Conductivity="0" Color="fffcad"/>
      <Material Name="AIR" Type="Dielectric" Permittivity="1.0" DielectricLossTangent="0.0" Conductivity="0" Color="d0d0d0"/>
      <Material Name="Vmim" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="2191000.0" Color="ffe6bf"/>
      <Material Name="MIM" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="500000.0" Color="e6ffbf"/>
      <Material Name="LOWLOSS" Type="Conductor" Permittivity="1" DielectricLossTangent="0" Conductivity="1E10" Color="ff0000"/>
    </Materials>
    <ELayers LengthUnit="um">
      <Dielectrics>
        <Dielectric Name="AIR" Material="AIR" Thickness="200.0000" />
        <Dielectric Name="Passive" Material="Passive" Thickness="0.4000" />
        <Dielectric Name="SiO2" Material="SiO2" Thickness="15.7303" />
        <Dielectric Name="EPI" Material="EPI" Thickness="3.7500" />
        <Dielectric Name="Substrate" Material="Substrate" Thickness="180.0000" />
      </Dielectrics>
      <Layers>
        <Substrate Offset="183.75"/>
        <Layer Name="Activ" Type="conductor" Zmin="0.0000" Zmax="0.4000" Material="Activ" Layer="1" />
        <Layer Name="Metal1" Type="conductor" Zmin="1.0400" Zmax="1.4600" Material="Metal1" Layer="8" />
        <Layer Name="Metal2" Type="conductor" Zmin="2.0000" Zmax="2.4900" Material="Metal2" Layer="10" />
        <Layer Name="TopMetal1" Type="conductor" Zmin="6.4303" Zmax="8.4303" Material="TopMetal1" Layer="126" />
        <Layer Name="TopMetal2" Type="conductor" Zmin="11.2303" Zmax="14.2303" Material="TopMetal2" Layer="134" />
        <Layer Name="SUBGND" Type="conductor" Zmin="-3.75" Zmax="0" Material="LOWLOSS" Layer="250" />
        <Layer Name="BACKSIDEGND" Type="conductor" Zmin="-190" Zmax="-183.75" Material="LOWLOSS" Layer="251" />
        <Layer Name="MIM" Type="conductor" Zmin="5.6043" Zmax="5.7540" Material="MIM" Layer="36" />
        <Layer Name="Vmim" Type="via" Zmin="5.7540" Zmax="6.4303" Material="Vmim" Layer="129" />
        <Layer Name="LBE" Type="dielectric" Zmin="-183.75" Zmax="0" Material="Air" Layer="157" />
      </Layers>
    </ELayers>
  </Stackup>

Simulation Configuration File

The simulation configuration file (.simcfg) defines how to mesh and run the electromagnetic simulation in Palace. You can either tweak existing .simcfg files from the src/orca/geometry/examples/ directory, create your own from scratch, or use the GUI provided by setupEM to create the .simcfg file interactively.

Example:

{
    "application": "setupEM",
    "data_format": "1.0",
    "saved_values": {
        "preprocess_gds": true,
        "merge_polygon_size": 0.5,
        "purpose": [
            0
        ],
        "fstart": 0.0,
        "fstop": 170.0,
        "refined_cellsize": 2.0,
        "order": 2,
        "cells_per_wavelength": 20.0,
        "meshsize_max": 100.0,
        "adaptive_mesh_iterations": 0,
        "iterative": false,
        "boundary": [
            "PEC",
            "PEC",
            "PEC",
            "PEC",
            "PEC",
            "PEC"
        ],
        "margin": 200.0,
        "air_around": 200.0,
        "ELMER_MPI_THREADS": 4,
        "model_basename": "tf_octa_c_ports",
        "sim_path": "/home/users/simone/OpenSource_LNA",
        "fstep": 1.0
    },
    "ports": [
        {
            "portnumber": 1,
            "source_layernum": 201,
            "target_layername": null,
            "from_layername": "Metal5",
            "to_layername": "TopMetal1",
            "direction": "Z",
            "port_Z0": 50.0,
            "voltage": 1.0
        },
        {
            "portnumber": 2,
            "source_layernum": 204,
            "target_layername": null,
            "from_layername": "Metal5",
            "to_layername": "TopMetal1",
            "direction": "Z",
            "port_Z0": 50.0,
            "voltage": 1.0
        },
        {
            "portnumber": 3,
            "source_layernum": 202,
            "target_layername": null,
            "from_layername": "Metal5",
            "to_layername": "TopMetal2",
            "direction": "Z",
            "port_Z0": 50.0,
            "voltage": 1.0
        },
        {
            "portnumber": 4,
            "source_layernum": 203,
            "target_layername": null,
            "from_layername": "Metal5",
            "to_layername": "TopMetal2",
            "direction": "Z",
            "port_Z0": 50.0,
            "voltage": 1.0
        }
    ]
}