pyetp 0.0.45__py3-none-any.whl → 0.0.46__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.
@@ -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,23 @@ 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
+
10070
10274
 
10071
10275
  @dataclass(slots=True, kw_only=True)
10072
10276
  class ActivityOfRadioactivityMeasure:
@@ -16947,6 +17151,62 @@ class Point3dZValueArray(AbstractPoint3dArray):
16947
17151
  }
16948
17152
  )
16949
17153
 
17154
+ @classmethod
17155
+ def from_regular_surface(
17156
+ cls,
17157
+ epc_external_part_reference: obj_EpcExternalPartReference,
17158
+ path_in_hdf_file: str,
17159
+ shape: tuple[int, int],
17160
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17161
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17162
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17163
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
17164
+ ) -> Self:
17165
+ supporting_geometry = Point3dLatticeArray(
17166
+ origin=Point3d(
17167
+ coordinate1=float(origin[0]),
17168
+ coordinate2=float(origin[1]),
17169
+ coordinate3=0.0,
17170
+ ),
17171
+ offset=[
17172
+ Point3dOffset(
17173
+ offset=Point3d(
17174
+ coordinate1=float(unit_vec_1[0]),
17175
+ coordinate2=float(unit_vec_1[1]),
17176
+ coordinate3=0.0,
17177
+ ),
17178
+ spacing=DoubleConstantArray(
17179
+ value=float(spacing[0]),
17180
+ # TODO: Figure out how we should treat the spacing! The
17181
+ # documentation states that it should be N - 1 for an
17182
+ # array of N elements (that is, it counts the number of
17183
+ # spaces between elements). However, we have seen cases
17184
+ # where this is instead set to N.
17185
+ count=int(shape[0]) - 1,
17186
+ ),
17187
+ ),
17188
+ Point3dOffset(
17189
+ offset=Point3d(
17190
+ coordinate1=float(unit_vec_2[0]),
17191
+ coordinate2=float(unit_vec_2[1]),
17192
+ coordinate3=0.0,
17193
+ ),
17194
+ spacing=DoubleConstantArray(
17195
+ value=float(spacing[1]),
17196
+ count=int(shape[1]) - 1,
17197
+ ),
17198
+ ),
17199
+ ],
17200
+ )
17201
+ zvalues = DoubleHdf5Array(
17202
+ values=Hdf5Dataset(
17203
+ path_in_hdf_file=path_in_hdf_file,
17204
+ hdf_proxy=DataObjectReference.from_object(epc_external_part_reference),
17205
+ ),
17206
+ )
17207
+
17208
+ return cls(supporting_geometry=supporting_geometry, zvalues=zvalues)
17209
+
16950
17210
 
16951
17211
  @dataclass(slots=True, kw_only=True)
16952
17212
  class ResqmlJaggedArray:
@@ -17958,12 +18218,13 @@ class obj_EpcExternalPartReference(AbstractCitedDataObject):
17958
18218
  target_namespace = "http://www.energistics.org/energyml/data/commonv2"
17959
18219
 
17960
18220
  mime_type: str = field(
18221
+ default="application/x-hdf5",
17961
18222
  metadata={
17962
18223
  "name": "MimeType",
17963
18224
  "type": "Element",
17964
18225
  "namespace": "http://www.energistics.org/energyml/data/commonv2",
17965
18226
  "required": True,
17966
- }
18227
+ },
17967
18228
  )
17968
18229
 
17969
18230
 
@@ -18584,6 +18845,32 @@ class PointGeometry(AbstractGeometry):
18584
18845
  },
18585
18846
  )
18586
18847
 
18848
+ @classmethod
18849
+ def from_regular_surface(
18850
+ cls,
18851
+ crs: AbstractLocal3dCrs,
18852
+ epc_external_part_reference: obj_EpcExternalPartReference,
18853
+ path_in_hdf_file: str,
18854
+ shape: tuple[int, int],
18855
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18856
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18857
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18858
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
18859
+ ) -> Self:
18860
+ local_crs = DataObjectReference.from_object(crs)
18861
+
18862
+ points = Point3dZValueArray.from_regular_surface(
18863
+ epc_external_part_reference=epc_external_part_reference,
18864
+ path_in_hdf_file=path_in_hdf_file,
18865
+ shape=shape,
18866
+ origin=origin,
18867
+ spacing=spacing,
18868
+ unit_vec_1=unit_vec_1,
18869
+ unit_vec_2=unit_vec_2,
18870
+ )
18871
+
18872
+ return cls(local_crs=local_crs, points=points)
18873
+
18587
18874
 
18588
18875
  @dataclass(slots=True, kw_only=True)
18589
18876
  class Regrid:
@@ -19427,44 +19714,49 @@ class AbstractLocal3dCrs(AbstractResqmlDataObject):
19427
19714
  target_namespace = "http://www.energistics.org/energyml/data/resqmlv2"
19428
19715
 
19429
19716
  yoffset: float = field(
19717
+ default=0.0,
19430
19718
  metadata={
19431
19719
  "name": "YOffset",
19432
19720
  "type": "Element",
19433
19721
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19434
19722
  "required": True,
19435
- }
19723
+ },
19436
19724
  )
19437
19725
  zoffset: float = field(
19726
+ default=0.0,
19438
19727
  metadata={
19439
19728
  "name": "ZOffset",
19440
19729
  "type": "Element",
19441
19730
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19442
19731
  "required": True,
19443
- }
19732
+ },
19444
19733
  )
19445
19734
  areal_rotation: PlaneAngleMeasure = field(
19735
+ default_factory=lambda: PlaneAngleMeasure(value=0.0, uom=PlaneAngleUom.RAD),
19446
19736
  metadata={
19447
19737
  "name": "ArealRotation",
19448
19738
  "type": "Element",
19449
19739
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19450
19740
  "required": True,
19451
- }
19741
+ },
19452
19742
  )
19453
19743
  projected_axis_order: AxisOrder2d = field(
19744
+ default=AxisOrder2d.EASTING_NORTHING,
19454
19745
  metadata={
19455
19746
  "name": "ProjectedAxisOrder",
19456
19747
  "type": "Element",
19457
19748
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19458
19749
  "required": True,
19459
- }
19750
+ },
19460
19751
  )
19461
19752
  projected_uom: LengthUom = field(
19753
+ default=LengthUom.M,
19462
19754
  metadata={
19463
19755
  "name": "ProjectedUom",
19464
19756
  "type": "Element",
19465
19757
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19466
19758
  "required": True,
19467
- }
19759
+ },
19468
19760
  )
19469
19761
  vertical_uom: LengthUom = field(
19470
19762
  metadata={
@@ -19475,20 +19767,22 @@ class AbstractLocal3dCrs(AbstractResqmlDataObject):
19475
19767
  }
19476
19768
  )
19477
19769
  xoffset: float = field(
19770
+ default=0.0,
19478
19771
  metadata={
19479
19772
  "name": "XOffset",
19480
19773
  "type": "Element",
19481
19774
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19482
19775
  "required": True,
19483
- }
19776
+ },
19484
19777
  )
19485
19778
  zincreasing_downward: bool = field(
19779
+ default=True,
19486
19780
  metadata={
19487
19781
  "name": "ZIncreasingDownward",
19488
19782
  "type": "Element",
19489
19783
  "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
19490
19784
  "required": True,
19491
- }
19785
+ },
19492
19786
  )
19493
19787
  vertical_crs: AbstractVerticalCrs = field(
19494
19788
  metadata={
@@ -19736,6 +20030,39 @@ class Grid2dPatch(Patch):
19736
20030
  }
19737
20031
  )
19738
20032
 
20033
+ @classmethod
20034
+ def from_regular_surface(
20035
+ cls,
20036
+ crs: AbstractLocal3dCrs,
20037
+ epc_external_part_reference: obj_EpcExternalPartReference,
20038
+ path_in_hdf_file: str,
20039
+ shape: tuple[int, int],
20040
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20041
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20042
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20043
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
20044
+ patch_index: int = 0,
20045
+ ) -> Self:
20046
+ geometry = PointGeometry.from_regular_surface(
20047
+ crs=crs,
20048
+ epc_external_part_reference=epc_external_part_reference,
20049
+ path_in_hdf_file=path_in_hdf_file,
20050
+ shape=shape,
20051
+ origin=origin,
20052
+ spacing=spacing,
20053
+ unit_vec_1=unit_vec_1,
20054
+ unit_vec_2=unit_vec_2,
20055
+ )
20056
+
20057
+ return cls(
20058
+ patch_index=patch_index,
20059
+ # Rows for NumPy-arrays in C-major ordering.
20060
+ slowest_axis_count=shape[0],
20061
+ # Columns for NumPy-arrays in C-major ordering.
20062
+ fastest_axis_count=shape[1],
20063
+ geometry=geometry,
20064
+ )
20065
+
19739
20066
 
19740
20067
  @dataclass(slots=True, kw_only=True)
19741
20068
  class HorizontalPlaneGeometry(AbstractPlaneGeometry):
@@ -21782,6 +22109,17 @@ class obj_LocalDepth3dCrs(AbstractLocal3dCrs):
21782
22109
  class Meta:
21783
22110
  target_namespace = "http://www.energistics.org/energyml/data/resqmlv2"
21784
22111
 
22112
+ # Overwriting the super-field to set a default.
22113
+ vertical_uom: LengthUom = field(
22114
+ default=LengthUom.M,
22115
+ metadata={
22116
+ "name": "VerticalUom",
22117
+ "type": "Element",
22118
+ "namespace": "http://www.energistics.org/energyml/data/resqmlv2",
22119
+ "required": True,
22120
+ },
22121
+ )
22122
+
21785
22123
 
21786
22124
  @dataclass(slots=True, kw_only=True)
21787
22125
  class obj_LocalTime3dCrs(AbstractLocal3dCrs):
@@ -23639,6 +23977,189 @@ class obj_Grid2dRepresentation(AbstractSurfaceRepresentation):
23639
23977
  }
23640
23978
  )
23641
23979
 
23980
+ def get_xy_grid(
23981
+ self, crs: AbstractLocal3dCrs | None = None
23982
+ ) -> tuple[
23983
+ npt.NDArray[np.float64],
23984
+ npt.NDArray[np.float64],
23985
+ ]:
23986
+ """
23987
+ Method constructing the `X`- and `Y`-grids for a regular surface. This
23988
+ currently only works for `obj_Grid2dRepresentation`-objects that
23989
+ represent regular surfaces. That is where the grids are specified using
23990
+ an origin, spacings, number of elements and unit vectors. Otherwise the
23991
+ `X`- and `Y`-grids are stored as arrays on an ETP server or in an
23992
+ hdf5-file. The function also takes in a local crs that can be
23993
+ transformed (translated and rotated) from a global crs. The method
23994
+ treats any rotation and translation in the grid as an _active
23995
+ transformation_, and any transformation in the local crs as a _passive
23996
+ transformation_.
23997
+
23998
+ Parameters
23999
+ ----------
24000
+ crs: AbstractLocal3dCrs
24001
+ A subclass of `AbstractLocal3dCrs`. It is used to correct for a
24002
+ potential passive transformation done by the crs. The crs does not
24003
+ have to be the same as referenced by the grid-object, but if it
24004
+ does not match a warning is raised. Setting `crs=None` avoids any
24005
+ transformation from the crs. Default is `None`.
24006
+
24007
+ Returns
24008
+ -------
24009
+ tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]
24010
+ A pair of `X`- and `Y`-grids that gives the `x` and `y` coordinates
24011
+ to the surface described by the grid-object. For an unrotated
24012
+ surface this corresponds to a meshgrid.
24013
+ """
24014
+ points = self.grid2d_patch.geometry.points
24015
+
24016
+ if not isinstance(points, Point3dZValueArray):
24017
+ raise NotImplementedError(
24018
+ "We do not support constructing the X, Y grid for points of type "
24019
+ f"{points.__class__.__name__}"
24020
+ )
24021
+
24022
+ sg = points.supporting_geometry
24023
+ if not isinstance(sg, Point3dLatticeArray):
24024
+ raise NotImplementedError(
24025
+ "We do not support constructing the X, Y grid for a supporting "
24026
+ f"geometry of type {sg.__class__.__name__}"
24027
+ )
24028
+
24029
+ from resqml_objects.surface_helpers import RegularGridParameters
24030
+
24031
+ shape = (
24032
+ self.grid2d_patch.slowest_axis_count,
24033
+ self.grid2d_patch.fastest_axis_count,
24034
+ )
24035
+ origin = sg.origin
24036
+ offsets = sg.offset
24037
+
24038
+ ori = np.array(
24039
+ [
24040
+ origin.coordinate1,
24041
+ origin.coordinate2,
24042
+ ]
24043
+ )
24044
+ dr = np.array(
24045
+ [
24046
+ offsets[0].spacing.value,
24047
+ offsets[1].spacing.value,
24048
+ ]
24049
+ )
24050
+
24051
+ unit_vectors = np.array(
24052
+ [
24053
+ [offsets[0].offset.coordinate1, offsets[1].offset.coordinate1],
24054
+ [offsets[0].offset.coordinate2, offsets[1].offset.coordinate2],
24055
+ ]
24056
+ )
24057
+
24058
+ crs_angle = 0.0
24059
+ crs_offset = np.array([0.0, 0.0])
24060
+
24061
+ if crs is not None:
24062
+ if crs.uuid != self.grid2d_patch.geometry.local_crs.uuid:
24063
+ warnings.warn(
24064
+ f"The provided crs has a different uuid '{crs.citation.uuid}' "
24065
+ " than the referenced crs "
24066
+ f"'{self.grid2d_patch.geometry.local_crs.uuid}'."
24067
+ )
24068
+
24069
+ # NOTE: We assume that coordinate1 (coordinate2) corresponds to
24070
+ # xoffset (yoffset) in the CRS, and that they share the same units.
24071
+ crs_offset[0] = crs.xoffset
24072
+ crs_offset[1] = crs.yoffset
24073
+
24074
+ crs_angle_unit = PlaneAngleUom(crs.areal_rotation.uom)
24075
+
24076
+ match crs_angle_unit:
24077
+ case PlaneAngleUom.RAD:
24078
+ crs_angle = crs.areal_rotation.value
24079
+ case PlaneAngleUom.DEGA:
24080
+ crs_angle = np.deg2rad(crs.areal_rotation.value)
24081
+ case _:
24082
+ raise NotImplementedError(
24083
+ f"No conversion from {crs_angle_unit} to radians implemented"
24084
+ )
24085
+
24086
+ rgp = RegularGridParameters(
24087
+ shape=shape,
24088
+ origin=ori,
24089
+ spacing=dr,
24090
+ unit_vectors=unit_vectors,
24091
+ crs_angle=crs_angle,
24092
+ crs_offset=crs_offset,
24093
+ )
24094
+
24095
+ return rgp.to_xy_grid(to_global_crs=True)
24096
+
24097
+ @classmethod
24098
+ def from_regular_surface(
24099
+ cls,
24100
+ citation: Citation,
24101
+ crs: AbstractLocal3dCrs,
24102
+ epc_external_part_reference: obj_EpcExternalPartReference,
24103
+ shape: tuple[int, int],
24104
+ origin: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24105
+ spacing: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24106
+ unit_vec_1: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24107
+ unit_vec_2: Annotated[npt.NDArray[np.float64], dict(shape=(2,))],
24108
+ patch_index: int = 0,
24109
+ path_in_hdf_file: str = "",
24110
+ uuid: str | uuid.UUID = uuid.uuid4(),
24111
+ surface_role: SurfaceRole | str = SurfaceRole.MAP,
24112
+ boundaries: list[PatchBoundaries] | None = None,
24113
+ represented_interpretation: AbstractFeatureInterpretation | None = None,
24114
+ extra_metadata: list[NameValuePair] | None = None,
24115
+ custom_data: CustomData | None = None,
24116
+ object_version: str | None = None,
24117
+ aliases: list[ObjectAlias] | None = None,
24118
+ ) -> Self:
24119
+ """
24120
+ Class method that sets up an `obj_Grid2dRepresentation`-object for a
24121
+ regular surface described by the eight parameters (seven free
24122
+ parameters) `shape`, `origin`, `dr` and `unit_vectors`, and the
24123
+ necessary (a local crs, and an epc-reference file) and/or optional
24124
+ metadata from RESQML.
24125
+ """
24126
+ uuid = str(uuid)
24127
+ surface_role = SurfaceRole(surface_role)
24128
+ boundaries = boundaries or []
24129
+ extra_metadata = extra_metadata or []
24130
+ aliases = aliases or []
24131
+ path_in_hdf_file = path_in_hdf_file or f"/RESQML/{uuid}/zvalues"
24132
+
24133
+ if represented_interpretation is not None:
24134
+ represented_interpretation = DataObjectReference.from_object(
24135
+ represented_interpretation
24136
+ )
24137
+
24138
+ grid2d_patch = Grid2dPatch.from_regular_surface(
24139
+ crs=crs,
24140
+ epc_external_part_reference=epc_external_part_reference,
24141
+ path_in_hdf_file=path_in_hdf_file,
24142
+ shape=shape,
24143
+ origin=origin,
24144
+ spacing=spacing,
24145
+ unit_vec_1=unit_vec_1,
24146
+ unit_vec_2=unit_vec_2,
24147
+ patch_index=patch_index,
24148
+ )
24149
+
24150
+ return cls(
24151
+ citation=citation,
24152
+ aliases=aliases,
24153
+ custom_data=custom_data,
24154
+ uuid=uuid,
24155
+ object_version=object_version,
24156
+ surface_role=surface_role,
24157
+ grid2d_patch=grid2d_patch,
24158
+ boundaries=boundaries,
24159
+ represented_interpretation=represented_interpretation,
24160
+ extra_metadata=extra_metadata,
24161
+ )
24162
+
23642
24163
 
23643
24164
  @dataclass(slots=True, kw_only=True)
23644
24165
  class obj_Grid2dSetRepresentation(AbstractSurfaceRepresentation):