pyetp 0.0.45__py3-none-any.whl → 0.0.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. energistics/__init__.py +0 -0
  2. energistics/etp/__init__.py +0 -0
  3. energistics/etp/v12/__init__.py +0 -0
  4. energistics/etp/v12/datatypes/__init__.py +25 -0
  5. energistics/etp/v12/datatypes/data_array_types/__init__.py +27 -0
  6. energistics/etp/v12/datatypes/object/__init__.py +22 -0
  7. energistics/etp/v12/protocol/__init__.py +0 -0
  8. energistics/etp/v12/protocol/core/__init__.py +19 -0
  9. energistics/etp/v12/protocol/data_array/__init__.py +51 -0
  10. energistics/etp/v12/protocol/dataspace/__init__.py +23 -0
  11. energistics/etp/v12/protocol/discovery/__init__.py +21 -0
  12. energistics/etp/v12/protocol/store/__init__.py +27 -0
  13. energistics/etp/v12/protocol/transaction/__init__.py +27 -0
  14. pyetp/__init__.py +1 -2
  15. pyetp/_version.py +2 -2
  16. pyetp/client.py +426 -306
  17. pyetp/errors.py +39 -0
  18. pyetp/uri.py +3 -1
  19. pyetp/utils_arrays.py +1 -7
  20. pyetp/utils_xml.py +1 -6
  21. {pyetp-0.0.45.dist-info → pyetp-0.0.47.dist-info}/METADATA +8 -3
  22. pyetp-0.0.47.dist-info/RECORD +39 -0
  23. {pyetp-0.0.45.dist-info → pyetp-0.0.47.dist-info}/WHEEL +1 -1
  24. {pyetp-0.0.45.dist-info → pyetp-0.0.47.dist-info}/top_level.txt +2 -0
  25. rddms_io/__init__.py +0 -0
  26. rddms_io/client.py +1234 -0
  27. rddms_io/data_types.py +11 -0
  28. resqml_objects/epc_readers.py +3 -7
  29. resqml_objects/parsers.py +18 -5
  30. resqml_objects/serializers.py +25 -2
  31. resqml_objects/surface_helpers.py +295 -0
  32. resqml_objects/v201/generated.py +582 -19
  33. resqml_objects/v201/utils.py +38 -0
  34. pyetp-0.0.45.dist-info/RECORD +0 -21
  35. {pyetp-0.0.45.dist-info → pyetp-0.0.47.dist-info}/licenses/LICENSE.md +0 -0
@@ -6,12 +6,22 @@ See: https://xsdata.readthedocs.io/
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import datetime
10
+ import re
11
+ import uuid
12
+ import warnings
9
13
  from dataclasses import dataclass, field
10
14
  from enum import Enum
11
- from typing import Any
15
+ from typing import Annotated, Any, Self, Type
12
16
 
17
+ import numpy as np
18
+ import numpy.typing as npt
13
19
  from xsdata.models.datatype import XmlDate, XmlDateTime, XmlPeriod
14
20
 
21
+ resqml_schema_version = "2.0.1"
22
+ common_schema_version = "2.0"
23
+ OBJ_TYPE_PATTERN = re.compile(r"type=(?P<obj_type>\w+)$")
24
+
15
25
 
16
26
  class APIGammaRayUom(Enum):
17
27
  """
@@ -747,15 +757,19 @@ class Citation:
747
757
  "white_space": "collapse",
748
758
  }
749
759
  )
750
- creation: XmlDateTime = field(
760
+ creation: XmlDateTime | datetime.datetime = field(
761
+ default_factory=lambda: XmlDateTime.from_datetime(
762
+ datetime.datetime.now(datetime.timezone.utc)
763
+ ),
751
764
  metadata={
752
765
  "name": "Creation",
753
766
  "type": "Element",
754
767
  "namespace": "http://www.energistics.org/energyml/data/commonv2",
755
768
  "required": True,
756
- }
769
+ },
757
770
  )
758
771
  format: str = field(
772
+ default="",
759
773
  metadata={
760
774
  "name": "Format",
761
775
  "type": "Element",
@@ -764,7 +778,7 @@ class Citation:
764
778
  "min_length": 1,
765
779
  "max_length": 256,
766
780
  "white_space": "collapse",
767
- }
781
+ },
768
782
  )
769
783
  editor: None | str = field(
770
784
  default=None,
@@ -777,7 +791,7 @@ class Citation:
777
791
  "white_space": "collapse",
778
792
  },
779
793
  )
780
- last_update: None | XmlDateTime = field(
794
+ last_update: None | XmlDateTime | datetime.datetime = field(
781
795
  default=None,
782
796
  metadata={
783
797
  "name": "LastUpdate",
@@ -816,6 +830,21 @@ class Citation:
816
830
  },
817
831
  )
818
832
 
833
+ def __post_init__(self) -> None:
834
+ # Delayed to avoid circular import. Fix once the ETP-client is made
835
+ # indepent of the RESQML-objects.
836
+ from pyetp._version import version
837
+
838
+ if not self.format:
839
+ self.format = f"equinor:pyetp:{version}"
840
+
841
+ # Let the user pass in the creation time as a Python datetime-object
842
+ if isinstance(self.creation, datetime.datetime):
843
+ self.creation = XmlDateTime.from_datetime(self.creation)
844
+
845
+ if isinstance(self.last_update, datetime.datetime):
846
+ self.last_update = XmlDateTime.from_datetime(self.last_update)
847
+
819
848
 
820
849
  @dataclass(slots=True, kw_only=True)
821
850
  class CustomData:
@@ -912,8 +941,153 @@ class DataObjectReference:
912
941
  },
913
942
  )
914
943
 
915
- def __post_init__(self):
916
- self.content_type = self.content_type.replace("type=obj_", "type=")
944
+ @staticmethod
945
+ def get_content_type_string(
946
+ obj: AbstractResqmlDataObject | Type[AbstractResqmlDataObject],
947
+ ) -> str:
948
+ """
949
+ Static method constructing a RESQML v2.0.1 or EML v2.0 content type
950
+ string based on the XML namespace of the provided object. The format of
951
+ the content type string for RESQML v2.0.1 is:
952
+
953
+ application/x-resqml+xml;version=2.0.1;type={object-type}
954
+
955
+ and for EML v2.0:
956
+
957
+ application/x-eml+xml;version=2.0;type={object-type}
958
+
959
+ where `object-type` should correspond to the XSD type of the object.
960
+ For example for a `obj_Grid2dRepresentation`-object this is exactly
961
+ `obj_Grid2dRepresentation`.
962
+
963
+ See Energistics Identifier Specification 4.0 (it is downloaded
964
+ alongside the RESQML v2.0.1 standard) section 4.1 for the
965
+ documentation of this format.
966
+
967
+ Parameters
968
+ ----------
969
+ obj: AbstractResqmlDataObject | Type[AbstractResqmlDataObject]
970
+ An instance or type that is a subclass of
971
+ `AbstractResqmlDataObject`.
972
+
973
+ Returns
974
+ -------
975
+ str
976
+ The content type string.
977
+ """
978
+
979
+ # Get class object instead of the instance.
980
+ if type(obj) is not type:
981
+ obj = type(obj)
982
+
983
+ namespace = getattr(obj.Meta, "namespace", None) or getattr(
984
+ obj.Meta, "target_namespace"
985
+ )
986
+
987
+ if namespace == "http://www.energistics.org/energyml/data/resqmlv2":
988
+ return (
989
+ f"application/x-resqml+xml;version={resqml_schema_version};"
990
+ f"type={obj.__name__}"
991
+ )
992
+ elif namespace == "http://www.energistics.org/energyml/data/commonv2":
993
+ return (
994
+ f"application/x-eml+xml;version={common_schema_version};"
995
+ f"type={obj.__name__}"
996
+ )
997
+
998
+ raise NotImplementedError(
999
+ f"Namespace {namespace} from object {obj} is not supported"
1000
+ )
1001
+
1002
+ @classmethod
1003
+ def from_object(
1004
+ cls,
1005
+ obj: AbstractResqmlDataObject,
1006
+ uuid_authority: None | str = None,
1007
+ version_string: None | str = None,
1008
+ ) -> Self:
1009
+ """
1010
+ Class method setting up a `DataObjectReference` from a RESQML-object
1011
+ instance (subclass of `AbstractResqmlDataObject`). This populates the
1012
+ mandatory fields from the `citation` field of the object.
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ obj: AbstractResqmlDataObject
1017
+ A subclass of the `AbstractResqmlDataObject` which contains a
1018
+ `citation`-field.
1019
+ uuid_authority: None | str
1020
+ See documentation of `DataObjectReference`. Default is `None`.
1021
+ version_string: None | str
1022
+ See documentation of `DataObjectReference`. Default is `None`.
1023
+
1024
+ Returns
1025
+ -------
1026
+ Self
1027
+ An instance of `DataObjectReference` with reference information on
1028
+ `obj`.
1029
+ """
1030
+ content_type = DataObjectReference.get_content_type_string(obj)
1031
+
1032
+ return cls(
1033
+ content_type=content_type,
1034
+ title=obj.citation.title,
1035
+ uuid=obj.uuid,
1036
+ uuid_authority=uuid_authority,
1037
+ version_string=version_string,
1038
+ )
1039
+
1040
+ def get_etp_data_object_uri(self, dataspace_path_or_uri: str) -> str:
1041
+ """
1042
+ Method that sets up a valid ETP data object uri from a
1043
+ `DataObjectReference`-instance. This is a helper function for easier
1044
+ querying towards an ETP server when downloading parts of a model at a
1045
+ time.
1046
+
1047
+ Parameters
1048
+ ----------
1049
+ dataspace_path_or_uri: str
1050
+ Either a full dataspace uri on the form `"eml:///"` or
1051
+ `"eml:///dataspace('foo/bar')"`, or just the dataspace path (or
1052
+ name) – which looking at the previous two examples – is `""`
1053
+ (empty) or "foo/bar".
1054
+
1055
+ Returns
1056
+ -------
1057
+ An ETP data object uri that be used to look up an object on an ETP
1058
+ server.
1059
+ """
1060
+
1061
+ domain_version = ""
1062
+
1063
+ if self.content_type.startswith("application/x-resqml+xml"):
1064
+ domain_version = "resqml20"
1065
+ elif self.content_type.startswith("application/x-eml+xml"):
1066
+ domain_version = "eml20"
1067
+ else:
1068
+ raise NotImplementedError(
1069
+ f"Qualified type for '{self.content_type}' is not implemented"
1070
+ )
1071
+
1072
+ m = re.search(OBJ_TYPE_PATTERN, self.content_type)
1073
+
1074
+ if m is None:
1075
+ raise ValueError("Content type string does not contain a valid object name")
1076
+
1077
+ obj_type = m.group("obj_type")
1078
+
1079
+ if dataspace_path_or_uri.startswith("eml:///"):
1080
+ dataspace_uri = dataspace_path_or_uri
1081
+ elif not dataspace_path_or_uri:
1082
+ # Only two forward slashes (!!!) as the combination with the
1083
+ # `data_object_part` adds an extra forward slash.
1084
+ dataspace_uri = "eml://"
1085
+ else:
1086
+ dataspace_uri = f"eml:///dataspace('{dataspace_path_or_uri}')"
1087
+
1088
+ data_object_part = f"{domain_version}.{obj_type}({self.uuid})"
1089
+
1090
+ return f"{dataspace_uri}/{data_object_part}"
917
1091
 
918
1092
 
919
1093
  class DataTransferSpeedUom(Enum):
@@ -9545,7 +9719,7 @@ class Timestamp:
9545
9719
  class Meta:
9546
9720
  target_namespace = "http://www.energistics.org/energyml/data/resqmlv2"
9547
9721
 
9548
- date_time: XmlDateTime = field(
9722
+ date_time: XmlDateTime | datetime.datetime = field(
9549
9723
  metadata={
9550
9724
  "name": "DateTime",
9551
9725
  "type": "Element",
@@ -9562,6 +9736,10 @@ class Timestamp:
9562
9736
  },
9563
9737
  )
9564
9738
 
9739
+ def __post_init__(self) -> None:
9740
+ if isinstance(self.date_time, datetime.datetime):
9741
+ self.date_time = XmlDateTime.from_datetime(self.date_time)
9742
+
9565
9743
 
9566
9744
  @dataclass(slots=True, kw_only=True)
9567
9745
  class AbstractObject_Type:
@@ -9659,12 +9837,16 @@ class DateTime:
9659
9837
  class Meta:
9660
9838
  namespace = "http://www.isotc211.org/2005/gco"
9661
9839
 
9662
- value: XmlDateTime = field(
9840
+ value: XmlDateTime | datetime.datetime = field(
9663
9841
  metadata={
9664
9842
  "required": True,
9665
9843
  }
9666
9844
  )
9667
9845
 
9846
+ def __post_init__(self) -> None:
9847
+ if isinstance(self.value, datetime.datetime):
9848
+ self.value = XmlDateTime.from_datetime(self.value)
9849
+
9668
9850
 
9669
9851
  @dataclass(slots=True, kw_only=True)
9670
9852
  class Real:
@@ -10043,18 +10225,23 @@ class AbstractObject_1:
10043
10225
  },
10044
10226
  )
10045
10227
  schema_version: str = field(
10228
+ # We set the schema_version in the __post_init__ if it is empty by
10229
+ # default.
10230
+ default="",
10046
10231
  metadata={
10047
10232
  "name": "schemaVersion",
10048
10233
  "type": "Attribute",
10049
10234
  "required": True,
10050
- }
10235
+ },
10051
10236
  )
10052
10237
  uuid: str = field(
10238
+ # We add a uuid by default, if it is not provided.
10239
+ default_factory=lambda: str(uuid.uuid4()),
10053
10240
  metadata={
10054
10241
  "type": "Attribute",
10055
10242
  "required": True,
10056
10243
  "pattern": r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}",
10057
- }
10244
+ },
10058
10245
  )
10059
10246
  object_version: None | str = field(
10060
10247
  default=None,
@@ -10067,6 +10254,58 @@ class AbstractObject_1:
10067
10254
  },
10068
10255
  )
10069
10256
 
10257
+ def __post_init__(self) -> None:
10258
+ if not self.schema_version:
10259
+ namespace = getattr(self.Meta, "namespace", None) or getattr(
10260
+ self.Meta, "target_namespace"
10261
+ )
10262
+
10263
+ if namespace == "http://www.energistics.org/energyml/data/resqmlv2":
10264
+ self.schema_version = resqml_schema_version
10265
+ elif namespace == "http://www.energistics.org/energyml/data/commonv2":
10266
+ self.schema_version = common_schema_version
10267
+ else:
10268
+ raise NotImplementedError(
10269
+ f"Namespace {namespace} from object {self} has no default schema "
10270
+ "version. Provide this manually as keyword argument when "
10271
+ "constructing the object."
10272
+ )
10273
+
10274
+ @classmethod
10275
+ def get_domain_version(cls) -> str:
10276
+ namespace = getattr(cls.Meta, "namespace", None) or getattr(
10277
+ cls.Meta, "target_namespace"
10278
+ )
10279
+
10280
+ if namespace == "http://www.energistics.org/energyml/data/resqmlv2":
10281
+ return "resqml20"
10282
+ elif namespace == "http://www.energistics.org/energyml/data/commonv2":
10283
+ return "eml20"
10284
+
10285
+ raise NotImplementedError(
10286
+ f"Namespace {namespace} from object {cls} is not supported"
10287
+ )
10288
+
10289
+ @classmethod
10290
+ def get_qualified_type(cls) -> str:
10291
+ return cls.get_domain_version() + f".{cls.__name__}"
10292
+
10293
+ def get_etp_data_object_uri(self, dataspace_path_or_uri: str) -> str:
10294
+ qualified_type = self.get_qualified_type()
10295
+ identifier = (
10296
+ self.uuid
10297
+ if self.object_version is None
10298
+ else f"uuid={self.uuid},version='{self.object_version}'"
10299
+ )
10300
+ data_object_part = f"{qualified_type}({identifier})"
10301
+
10302
+ if not dataspace_path_or_uri or dataspace_path_or_uri == "eml:///":
10303
+ return f"eml:///{data_object_part}"
10304
+ elif dataspace_path_or_uri.startswith("eml:///dataspace"):
10305
+ return f"{dataspace_path_or_uri}/{data_object_part}"
10306
+
10307
+ return f"eml:///dataspace('{dataspace_path_or_uri}')/{data_object_part}"
10308
+
10070
10309
 
10071
10310
  @dataclass(slots=True, kw_only=True)
10072
10311
  class ActivityOfRadioactivityMeasure:
@@ -16947,6 +17186,62 @@ class Point3dZValueArray(AbstractPoint3dArray):
16947
17186
  }
16948
17187
  )
16949
17188
 
17189
+ @classmethod
17190
+ def from_regular_surface(
17191
+ cls,
17192
+ epc_external_part_reference: obj_EpcExternalPartReference,
17193
+ path_in_hdf_file: str,
17194
+ shape: tuple[int, int],
17195
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17196
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17197
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17198
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17199
+ ) -> Self:
17200
+ supporting_geometry = Point3dLatticeArray(
17201
+ origin=Point3d(
17202
+ coordinate1=float(origin[0]),
17203
+ coordinate2=float(origin[1]),
17204
+ coordinate3=0.0,
17205
+ ),
17206
+ offset=[
17207
+ Point3dOffset(
17208
+ offset=Point3d(
17209
+ coordinate1=float(unit_vec_1[0]),
17210
+ coordinate2=float(unit_vec_1[1]),
17211
+ coordinate3=0.0,
17212
+ ),
17213
+ spacing=DoubleConstantArray(
17214
+ value=float(spacing[0]),
17215
+ # TODO: Figure out how we should treat the spacing! The
17216
+ # documentation states that it should be N - 1 for an
17217
+ # array of N elements (that is, it counts the number of
17218
+ # spaces between elements). However, we have seen cases
17219
+ # where this is instead set to N.
17220
+ count=int(shape[0]) - 1,
17221
+ ),
17222
+ ),
17223
+ Point3dOffset(
17224
+ offset=Point3d(
17225
+ coordinate1=float(unit_vec_2[0]),
17226
+ coordinate2=float(unit_vec_2[1]),
17227
+ coordinate3=0.0,
17228
+ ),
17229
+ spacing=DoubleConstantArray(
17230
+ value=float(spacing[1]),
17231
+ count=int(shape[1]) - 1,
17232
+ ),
17233
+ ),
17234
+ ],
17235
+ )
17236
+ zvalues = DoubleHdf5Array(
17237
+ values=Hdf5Dataset(
17238
+ path_in_hdf_file=path_in_hdf_file,
17239
+ hdf_proxy=DataObjectReference.from_object(epc_external_part_reference),
17240
+ ),
17241
+ )
17242
+
17243
+ return cls(supporting_geometry=supporting_geometry, zvalues=zvalues)
17244
+
16950
17245
 
16951
17246
  @dataclass(slots=True, kw_only=True)
16952
17247
  class ResqmlJaggedArray:
@@ -17958,12 +18253,13 @@ class obj_EpcExternalPartReference(AbstractCitedDataObject):
17958
18253
  target_namespace = "http://www.energistics.org/energyml/data/commonv2"
17959
18254
 
17960
18255
  mime_type: str = field(
18256
+ default="application/x-hdf5",
17961
18257
  metadata={
17962
18258
  "name": "MimeType",
17963
18259
  "type": "Element",
17964
18260
  "namespace": "http://www.energistics.org/energyml/data/commonv2",
17965
18261
  "required": True,
17966
- }
18262
+ },
17967
18263
  )
17968
18264
 
17969
18265
 
@@ -18584,6 +18880,32 @@ class PointGeometry(AbstractGeometry):
18584
18880
  },
18585
18881
  )
18586
18882
 
18883
+ @classmethod
18884
+ def from_regular_surface(
18885
+ cls,
18886
+ crs: AbstractLocal3dCrs,
18887
+ epc_external_part_reference: obj_EpcExternalPartReference,
18888
+ path_in_hdf_file: str,
18889
+ shape: tuple[int, int],
18890
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18891
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18892
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18893
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18894
+ ) -> Self:
18895
+ local_crs = DataObjectReference.from_object(crs)
18896
+
18897
+ points = Point3dZValueArray.from_regular_surface(
18898
+ epc_external_part_reference=epc_external_part_reference,
18899
+ path_in_hdf_file=path_in_hdf_file,
18900
+ shape=shape,
18901
+ origin=origin,
18902
+ spacing=spacing,
18903
+ unit_vec_1=unit_vec_1,
18904
+ unit_vec_2=unit_vec_2,
18905
+ )
18906
+
18907
+ return cls(local_crs=local_crs, points=points)
18908
+
18587
18909
 
18588
18910
  @dataclass(slots=True, kw_only=True)
18589
18911
  class Regrid:
@@ -19427,44 +19749,49 @@ class AbstractLocal3dCrs(AbstractResqmlDataObject):
19427
19749
  target_namespace = "http://www.energistics.org/energyml/data/resqmlv2"
19428
19750
 
19429
19751
  yoffset: float = field(
19752
+ default=0.0,
19430
19753
  metadata={
19431
19754
  "name": "YOffset",
19432
19755
  "type": "Element",
19433
19756
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19434
19757
  "required": True,
19435
- }
19758
+ },
19436
19759
  )
19437
19760
  zoffset: float = field(
19761
+ default=0.0,
19438
19762
  metadata={
19439
19763
  "name": "ZOffset",
19440
19764
  "type": "Element",
19441
19765
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19442
19766
  "required": True,
19443
- }
19767
+ },
19444
19768
  )
19445
19769
  areal_rotation: PlaneAngleMeasure = field(
19770
+ default_factory=lambda: PlaneAngleMeasure(value=0.0, uom=PlaneAngleUom.RAD),
19446
19771
  metadata={
19447
19772
  "name": "ArealRotation",
19448
19773
  "type": "Element",
19449
19774
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19450
19775
  "required": True,
19451
- }
19776
+ },
19452
19777
  )
19453
19778
  projected_axis_order: AxisOrder2d = field(
19779
+ default=AxisOrder2d.EASTING_NORTHING,
19454
19780
  metadata={
19455
19781
  "name": "ProjectedAxisOrder",
19456
19782
  "type": "Element",
19457
19783
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19458
19784
  "required": True,
19459
- }
19785
+ },
19460
19786
  )
19461
19787
  projected_uom: LengthUom = field(
19788
+ default=LengthUom.M,
19462
19789
  metadata={
19463
19790
  "name": "ProjectedUom",
19464
19791
  "type": "Element",
19465
19792
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19466
19793
  "required": True,
19467
- }
19794
+ },
19468
19795
  )
19469
19796
  vertical_uom: LengthUom = field(
19470
19797
  metadata={
@@ -19475,20 +19802,22 @@ class AbstractLocal3dCrs(AbstractResqmlDataObject):
19475
19802
  }
19476
19803
  )
19477
19804
  xoffset: float = field(
19805
+ default=0.0,
19478
19806
  metadata={
19479
19807
  "name": "XOffset",
19480
19808
  "type": "Element",
19481
19809
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19482
19810
  "required": True,
19483
- }
19811
+ },
19484
19812
  )
19485
19813
  zincreasing_downward: bool = field(
19814
+ default=True,
19486
19815
  metadata={
19487
19816
  "name": "ZIncreasingDownward",
19488
19817
  "type": "Element",
19489
19818
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19490
19819
  "required": True,
19491
- }
19820
+ },
19492
19821
  )
19493
19822
  vertical_crs: AbstractVerticalCrs = field(
19494
19823
  metadata={
@@ -19736,6 +20065,39 @@ class Grid2dPatch(Patch):
19736
20065
  }
19737
20066
  )
19738
20067
 
20068
+ @classmethod
20069
+ def from_regular_surface(
20070
+ cls,
20071
+ crs: AbstractLocal3dCrs,
20072
+ epc_external_part_reference: obj_EpcExternalPartReference,
20073
+ path_in_hdf_file: str,
20074
+ shape: tuple[int, int],
20075
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20076
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20077
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20078
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20079
+ patch_index: int = 0,
20080
+ ) -> Self:
20081
+ geometry = PointGeometry.from_regular_surface(
20082
+ crs=crs,
20083
+ epc_external_part_reference=epc_external_part_reference,
20084
+ path_in_hdf_file=path_in_hdf_file,
20085
+ shape=shape,
20086
+ origin=origin,
20087
+ spacing=spacing,
20088
+ unit_vec_1=unit_vec_1,
20089
+ unit_vec_2=unit_vec_2,
20090
+ )
20091
+
20092
+ return cls(
20093
+ patch_index=patch_index,
20094
+ # Rows for NumPy-arrays in C-major ordering.
20095
+ slowest_axis_count=shape[0],
20096
+ # Columns for NumPy-arrays in C-major ordering.
20097
+ fastest_axis_count=shape[1],
20098
+ geometry=geometry,
20099
+ )
20100
+
19739
20101
 
19740
20102
  @dataclass(slots=True, kw_only=True)
19741
20103
  class HorizontalPlaneGeometry(AbstractPlaneGeometry):
@@ -21782,6 +22144,17 @@ class obj_LocalDepth3dCrs(AbstractLocal3dCrs):
21782
22144
  class Meta:
21783
22145
  target_namespace = "http://www.energistics.org/energyml/data/resqmlv2"
21784
22146
 
22147
+ # Overwriting the super-field to set a default.
22148
+ vertical_uom: LengthUom = field(
22149
+ default=LengthUom.M,
22150
+ metadata={
22151
+ "name": "VerticalUom",
22152
+ "type": "Element",
22153
+ "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
22154
+ "required": True,
22155
+ },
22156
+ )
22157
+
21785
22158
 
21786
22159
  @dataclass(slots=True, kw_only=True)
21787
22160
  class obj_LocalTime3dCrs(AbstractLocal3dCrs):
@@ -23639,6 +24012,196 @@ class obj_Grid2dRepresentation(AbstractSurfaceRepresentation):
23639
24012
  }
23640
24013
  )
23641
24014
 
24015
+ def get_xy_grid(
24016
+ self, crs: AbstractLocal3dCrs | None = None
24017
+ ) -> tuple[
24018
+ npt.NDArray[np.float64],
24019
+ npt.NDArray[np.float64],
24020
+ ]:
24021
+ """
24022
+ Method constructing the `X`- and `Y`-grids for a regular surface. This
24023
+ currently only works for `obj_Grid2dRepresentation`-objects that
24024
+ represent regular surfaces. That is where the grids are specified using
24025
+ an origin, spacings, number of elements and unit vectors. Otherwise the
24026
+ `X`- and `Y`-grids are stored as arrays on an ETP server or in an
24027
+ hdf5-file. The function also takes in a local crs that can be
24028
+ transformed (translated and rotated) from a global crs. The method
24029
+ treats any rotation and translation in the grid as an _active
24030
+ transformation_, and any transformation in the local crs as a _passive
24031
+ transformation_.
24032
+
24033
+ Parameters
24034
+ ----------
24035
+ crs: AbstractLocal3dCrs
24036
+ A subclass of `AbstractLocal3dCrs`. It is used to correct for a
24037
+ potential passive transformation done by the crs. The crs does not
24038
+ have to be the same as referenced by the grid-object, but if it
24039
+ does not match a warning is raised. Setting `crs=None` avoids any
24040
+ transformation from the crs. Default is `None`.
24041
+
24042
+ Returns
24043
+ -------
24044
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]
24045
+ A pair of `X`- and `Y`-grids that gives the `x` and `y` coordinates
24046
+ to the surface described by the grid-object. For an unrotated
24047
+ surface this corresponds to a meshgrid.
24048
+ """
24049
+ points = self.grid2d_patch.geometry.points
24050
+
24051
+ if not isinstance(points, Point3dZValueArray):
24052
+ raise NotImplementedError(
24053
+ "We do not support constructing the X, Y grid for points of type "
24054
+ f"{points.__class__.__name__}"
24055
+ )
24056
+
24057
+ sg = points.supporting_geometry
24058
+ if not isinstance(sg, Point3dLatticeArray):
24059
+ raise NotImplementedError(
24060
+ "We do not support constructing the X, Y grid for a supporting "
24061
+ f"geometry of type {sg.__class__.__name__}"
24062
+ )
24063
+
24064
+ from resqml_objects.surface_helpers import RegularGridParameters
24065
+
24066
+ shape = (
24067
+ self.grid2d_patch.slowest_axis_count,
24068
+ self.grid2d_patch.fastest_axis_count,
24069
+ )
24070
+ origin = sg.origin
24071
+ offsets = sg.offset
24072
+
24073
+ ori = np.array(
24074
+ [
24075
+ origin.coordinate1,
24076
+ origin.coordinate2,
24077
+ ]
24078
+ )
24079
+ dr = np.array(
24080
+ [
24081
+ offsets[0].spacing.value,
24082
+ offsets[1].spacing.value,
24083
+ ]
24084
+ )
24085
+
24086
+ unit_vectors = np.array(
24087
+ [
24088
+ [offsets[0].offset.coordinate1, offsets[1].offset.coordinate1],
24089
+ [offsets[0].offset.coordinate2, offsets[1].offset.coordinate2],
24090
+ ]
24091
+ )
24092
+
24093
+ crs_angle = 0.0
24094
+ crs_offset = np.array([0.0, 0.0])
24095
+
24096
+ if crs is not None:
24097
+ if crs.uuid != self.grid2d_patch.geometry.local_crs.uuid:
24098
+ warnings.warn(
24099
+ f"The provided crs has a different uuid '{crs.citation.uuid}' "
24100
+ " than the referenced crs "
24101
+ f"'{self.grid2d_patch.geometry.local_crs.uuid}'."
24102
+ )
24103
+
24104
+ # NOTE: We assume that coordinate1 (coordinate2) corresponds to
24105
+ # xoffset (yoffset) in the CRS, and that they share the same units.
24106
+ crs_offset[0] = crs.xoffset
24107
+ crs_offset[1] = crs.yoffset
24108
+
24109
+ crs_angle_unit = PlaneAngleUom(crs.areal_rotation.uom)
24110
+
24111
+ match crs_angle_unit:
24112
+ case PlaneAngleUom.RAD:
24113
+ crs_angle = crs.areal_rotation.value
24114
+ case PlaneAngleUom.DEGA:
24115
+ crs_angle = np.deg2rad(crs.areal_rotation.value)
24116
+ case _:
24117
+ raise NotImplementedError(
24118
+ f"No conversion from {crs_angle_unit} to radians implemented"
24119
+ )
24120
+
24121
+ rgp = RegularGridParameters(
24122
+ shape=shape,
24123
+ origin=ori,
24124
+ spacing=dr,
24125
+ unit_vectors=unit_vectors,
24126
+ crs_angle=crs_angle,
24127
+ crs_offset=crs_offset,
24128
+ )
24129
+
24130
+ return rgp.to_xy_grid(to_global_crs=True)
24131
+
24132
+ @classmethod
24133
+ def from_regular_surface(
24134
+ cls,
24135
+ citation: Citation,
24136
+ crs: AbstractLocal3dCrs,
24137
+ epc_external_part_reference: obj_EpcExternalPartReference,
24138
+ shape: tuple[int, int],
24139
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24140
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24141
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24142
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24143
+ patch_index: int = 0,
24144
+ path_in_hdf_file: str = "",
24145
+ uuid: str | uuid.UUID | None = None,
24146
+ surface_role: SurfaceRole | str = SurfaceRole.MAP,
24147
+ boundaries: list[PatchBoundaries] | None = None,
24148
+ represented_interpretation: AbstractFeatureInterpretation | None = None,
24149
+ extra_metadata: list[NameValuePair] | None = None,
24150
+ custom_data: CustomData | None = None,
24151
+ object_version: str | None = None,
24152
+ aliases: list[ObjectAlias] | None = None,
24153
+ ) -> Self:
24154
+ """
24155
+ Class method that sets up an `obj_Grid2dRepresentation`-object for a
24156
+ regular surface described by the eight parameters (seven free
24157
+ parameters) `shape`, `origin`, `dr` and `unit_vectors`, and the
24158
+ necessary (a local crs, and an epc-reference file) and/or optional
24159
+ metadata from RESQML.
24160
+ """
24161
+
24162
+ if uuid is None:
24163
+ # Cursed solution as the `uuid`-argument overwrites the top-level
24164
+ # import of the standard-library `uuid`.
24165
+ import uuid
24166
+
24167
+ uuid = str(uuid.uuid4())
24168
+
24169
+ surface_role = SurfaceRole(surface_role)
24170
+ boundaries = boundaries or []
24171
+ extra_metadata = extra_metadata or []
24172
+ aliases = aliases or []
24173
+ path_in_hdf_file = path_in_hdf_file or f"/RESQML/{uuid}/zvalues"
24174
+
24175
+ if represented_interpretation is not None:
24176
+ represented_interpretation = DataObjectReference.from_object(
24177
+ represented_interpretation
24178
+ )
24179
+
24180
+ grid2d_patch = Grid2dPatch.from_regular_surface(
24181
+ crs=crs,
24182
+ epc_external_part_reference=epc_external_part_reference,
24183
+ path_in_hdf_file=path_in_hdf_file,
24184
+ shape=shape,
24185
+ origin=origin,
24186
+ spacing=spacing,
24187
+ unit_vec_1=unit_vec_1,
24188
+ unit_vec_2=unit_vec_2,
24189
+ patch_index=patch_index,
24190
+ )
24191
+
24192
+ return cls(
24193
+ citation=citation,
24194
+ aliases=aliases,
24195
+ custom_data=custom_data,
24196
+ uuid=uuid,
24197
+ object_version=object_version,
24198
+ surface_role=surface_role,
24199
+ grid2d_patch=grid2d_patch,
24200
+ boundaries=boundaries,
24201
+ represented_interpretation=represented_interpretation,
24202
+ extra_metadata=extra_metadata,
24203
+ )
24204
+
23642
24205
 
23643
24206
  @dataclass(slots=True, kw_only=True)
23644
24207
  class obj_Grid2dSetRepresentation(AbstractSurfaceRepresentation):