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:
- An
obj_EpcExternalPartReferencewhich 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. - A local coordinate system, either
obj_LocalDepth3dCrsif the surface describes depth in units of distance orobj_LocalTime3dCrsif it is in units of time. - An
obj_Grid2dRepresentationfor the actual grid metadata and references to 1. and 2. above. We will often denote theobj_Grid2dRepresentationas "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"
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'
)
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><name/username/email></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>
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),
)
"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:
VerticalCrsEpsgCodeandProjectedCrsEpsgCodewhere the global coordinate system is described via an EPSG code.GmlVerticalCrsDefinitionandGmlProjectedCrsDefinitionwith the global coordinate system described by the Geography Markup Language (GML).VerticalUnknownCrsandProjectedUnknownCrs, 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)
)
<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><name/username/email></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>
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])
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
)
)
)
<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><name/username/email></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>
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.,
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)