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
36 stars 39 forks source link

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

Open oberstet opened 11 months ago

oberstet commented 11 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))
thliebig commented 3 weeks ago

Thank you for this and I can see the issue. But I'm not sure yet how to solve this correctly. CSXCAD and openEMS are supposed to be separate projects as CSXCAD can and should be usable with other solvers, e.g. a static field solver. And this lumped port is more or less an FDTD version of this port and CSXCAD should not really be bothered with the details. Thus this implementation should happen in openEMS and openEMS should be able to write the xml file and add details e.g. about all the different ports and how they reference CSXCAD (lower level) objects. Does that make sense?

oberstet commented 3 weeks ago

And this lumped port is more or less an FDTD version of this port and CSXCAD should not really be bothered with the details.

Talking about "lumped port" in general - thus, not the specific flavor used by openEMS - even these are considered NOT be part of what CSXCAD addresses in its scope?

I am really a bit of an outsider with a fragment slice of perspective on EM and openEMS:

"lumped ports" are

but they are required for any kind of simulation?

is there a "simulation level" file format - if that is not what CSXCAD also wants to cover?


put differently, for geometry alone, STL / STEP are enough, and CSXCAD is not needed, but for a complete simulation - a definition which I can roundtrip - CSXCAD is required, but not enough? catch22? ;)

thliebig commented 3 weeks ago

A lumped port in general could be part of CSXCAD, but all the sub-objects created by openEMS do only make sense for FDTD. And then openEMS does offer lots more ports that make even less sense for other types of field solvers.

openEMS can and does create its own xml file where CSXCAD is only a part of. I think this would be the correct place to save/load higher level objects like ports. I will have a look into it because the python interface currently cannot do this.

CSXCAD itself is indeed only meant to be a CAD tool. But for me STL/Step was only a last resort. Usually everything is defines as boxed, polygons and e.g. cylinders and CSXCAD offers an easy interface to all that.

oberstet commented 3 weeks ago

openEMS can and does create its own xml file where CSXCAD is only a part of. I think this would be the correct place to save/load higher level objects like ports.

fwiw, and as far as I understand topics / matters, I agree, sounds good +1!