thliebig / CSXCAD

A C++ library to describe geometrical objects and their physical or non-physical properties.
http://openEMS.de
GNU Lesser General Public License v3.0
34 stars 38 forks source link

Complete round-tripping CSXCAD files: Write2XML / ReadFromXML and CSPropLumpedElement vs LumpedPort #39

Open oberstet opened 10 months ago

oberstet commented 10 months ago

I want to round-trip openEMS simulations via CSXCAD files stored via Write2XML and re-created using ReadFromXML.

Doing so required me to create a custom class LumpedPortExt - code is below.

I have tested this with the CSXCAD created from the tutorial "Simple Patch" antenna: https://gist.github.com/oberstet/cf93ac19332ccbedeb1d414b461b9692

There is only a minor question left, pls see FIXME in the code below.

Should there be interest in merging, please let me know, I could push a PR.

Otherwise this might still help other users, hence I am posting this as an issue.


There is a problem doing so, since a CSXCAD.CSProperties.CSPropLumpedElement and other elements are stored in CSXCAD, but NOT the openEMS.ports.LumpedPort (as its own serialized object refering to the 4 objects created).

When originally calling AddLumpedPort, e.g. as in Simple Patch Antenna

# apply the excitation & resist as a current source
start = [feed_pos, 0, 0]
stop = [feed_pos, 0, substrate_thickness]
port = FDTD.AddLumpedPort(1, feed_R, start, stop, "z", 1.0, priority=5, edges2grid="xy")

this will actually create 4 objects in the CSXCAD continuous structure

which gets serialized into XML via Write2XML like so

        <LumpedElement ID="3" Name="port_resist_1" Direction="2" Caps="1" R="5.000000e+01" C="nan" L="nan" LEtype="-1.000000e+00">
            <FillColor R="205" G="186" B="171" a="255" />
            <EdgeColor R="205" G="186" B="171" a="255" />
            <Primitives>
                <Box Priority="5">
                    <P1 X="-6.000000e+00" Y="0.000000e+00" Z="0.000000e+00" />
                    <P2 X="-6.000000e+00" Y="0.000000e+00" Z="1.524000e+00" />
                </Box>
            </Primitives>
        </LumpedElement>
        <Excitation ID="4" Name="port_excite_1" Number="0" Frequency="0.000000e+00" Delay="0.000000e+00" Type="0" Excite="0.000000e+00,0.000000e+00,-1.000000e+00" PropDir="0.000000e+00,0.000000e+00,0.000000e+00">
            <FillColor R="242" G="251" B="227" a="255" />
            <EdgeColor R="242" G="251" B="227" a="255" />
            <Primitives>
                <Box Priority="5">
                    <P1 X="-6.000000e+00" Y="0.000000e+00" Z="0.000000e+00" />
                    <P2 X="-6.000000e+00" Y="0.000000e+00" Z="1.524000e+00" />
                </Box>
            </Primitives>
            <Weight X="1.000000e+00" Y="1.000000e+00" Z="1.000000e+00" />
        </Excitation>
        <ProbeBox ID="5" Name="port_ut_1" Number="0" Type="0" Weight="-1" NormDir="-1" StartTime="0" StopTime="0">
            <FillColor R="70" G="124" B="194" a="255" />
            <EdgeColor R="70" G="124" B="194" a="255" />
            <Primitives>
                <Box Priority="0">
                    <P1 X="-6.000000e+00" Y="0.000000e+00" Z="0.000000e+00" />
                    <P2 X="-6.000000e+00" Y="0.000000e+00" Z="1.524000e+00" />
                </Box>
            </Primitives>
        </ProbeBox>
        <ProbeBox ID="6" Name="port_it_1" Number="0" NormDir="2" Type="1" Weight="1" StartTime="0" StopTime="0">
            <FillColor R="84" G="248" B="27" a="255" />
            <EdgeColor R="84" G="248" B="27" a="255" />
            <Primitives>
                <Box Priority="0">
                    <P1 X="-6.000000e+00" Y="0.000000e+00" Z="7.620000e-01" />
                    <P2 X="-6.000000e+00" Y="0.000000e+00" Z="7.620000e-01" />
                </Box>
            </Primitives>
        </ProbeBox>

However, when reading in such XML again via ReadFromXML, this will re-create the 4 individual objects

but it will not re-created a LumpedPort!!

Specifically, it will create a CSXCAD.CSProperties.CSPropLumpedElement but NOT a openEMS.ports.LumpedPort


from typing import List

import numpy as np

from CSXCAD import ContinuousStructure
from CSXCAD.CSProperties import CSProperties, CSPropLumpedElement
from CSXCAD.CSPrimitives import CSPrimBox
from CSXCAD.Utilities import CheckNyDir
from openEMS.ports import LumpedPort

class LumpedPortExt(LumpedPort):
    """
    Extension class for ``LumpedPort`` which allows to instantiate a lumped port from
    the CSXCAD continuous structure (as serialized in CSXCAD XML files) based on the
    port number only, and assuming element names like openEMS creates.

    For example, when creating a ``LumpedPort`` with ``port_nr == 1``, this will create
    the following **four** CSXCAD objects (assuming no ``PortNamePrefix`` is used):

    - a *LumpedElement* element named ``"port_resist_1"``
    - an *Excitation* element named ``"port_excite_1"``
    - a *ProbeBox* element named ``"port_ut_1"``
    - a *ProbeBox* element named ``"port_it_1"`

    .. seealso::
        - `openEMS.openEMS.AddLumpedPort <https://docs.openems.de/python/openEMS/openEMS.html#openEMS.openEMS.AddLumpedPort>`_  # noqa
        - `openEMS.ports.LumpedPort <https://docs.openems.de/python/openEMS/ports.html#openEMS.ports.LumpedPort>`_
    """

    def __init__(self, CSX: ContinuousStructure, port_nr: int, **kw):
        """

        :param CSX: continuous structure
        :param port_nr: port number
        """
        if "PortNamePrefix" in kw:
            prefix = kw["PortNamePrefix"]
        else:
            prefix = ""
        lbl_temp = prefix + "port_{}" + "_{}".format(port_nr)

        elm_name = lbl_temp.format("resist")
        elm_props: List[CSProperties] = CSX.GetPropertiesByName(elm_name)

        if len(elm_props) != 1:
            raise RuntimeError(
                'unexpected element count {} for property "{}" - there must be exactly 1'.format(
                    len(elm_props), elm_name
                )
            )

        elm_prop: CSProperties = elm_props[0]
        elm_type = elm_prop.GetTypeString()

        match elm_type:
            case "LumpedElement":
                port: CSPropLumpedElement = elm_prop  # noqa
                assert port.GetQtyPrimitives() == 1
                pbox: CSPrimBox = port.GetAllPrimitives()[0]

                # FIXME:
                #  How to get this stuff from a CSPropLumpedElement originally created from a LumpedPort, and
                #    saved to  CSXCAD file?
                #  This isn't stored in CSXCAD at all, right?
                excite = 1.0
                if "priority" not in kw:
                    kw["priority"] = 5

                # we cannot just call our direct base class constructor, since that will create new
                # object in the CSX continuous structure - which we do not want, since the premise is
                # our CSX has already all objects, e.g. like what ReadFromXML() does create. we _only_ want
                # to create a LumpedPort Python side wrapping object
                #
                # super(LumpedPortExt, self).__init__(
                #     CSX,
                #     port_nr=port_nr,
                #     R=port.GetResistance(),
                #     start=pbox.GetStart(),
                #     stop=pbox.GetStop(),
                #     exc_dir=port.GetDirection(),
                #     **kw,
                # )
                #
                # because of above, we call the indirect base constructor (Port.__init__) and
                # manually initialize the LumpedPort object
                #
                super(LumpedPort, self).__init__(
                    CSX, port_nr=port_nr, start=pbox.GetStart(), stop=pbox.GetStop(), excite=excite, **kw
                )
                self.R = port.GetResistance()
                self.exc_ny = CheckNyDir(port.GetDirection())
                self.direction = np.sign(self.stop[self.exc_ny] - self.start[self.exc_ny])
                if not self.start[self.exc_ny] != self.stop[self.exc_ny]:
                    raise Exception("LumpedPort: start and stop may not be identical in excitation direction")
                self.U_filenames = [
                    self.lbl_temp.format("ut"),
                ]
                self.I_filenames = [
                    self.lbl_temp.format("it"),
                ]

            case "Metal":
                raise NotImplementedError("Metal property not implemented")
            case _:
                raise RuntimeError('unexpected property type "{}" for property "{}"'.format(elm_type, elm_name))