Skip to content

Setting up a regular surface

In this tutorial we will show how to represent a regular surface in RESQML v2.0.1 using resqml_objects which is installed alongside pyetp.

Warning

Due to the flexibility of RESQML there are multiple ways to represent a regular surface. Different providers make different choices, and there is no guarantee that their software will understand all the possible ways of representing a surface. If compatibility towards a software platform is important, make sure that you write your surfaces in such a way that it can be read by that platform.

To represent a regular surface we utilize three objects from RESQML v2.0.1. They are:

  1. An obj_EpcExternalPartReference which is a RESQML v2.0.1 specific object to explain (loosely speaking) that the raw array data is stored alongside the objects. This object is needed when uploading data to an RDDMS server.
  2. A local coordinate system, either obj_LocalDepth3dCrs if the surface describes depth in units of distance or obj_LocalTime3dCrs if it is in units of time.
  3. An obj_Grid2dRepresentation for the actual grid metadata and references to 1. and 2. above. We will often denote the obj_Grid2dRepresentation as "the grid-object".

There are two important considerations when setting up these objects. The first is that multiple obj_Grid2dRepresentation can reference the same coordinate system and obj_EpcExternalPartReference. Secondly, RESQML allows both active and passive transformations of the surface data. That is, the coordinates of the surface is described by both the coordinate system and the grid-object. The coordinate system describes passive transformations and the grid-object active transformations.

We will demonstrate a few variations for setting up these objects below.

Info

We call a collection of linked RESQML objects a model. For example, a regular surface consisting of an obj_EpcExternalPartReference, an obj_LocalDepth3dCrs and an obj_Grid2dRepresentation constitutes a model.

Single, stand-alone surface in an unrotated coordinate system

The first example we show is for a regular surface that is not connected to any other RESQML-objects, and where we keep any potential rotation in the obj_Grid2dRepresentation-object and leave the coordinate system unrotated and aligned with the global coordinate system (which in this case is an EPSG-code).

This example demonstrates the minimally required information needed to represent a regular surface in RESQML v2.0.1.

Tip

The constructed objects are normal Python dataclasses, and can be printed to console for a good overview of the content and default values. Use rich for pretty-printing. It is included as a sub-dependency in pyetp.

In this example we will use a fixed creation datetime, and fixed uuids for the three objects. These parameters are optional. If not specified the creation-field in the Citation-object will be set to datetime.datetime.now(datetime.timezone.utc), and the uuids will be set to a random str(uuid.uuid4())-value.

import datetime

import numpy as np

import resqml_objects.v201 as ro


originator = "<name/username/email>"

creation = datetime.datetime(2026, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)

epc_uuid = "d53c9c04-e83a-4ad7-87fc-567a0dd5e660"
crs_uuid = "dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62"
gri_uuid = "be3dc02d-ed9d-45b1-b291-6edced323411"
The first object we set up is the obj_EpcExternalPartReference-object.
epc = ro.obj_EpcExternalPartReference(
    citation=ro.Citation(
        title="Demo epc",
        originator=originator,
        creation=creation,
    ),
    uuid=epc_uuid,
)

Printing the obj_EpcExternalPartReference-object

We can use print(epc) directly, or rich.print(epc) from the rich library to get an impression on the different fields of the object.

obj_EpcExternalPartReference(
    citation=Citation(
        title='Demo epc',
        originator='<name/username/email>',
        creation=XmlDateTime(2026, 1, 2, 3, 4, 5, 0, 0),
        format='equinor:pyetp:0.0.49.dev12+ga521a78a1',
        editor=None,
        last_update=None,
        version_string=None,
        description=None,
        descriptive_keywords=None
    ),
    aliases=[],
    custom_data=None,
    schema_version='2.0',
    uuid='d53c9c04-e83a-4ad7-87fc-567a0dd5e660',
    object_version=None,
    mime_type='application/x-hdf5'
)
To see the corresponding XML representation we use serialize_resqml_v201_object.
<eml:EpcExternalPartReference xmlns:eml="http://www.energistics.org/energyml/data/commonv2" xmlns:resqml2="http://www.energistics.org/energyml/data/resqmlv2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="2.0" uuid="d53c9c04-e83a-4ad7-87fc-567a0dd5e660" xsi:type="eml:obj_EpcExternalPartReference">
  <eml:Citation>
    <eml:Title>Demo epc</eml:Title>
    <eml:Originator>&lt;name/username/email&gt;</eml:Originator>
    <eml:Creation>2026-01-02T03:04:05Z</eml:Creation>
    <eml:Format>equinor:pyetp:0.0.49.dev12+ga521a78a1</eml:Format>
  </eml:Citation>
  <eml:MimeType>application/x-hdf5</eml:MimeType>
</eml:EpcExternalPartReference>
See the documentation for Citation and obj_EpcExternalPartReference (and for its superclasses) for an explanation of the various fields.

Setting up a coordinate system

Here we set up a default obj_LocalDepth3dCrs. That is, we leave the local coordinate system untransformed relative to the global coordinate system, we choose our first axis to describe eastings and our second axis northings, and let the \(z\)-axis point downwards.

crs = ro.obj_LocalDepth3dCrs(
    citation=ro.Citation(
        title="Demo crs",
        originator=originator,
        creation=creation,
    ),
    uuid=crs_uuid,
    vertical_crs=ro.VerticalUnknownCrs(unknown="Mean sea level"),
    projected_crs=ro.ProjectedCrsEpsgCode(epsg_code=23031),
)
In this example we have chosen a nondescript "Mean sea level" for the vertical coordinate system, and the EPSG code 23031 covering much of Europe and the North Sea.

Choosing a global coordinate system

A global coordinate system is described by the two fields vertical_crs and projected_crs, and the local coordinate system describes additional transformations on top of this global system. There are only three built-in choices (for RESQML v2.0.1) for the field vertical_crs and projected_crs. These are:

  1. VerticalCrsEpsgCode and ProjectedCrsEpsgCode where the global coordinate system is described via an EPSG code.
  2. GmlVerticalCrsDefinition and GmlProjectedCrsDefinition with the global coordinate system described by the Geography Markup Language (GML).
  3. VerticalUnknownCrs and ProjectedUnknownCrs, which are used when the global coordinate system is irrelevant or anonymized.

If the global coordinate system is not an EPSG code or in GML, we use the unknown-option and add a custom coordinate system description, e.g., a well-known text, in the custom_data-field of the obj_LocalDepth3dCrs or obj_LocalTime3dCrs. See here for a wider discussion of coordinate systems in RESQML and their relation to OSDU coordinate systems.

Printing the obj_LocalDepth3dCrs-object

Printing the crs-object with rich.print(crs) we get:

obj_LocalDepth3dCrs(
    citation=Citation(
        title='Demo crs',
        originator='<name/username/email>',
        creation=XmlDateTime(2026, 1, 2, 3, 4, 5, 0, 0),
        format='equinor:pyetp:0.0.49.dev12+ga521a78a1',
        editor=None,
        last_update=None,
        version_string=None,
        description=None,
        descriptive_keywords=None
    ),
    aliases=[],
    custom_data=None,
    schema_version='2.0.1',
    uuid='dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62',
    object_version=None,
    extra_metadata=[],
    yoffset=0.0,
    zoffset=0.0,
    areal_rotation=PlaneAngleMeasure(value=0.0, uom=<PlaneAngleUom.RAD: 'rad'>),
    projected_axis_order=<AxisOrder2d.EASTING_NORTHING: 'easting northing'>,
    projected_uom=<LengthUom.M: 'm'>,
    vertical_uom=<LengthUom.M: 'm'>,
    xoffset=0.0,
    zincreasing_downward=True,
    vertical_crs=VerticalUnknownCrs(unknown='Mean sea level'),
    projected_crs=ProjectedCrsEpsgCode(epsg_code=23031)
)
and the corresponding serialized XML:
<resqml2:LocalDepth3dCrs xmlns:eml="http://www.energistics.org/energyml/data/commonv2" xmlns:resqml2="http://www.energistics.org/energyml/data/resqmlv2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="2.0.1" uuid="dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62" xsi:type="resqml2:obj_LocalDepth3dCrs">
  <eml:Citation>
    <eml:Title>Demo crs</eml:Title>
    <eml:Originator>&lt;name/username/email&gt;</eml:Originator>
    <eml:Creation>2026-01-02T03:04:05Z</eml:Creation>
    <eml:Format>equinor:pyetp:0.0.49.dev12+ga521a78a1</eml:Format>
  </eml:Citation>
  <resqml2:YOffset>0.0</resqml2:YOffset>
  <resqml2:ZOffset>0.0</resqml2:ZOffset>
  <resqml2:ArealRotation uom="rad">0.0</resqml2:ArealRotation>
  <resqml2:ProjectedAxisOrder>easting northing</resqml2:ProjectedAxisOrder>
  <resqml2:ProjectedUom>m</resqml2:ProjectedUom>
  <resqml2:VerticalUom>m</resqml2:VerticalUom>
  <resqml2:XOffset>0.0</resqml2:XOffset>
  <resqml2:ZIncreasingDownward>true</resqml2:ZIncreasingDownward>
  <resqml2:VerticalCrs xsi:type="eml:VerticalUnknownCrs">
    <eml:Unknown>Mean sea level</eml:Unknown>
  </resqml2:VerticalCrs>
  <resqml2:ProjectedCrs xsi:type="eml:ProjectedCrsEpsgCode">
    <eml:EpsgCode>23031</eml:EpsgCode>
  </resqml2:ProjectedCrs>
</resqml2:LocalDepth3dCrs>
We note that the obj_LocalDepth3dCrs-object by default has zero offset (xoffset == yoffset == zoffset == 0.0) and zero rotation (areal_rotation = ro.PlaneAngleMeasure(value=0.0, uom=ro.PlaneAngleUom.RAD)). The units for the vertical axis and the projected axes is set to meters (vertical_uom=ro.LengthUom.M and projected_uom=ro.LengthUom.M, respectively), and the surface is expected to have the \(z\)-axis pointing downwards (zincreasing_downward=True).

Setting up the grid object

A regular surface is a gridded two-dimensional height map with coordinates that have uniform spacing along each plane axis. The height map will be given as a dense array whereas the coordinates can be compactly represented as an origin, a shape, a spacing, and a rotation angle or a pair of orthonormal vectors.

Arrays in RESQML

In RESQML the array data is stored alongside the objects, and the objects keep a reference to the arrays. The reference to the array is a combination of the uri of an obj_EpcExternalPartReference and a key called path_in_hdf_file (typically in RESQML) or path_in_resource (in ETP). The obj_EpcExternalPartReference acts as a proxy to an external storage location, often a HDF5-file (this applies only when the data is stored to disk), and the path_in_hdf_file is a key into that HDF5-file. If multiple arrays are stored in the same HDF5-file, then the objects that own these arrays must point to the same obj_EpcExternalPartReference and use unique path_in_hdf_file-keys.

In this example we set up a random height map z and coordinates defined by

z = np.random.random((101, 103))
origin = np.array([10.0, 11.0])
spacing = np.array([1.0, 0.9])
u1 = np.array([np.sqrt(3.0) / 2.0, 0.5])
u2 = np.array([-0.5, np.sqrt(3.0) / 2.0])
This describes a regular surface with \(101 \times 103\) elements, that has its first axis rotated by an angle \(\pi / 6\) counter-clockwise to the first axis described by the crs-object (the u1 unit vector), and the second axis rotated by an additional angle \(\pi / 2\) (the u2 unit vector). The origin of the surface is placed in \((10, 11)\) relative to the offset in the crs-object and in the same units as listed in the crs-object. Finally, the spacing-array gives the spacing between points for the first axis and the second axis.

Mapping out the coordinates

The coordinates of the regular surface, \(\mathbf{r}_{ij}\), are given by $$ \mathbf{r}_{ij} = \mathbf{r}_0 + i \delta_1 \mathbf{u}_1 + j \delta_2 \mathbf{u}_2, $$ where \(\mathbf{r}_0\) corresponds to the origin, \(\delta_1\) and \(\delta_2\) the first and second component of the spacing-array, \(\mathbf{u}_1\) and \(\mathbf{u}_2\) the two unit vectors, and \(i\) and \(j\) are integers limited to the shape (z.shape) of the surface array. In total this will give coordinates \(\mathbf{r}_{ij}\) represented in the given local coordinate system.

Note that the height map z is not included in any of the objects, only the shape is stored in the gri-object.

Warning

Rotation and translation is stored two places for a regular surface in RESQML v2.0.1. Rotation is stored as an angle in the obj_LocalDepth3dCrs or obj_LocalTime3dCrs, and it is stored as a pair of unit vectors for the coordinates in the obj_Grid2dRepresentation. Translation is stored as xoffset, yoffset, and zoffset in the obj_LocalDepth3dCrs and obj_LocalTime3dCrs and as origin in the obj_Grid2dRepresentation. As seen in the output of the the crs-object the default local coordinate system has zero rotation and zero offset relative to the global coordinate system.

We use the classmethod obj_Grid2dRepresentation.from_regular_surface to set up the RESQML-object. This method is opinionated in choosing a specific set of RESQML array types.

gri = ro.obj_Grid2dRepresentation.from_regular_surface(
    citation=ro.Citation(
        title="Demo grid",
        originator=originator,
        creation=creation,
    ),
    uuid=gri_uuid,
    crs=crs,
    epc_external_part_reference=epc,
    shape=z.shape,
    origin=origin,
    spacing=spacing,
    unit_vec_1=u1,
    unit_vec_2=u2,
)

Printing the obj_Grid2dRepresentation-object

Printing the gri-object with rich.print(gri) we get:

obj_Grid2dRepresentation(
    citation=Citation(
        title='Demo grid',
        originator='<name/username/email>',
        creation=XmlDateTime(2026, 1, 2, 3, 4, 5, 0, 0),
        format='equinor:pyetp:0.0.49.dev12+ga521a78a1',
        editor=None,
        last_update=None,
        version_string=None,
        description=None,
        descriptive_keywords=None
    ),
    aliases=[],
    custom_data=None,
    schema_version='2.0.1',
    uuid='be3dc02d-ed9d-45b1-b291-6edced323411',
    object_version=None,
    extra_metadata=[],
    represented_interpretation=None,
    surface_role=<SurfaceRole.MAP: 'map'>,
    boundaries=[],
    grid2d_patch=Grid2dPatch(
        patch_index=0,
        fastest_axis_count=103,
        slowest_axis_count=101,
        geometry=PointGeometry(
            time_index=None,
            local_crs=DataObjectReference(
                content_type='application/x-resqml+xml;version=2.0.1;type=obj_LocalDepth3dCrs',
                title='Demo crs',
                uuid='dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62',
                uuid_authority=None,
                version_string=None
            ),
            points=Point3dZValueArray(
                supporting_geometry=Point3dLatticeArray(
                    all_dimensions_are_orthogonal=None,
                    origin=Point3d(coordinate1=10.0, coordinate2=11.0, coordinate3=0.0),
                    offset=[
                        Point3dOffset(
                            offset=Point3d(coordinate1=0.8660254037844386, coordinate2=0.5, coordinate3=0.0),
                            spacing=DoubleConstantArray(value=1.0, count=100)
                        ),
                        Point3dOffset(
                            offset=Point3d(coordinate1=-0.5, coordinate2=0.8660254037844386, coordinate3=0.0),
                            spacing=DoubleConstantArray(value=0.9, count=102)
                        )
                    ]
                ),
                zvalues=DoubleHdf5Array(
                    values=Hdf5Dataset(
                        path_in_hdf_file='/RESQML/be3dc02d-ed9d-45b1-b291-6edced323411/zvalues',
                        hdf_proxy=DataObjectReference(
                            content_type='application/x-eml+xml;version=2.0;type=obj_EpcExternalPartReference',
                            title='Demo epc',
                            uuid='d53c9c04-e83a-4ad7-87fc-567a0dd5e660',
                            uuid_authority=None,
                            version_string=None
                        )
                    )
                )
            ),
            seismic_coordinates=None
        )
    )
)
and the serialized XML:
<resqml2:Grid2dRepresentation xmlns:eml="http://www.energistics.org/energyml/data/commonv2" xmlns:resqml2="http://www.energistics.org/energyml/data/resqmlv2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" schemaVersion="2.0.1" uuid="be3dc02d-ed9d-45b1-b291-6edced323411" xsi:type="resqml2:obj_Grid2dRepresentation">
  <eml:Citation>
    <eml:Title>Demo grid</eml:Title>
    <eml:Originator>&lt;name/username/email&gt;</eml:Originator>
    <eml:Creation>2026-01-02T03:04:05Z</eml:Creation>
    <eml:Format>equinor:pyetp:0.0.49.dev12+ga521a78a1</eml:Format>
  </eml:Citation>
  <resqml2:SurfaceRole>map</resqml2:SurfaceRole>
  <resqml2:Grid2dPatch>
    <resqml2:PatchIndex>0</resqml2:PatchIndex>
    <resqml2:FastestAxisCount>103</resqml2:FastestAxisCount>
    <resqml2:SlowestAxisCount>101</resqml2:SlowestAxisCount>
    <resqml2:Geometry>
      <resqml2:LocalCrs>
        <eml:ContentType>application/x-resqml+xml;version=2.0.1;type=obj_LocalDepth3dCrs</eml:ContentType>
        <eml:Title>Demo crs</eml:Title>
        <eml:UUID>dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62</eml:UUID>
      </resqml2:LocalCrs>
      <resqml2:Points xsi:type="resqml2:Point3dZValueArray">
        <resqml2:SupportingGeometry xsi:type="resqml2:Point3dLatticeArray">
          <resqml2:Origin>
            <resqml2:Coordinate1>10.0</resqml2:Coordinate1>
            <resqml2:Coordinate2>11.0</resqml2:Coordinate2>
            <resqml2:Coordinate3>0.0</resqml2:Coordinate3>
          </resqml2:Origin>
          <resqml2:Offset>
            <resqml2:Offset>
              <resqml2:Coordinate1>0.8660254037844386</resqml2:Coordinate1>
              <resqml2:Coordinate2>0.5</resqml2:Coordinate2>
              <resqml2:Coordinate3>0.0</resqml2:Coordinate3>
            </resqml2:Offset>
            <resqml2:Spacing xsi:type="resqml2:DoubleConstantArray">
              <resqml2:Value>1.0</resqml2:Value>
              <resqml2:Count>100</resqml2:Count>
            </resqml2:Spacing>
          </resqml2:Offset>
          <resqml2:Offset>
            <resqml2:Offset>
              <resqml2:Coordinate1>-0.5</resqml2:Coordinate1>
              <resqml2:Coordinate2>0.8660254037844386</resqml2:Coordinate2>
              <resqml2:Coordinate3>0.0</resqml2:Coordinate3>
            </resqml2:Offset>
            <resqml2:Spacing xsi:type="resqml2:DoubleConstantArray">
              <resqml2:Value>0.9</resqml2:Value>
              <resqml2:Count>102</resqml2:Count>
            </resqml2:Spacing>
          </resqml2:Offset>
        </resqml2:SupportingGeometry>
        <resqml2:ZValues xsi:type="resqml2:DoubleHdf5Array">
          <resqml2:Values>
            <eml:PathInHdfFile>/RESQML/be3dc02d-ed9d-45b1-b291-6edced323411/zvalues</eml:PathInHdfFile>
            <eml:HdfProxy>
              <eml:ContentType>application/x-eml+xml;version=2.0;type=obj_EpcExternalPartReference</eml:ContentType>
              <eml:Title>Demo epc</eml:Title>
              <eml:UUID>d53c9c04-e83a-4ad7-87fc-567a0dd5e660</eml:UUID>
            </eml:HdfProxy>
          </resqml2:Values>
        </resqml2:ZValues>
      </resqml2:Points>
    </resqml2:Geometry>
  </resqml2:Grid2dPatch>
</resqml2:Grid2dRepresentation>
Other representations of a surface using obj_Grid2dRepresentation can differ in their choice of arrays types, starting from the field obj_Grid2dRepresentation.grid2d_patch.geometry.points (called Points in the XML output). In our implementation of resqml_objects we see that the XML attribute xsi:type is only included on the top-level element, and any element that supports multiple types.

Retrieving the coordinates

We have included a method, obj_Grid2dRepresentation.get_xy_grid, that simplifies the process of fleshing out the coordinate arrays X and Y from the representation of the obj_Grid2dRepresentation described above, and a given local coordinate system, viz.,

X, Y = gri.get_xy_grid(crs=crs)
This method is limited to the specific set of array types shown in the obj_Grid2dRepresentation-above.

Full script

We include the full script for the sake of completeness.

import datetime

import numpy as np

import resqml_objects.v201 as ro


originator = "<name/username/email>"

creation = datetime.datetime(2026, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)

epc_uuid = "d53c9c04-e83a-4ad7-87fc-567a0dd5e660"
crs_uuid = "dbe0e6ba-1ea6-4dd7-b541-9c4c14c16f62"
gri_uuid = "be3dc02d-ed9d-45b1-b291-6edced323411"

epc = ro.obj_EpcExternalPartReference(
    citation=ro.Citation(
        title="Demo epc",
        originator=originator,
        creation=creation,
    ),
    uuid=epc_uuid,
)

crs = ro.obj_LocalDepth3dCrs(
    citation=ro.Citation(
        title="Demo crs",
        originator=originator,
        creation=creation,
    ),
    uuid=crs_uuid,
    vertical_crs=ro.VerticalUnknownCrs(unknown="Mean sea level"),
    projected_crs=ro.ProjectedCrsEpsgCode(epsg_code=23031),
)

z = np.random.random((101, 103))
origin = np.array([10.0, 11.0])
spacing = np.array([1.0, 0.9])
u1 = np.array([np.sqrt(3.0) / 2.0, 0.5])
u2 = np.array([-0.5, np.sqrt(3.0) / 2.0])

gri = ro.obj_Grid2dRepresentation.from_regular_surface(
    citation=ro.Citation(
        title="Demo grid",
        originator=originator,
        creation=creation,
    ),
    uuid=gri_uuid,
    crs=crs,
    epc_external_part_reference=epc,
    shape=z.shape,
    origin=origin,
    spacing=spacing,
    unit_vec_1=u1,
    unit_vec_2=u2,
)

X, Y = gri.get_xy_grid(crs=crs)