pyetp 0.0.44__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.
- pyetp/__init__.py +1 -2
- pyetp/_version.py +2 -2
- pyetp/client.py +205 -138
- pyetp/uri.py +3 -1
- pyetp/utils_arrays.py +1 -7
- pyetp/utils_xml.py +1 -6
- {pyetp-0.0.44.dist-info → pyetp-0.0.46.dist-info}/METADATA +12 -3
- pyetp-0.0.46.dist-info/RECORD +22 -0
- {pyetp-0.0.44.dist-info → pyetp-0.0.46.dist-info}/WHEEL +1 -1
- resqml_objects/epc_readers.py +3 -7
- resqml_objects/parsers.py +18 -5
- resqml_objects/serializers.py +25 -2
- resqml_objects/surface_helpers.py +295 -0
- resqml_objects/v201/generated.py +540 -19
- pyetp-0.0.44.dist-info/RECORD +0 -21
- {pyetp-0.0.44.dist-info → pyetp-0.0.46.dist-info}/licenses/LICENSE.md +0 -0
- {pyetp-0.0.44.dist-info → pyetp-0.0.46.dist-info}/top_level.txt +0 -0
resqml_objects/serializers.py
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
+
from xsdata.formats.dataclass.models.generics import DerivedElement
|
|
1
2
|
from xsdata.formats.dataclass.serializers import XmlSerializer
|
|
2
3
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
|
3
4
|
|
|
4
5
|
import resqml_objects.v201 as ro_201
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
def serialize_resqml_v201_object(
|
|
8
|
+
def serialize_resqml_v201_object(
|
|
9
|
+
obj: ro_201.AbstractObject | DerivedElement[ro_201.AbstractObject],
|
|
10
|
+
) -> bytes:
|
|
8
11
|
serializer = XmlSerializer(config=SerializerConfig())
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
namespace = getattr(obj.Meta, "namespace", None) or obj.Meta.target_namespace
|
|
14
|
+
name = obj.__class__.__name__
|
|
15
|
+
|
|
16
|
+
# This is a solution to enforce the inclusion of the `xsi:type`-attribute
|
|
17
|
+
# on the generated XML-elements.
|
|
18
|
+
if not isinstance(obj, DerivedElement) and name.startswith("obj_"):
|
|
19
|
+
obj = DerivedElement(
|
|
20
|
+
qname=f"{{{namespace}}}{name[4:]}",
|
|
21
|
+
value=obj,
|
|
22
|
+
type=f"{{{namespace}}}{name}",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return str.encode(
|
|
26
|
+
serializer.render(
|
|
27
|
+
obj,
|
|
28
|
+
ns_map={
|
|
29
|
+
"eml": "http://www.energistics.org/energyml/data/commonv2",
|
|
30
|
+
"resqml2": "http://www.energistics.org/energyml/data/resqmlv2",
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
|
|
7
|
+
DType = typing.TypeVar("DType", bound=np.float32 | np.float64)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rotate_2d_vector(r: npt.NDArray[DType], angle: float) -> npt.NDArray[DType]:
|
|
11
|
+
"""
|
|
12
|
+
Function used to rotate a set of `N` 2d-vectors stacked in an array of
|
|
13
|
+
shape `(2, N)` by an angle `angle` (in radians) counter-clockwise. This
|
|
14
|
+
function does not remove the origin of the vectors, so that is up to the
|
|
15
|
+
user if this should be done.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
npt.NDArray[DType]
|
|
20
|
+
The rotated 2d vector with the same shape as the input vector.
|
|
21
|
+
"""
|
|
22
|
+
if not np.shape(r)[0] == 2:
|
|
23
|
+
raise TypeError("Array 'r' must have shape '(2,)' or '(2, None)'")
|
|
24
|
+
|
|
25
|
+
if len(np.shape(r)) == 1:
|
|
26
|
+
r = r.reshape(-1, 1)
|
|
27
|
+
|
|
28
|
+
c = np.cos(angle / 2.0)
|
|
29
|
+
s = np.sin(angle / 2.0)
|
|
30
|
+
|
|
31
|
+
return (c**2 - s**2) * r + 2 * c * s * np.vstack([-r[1], r[0]])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def angle_to_unit_vectors(
|
|
35
|
+
angle: float,
|
|
36
|
+
) -> typing.Annotated[npt.NDArray[np.float64], dict(shape=(2, 2))]:
|
|
37
|
+
"""
|
|
38
|
+
Function that constructs a pair of orthonormal unit vectors from an angle
|
|
39
|
+
(in radians), where the first unit vector (the `x`-direction) is rotated
|
|
40
|
+
counter-clockwise to the `[1.0, 0.0]`-axis, and the second unit vector is
|
|
41
|
+
rotated `pi/2.0` compared to the `x`-unit vector.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
return rotate_2d_vector(np.eye(2).astype(np.float64), angle)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def unit_vectors_to_angle(
|
|
48
|
+
unit_vectors: typing.Annotated[npt.NDArray[DType], dict(shape=(2, 2))],
|
|
49
|
+
) -> float:
|
|
50
|
+
"""
|
|
51
|
+
Function returning the angle (in radians) from the `x`-axis (i.e., the
|
|
52
|
+
`[1.0, 0.0]`-vector) to the `unit_vectors[:, 0]`-vector. This function
|
|
53
|
+
takes in a pair of unit vectors, where the second vector (`unit_vectors[:,
|
|
54
|
+
1]`) is only used to check that the vectors are orthonormal, and that it is
|
|
55
|
+
rotated `np.pi / 2.0` counter-clockwise relative to the first vector.
|
|
56
|
+
|
|
57
|
+
This function uses `np.atan2` to choose the correct quadrant.
|
|
58
|
+
|
|
59
|
+
Note this entire function can be replaced by `np.atan2` (or `np.angle`
|
|
60
|
+
making the `y`-component of the `x`-vector complex) for the `x`-vector of
|
|
61
|
+
the unit vectors, but is kept to give an interface where you can pass in
|
|
62
|
+
both unit vectors and verify that they are orthonormal and rotated the
|
|
63
|
+
expected way relative to each other.
|
|
64
|
+
"""
|
|
65
|
+
x_vec = unit_vectors[:, 0]
|
|
66
|
+
y_vec = unit_vectors[:, 1]
|
|
67
|
+
|
|
68
|
+
tol = np.finfo(x_vec.dtype).eps * 100
|
|
69
|
+
if not np.allclose(x_vec @ y_vec, 0.0, atol=tol):
|
|
70
|
+
raise ValueError("Unit vectors are not orthonormal")
|
|
71
|
+
|
|
72
|
+
angle = np.atan2(x_vec[1], x_vec[0])
|
|
73
|
+
|
|
74
|
+
# Check to ensure that the `y`-vector is rotated `pi / 2.0`
|
|
75
|
+
# counter-clockwise from the `x`-vector.
|
|
76
|
+
y_angle = np.atan2(y_vec[1], y_vec[0])
|
|
77
|
+
|
|
78
|
+
if angle == np.pi / 2.0:
|
|
79
|
+
if y_angle >= 0.0:
|
|
80
|
+
np.testing.assert_allclose(
|
|
81
|
+
angle,
|
|
82
|
+
y_angle - np.pi / 2.0,
|
|
83
|
+
atol=tol,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
np.testing.assert_allclose(
|
|
87
|
+
angle,
|
|
88
|
+
y_angle + np.pi,
|
|
89
|
+
atol=tol,
|
|
90
|
+
)
|
|
91
|
+
elif np.pi / 2.0 < angle <= np.pi:
|
|
92
|
+
np.testing.assert_allclose(angle, y_angle + 3 * np.pi / 2.0, atol=tol)
|
|
93
|
+
else:
|
|
94
|
+
np.testing.assert_allclose(
|
|
95
|
+
angle,
|
|
96
|
+
y_angle - np.pi / 2.0,
|
|
97
|
+
atol=tol,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return angle
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class RegularGridParameters(typing.Generic[DType]):
|
|
105
|
+
"""
|
|
106
|
+
Dataclass acting as a set of helper functions and container for regular
|
|
107
|
+
two-dimensional `X` and `Y` grids that follow a surface `Z`. The purpose of
|
|
108
|
+
this dataclass is to either take in uniformly spaced, regular, grids `X`
|
|
109
|
+
and `Y` (can be rotated), or grid vectors `x` and `y`, and store the
|
|
110
|
+
`shape`, `origin`, `spacing` and `unit_vectors` that are needed to
|
|
111
|
+
reconstruct the full grids, and provide methods that returns the full
|
|
112
|
+
grids. Furthermore, the class has optional `crs_angle` (set to `0.0` by
|
|
113
|
+
default, and measured in radians) which makes it convenient to work with
|
|
114
|
+
rotated grids inside rotated local coordinate reference systems.
|
|
115
|
+
|
|
116
|
+
Attributes
|
|
117
|
+
----------
|
|
118
|
+
shape: tuple[int, int]
|
|
119
|
+
The shape of the full `X` and `Y` grids and the corresponding `Z`-array
|
|
120
|
+
of surface values.
|
|
121
|
+
origin: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))]
|
|
122
|
+
A two-dimensional array of origin values. Corresponds to `X[0, 0]` and
|
|
123
|
+
`Y[0, 0]` in the full grids.
|
|
124
|
+
spacing: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))]
|
|
125
|
+
A two-dimensional array with the grid spacing in each direction.
|
|
126
|
+
unit_vectors: typing.Annotated[npt.NDArray[DType], dict(shape=(2, 2))]
|
|
127
|
+
Two two-dimensional unit vectors of the `X` and `Y` grids. The vectors
|
|
128
|
+
lie in the columns of `unit_vectors`, i.e., `vec_1 = unit_vectors[:,
|
|
129
|
+
0]` and `vec_2 = unit_vectors[:, 1]`.
|
|
130
|
+
crs_angle: float
|
|
131
|
+
The rotation of the local coordinate reference system, if applicable.
|
|
132
|
+
The default is `0.0`, i.e., an urotated coordinate system is used.
|
|
133
|
+
crs_offset: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))] | None
|
|
134
|
+
The offset of the origin of the local coordinate reference system, if
|
|
135
|
+
applicable. The default is `None` which gets defaulted to `array([0.0,
|
|
136
|
+
0.0])` of type `DType`.
|
|
137
|
+
|
|
138
|
+
Note that we construct the edge of `X` from `shape[0]`, `origin[0]`,
|
|
139
|
+
`spacing[0]`, and `unit_vectors[:, 0]`, and the edge of `Y` from the second
|
|
140
|
+
element/column.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
shape: tuple[int, int]
|
|
144
|
+
origin: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))]
|
|
145
|
+
spacing: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))]
|
|
146
|
+
unit_vectors: typing.Annotated[npt.NDArray[DType], dict(shape=(2, 2))]
|
|
147
|
+
crs_angle: float = 0.0
|
|
148
|
+
crs_offset: typing.Annotated[npt.NDArray[DType], dict(shape=(2,))] | None = None
|
|
149
|
+
|
|
150
|
+
# Attaching the helper functions to this class to avoid doing extra
|
|
151
|
+
# imports.
|
|
152
|
+
rotate_2d_vector = staticmethod(rotate_2d_vector)
|
|
153
|
+
angle_to_unit_vectors = staticmethod(angle_to_unit_vectors)
|
|
154
|
+
unit_vectors_to_angle = staticmethod(unit_vectors_to_angle)
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
if self.crs_offset is None:
|
|
158
|
+
self.crs_offset = np.zeros_like(self.origin)
|
|
159
|
+
|
|
160
|
+
def to_xy_grid(
|
|
161
|
+
self, to_global_crs: bool = True
|
|
162
|
+
) -> tuple[
|
|
163
|
+
npt.NDArray[DType],
|
|
164
|
+
npt.NDArray[DType],
|
|
165
|
+
]:
|
|
166
|
+
vec_1 = self.unit_vectors[:, 0]
|
|
167
|
+
vec_2 = self.unit_vectors[:, 1]
|
|
168
|
+
origin = self.origin
|
|
169
|
+
|
|
170
|
+
if to_global_crs:
|
|
171
|
+
# Here we construct the unit vectors of the global CRS as seen from
|
|
172
|
+
# the local CRS (global-in-local -> ginl). Hence, the negative sign
|
|
173
|
+
# on the rotation. The global unit vectors are in the columns (axis
|
|
174
|
+
# 1) of the 2 x 2 matrix.
|
|
175
|
+
ginl_unit_vectors = self.angle_to_unit_vectors(-self.crs_angle)
|
|
176
|
+
# Next, we find the elements of the grid unit vectors in the global
|
|
177
|
+
# coordinate system.
|
|
178
|
+
#
|
|
179
|
+
# (new_vec_i)_k = (vec_i)_k @ e_k,
|
|
180
|
+
#
|
|
181
|
+
# where `e_k` is the `k`'th column of `ginl_unit_vectors` and
|
|
182
|
+
# `(vec_i)_k` is the `k`'th element of `vec_i` (with `i` being
|
|
183
|
+
# either `1` or `2`). Below we do the full transformation in a
|
|
184
|
+
# single call.
|
|
185
|
+
new_unit_vectors = np.einsum(
|
|
186
|
+
"ij, kj -> ik", self.unit_vectors, ginl_unit_vectors
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
vec_1 = new_unit_vectors[:, 0]
|
|
190
|
+
vec_2 = new_unit_vectors[:, 1]
|
|
191
|
+
|
|
192
|
+
# Computing the origin of the surface in the global CRS by adding
|
|
193
|
+
# in the offset of the local CRS.
|
|
194
|
+
origin = origin + self.crs_offset
|
|
195
|
+
|
|
196
|
+
edge_1 = vec_1 * self.spacing[0] * np.arange(self.shape[0]).reshape(-1, 1)
|
|
197
|
+
edge_2 = vec_2 * self.spacing[1] * np.arange(self.shape[1]).reshape(-1, 1)
|
|
198
|
+
|
|
199
|
+
X = edge_1[:, 0].reshape(-1, 1) + edge_2[:, 0]
|
|
200
|
+
Y = edge_1[:, 1].reshape(-1, 1) + edge_2[:, 1]
|
|
201
|
+
|
|
202
|
+
return X + origin[0], Y + origin[1]
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def from_xy_grid(
|
|
206
|
+
cls,
|
|
207
|
+
X: npt.NDArray[DType],
|
|
208
|
+
Y: npt.NDArray[DType],
|
|
209
|
+
crs_angle: float = 0.0,
|
|
210
|
+
crs_offset: npt.NDArray[DType] | None = None,
|
|
211
|
+
) -> typing.Self:
|
|
212
|
+
if len(np.shape(np.squeeze(X))) == 1 and len(np.shape(np.squeeze(Y))) == 1:
|
|
213
|
+
return cls.from_xy_grid_vectors(
|
|
214
|
+
X, Y, crs_angle=crs_angle, crs_offset=crs_offset
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
assert len(np.shape(X)) == 2
|
|
218
|
+
assert np.shape(X) == np.shape(Y)
|
|
219
|
+
assert X.dtype == Y.dtype
|
|
220
|
+
|
|
221
|
+
x_col_diffs = np.diff(X, axis=1)
|
|
222
|
+
y_col_diffs = np.diff(Y, axis=1)
|
|
223
|
+
x_row_diffs = np.diff(X, axis=0)
|
|
224
|
+
y_row_diffs = np.diff(Y, axis=0)
|
|
225
|
+
|
|
226
|
+
tol = np.finfo(X.dtype).eps * 10
|
|
227
|
+
# Check that the spacing is uniform in all directions.
|
|
228
|
+
np.testing.assert_allclose(x_col_diffs, x_col_diffs[0, 0], atol=tol)
|
|
229
|
+
np.testing.assert_allclose(y_col_diffs, y_col_diffs[0, 0], atol=tol)
|
|
230
|
+
np.testing.assert_allclose(x_row_diffs, x_row_diffs[0, 0], atol=tol)
|
|
231
|
+
np.testing.assert_allclose(y_row_diffs, y_row_diffs[0, 0], atol=tol)
|
|
232
|
+
|
|
233
|
+
xvec = np.array([x_row_diffs[0, 0], y_row_diffs[0, 0]])
|
|
234
|
+
yvec = np.array([x_col_diffs[0, 0], y_col_diffs[0, 0]])
|
|
235
|
+
|
|
236
|
+
spacing = np.array([np.linalg.norm(xvec), np.linalg.norm(yvec)])
|
|
237
|
+
xu = xvec / spacing[0]
|
|
238
|
+
yu = yvec / spacing[1]
|
|
239
|
+
|
|
240
|
+
# Verify that the unit vectors are orthonormal.
|
|
241
|
+
np.testing.assert_allclose(abs(xu @ yu) ** 2, 0, atol=tol)
|
|
242
|
+
|
|
243
|
+
unit_vectors = np.column_stack([xu, yu])
|
|
244
|
+
# TODO: Remove these
|
|
245
|
+
np.testing.assert_equal(unit_vectors[:, 0], xu)
|
|
246
|
+
np.testing.assert_equal(unit_vectors[:, 1], yu)
|
|
247
|
+
|
|
248
|
+
return cls(
|
|
249
|
+
shape=np.shape(X),
|
|
250
|
+
origin=np.array([X[0, 0], Y[0, 0]]),
|
|
251
|
+
spacing=spacing,
|
|
252
|
+
unit_vectors=unit_vectors,
|
|
253
|
+
crs_angle=crs_angle,
|
|
254
|
+
crs_offset=crs_offset,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def from_xy_grid_vectors(
|
|
259
|
+
cls,
|
|
260
|
+
x: npt.NDArray[DType],
|
|
261
|
+
y: npt.NDArray[DType],
|
|
262
|
+
crs_angle: float = 0.0,
|
|
263
|
+
crs_offset: npt.NDArray[DType] | None = None,
|
|
264
|
+
) -> typing.Self:
|
|
265
|
+
x = np.squeeze(x)
|
|
266
|
+
y = np.squeeze(y)
|
|
267
|
+
|
|
268
|
+
if len(np.shape(x)) != 1 or len(np.shape(y)) != 1:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"The 'x' and 'y' grid vectors must be 1-d arrays (after using "
|
|
271
|
+
"`np.squeeze`)"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
x_spacing = np.diff(x)
|
|
275
|
+
y_spacing = np.diff(y)
|
|
276
|
+
|
|
277
|
+
tol = np.finfo(x.dtype).eps * 10
|
|
278
|
+
if not np.allclose(x_spacing, x_spacing[0], atol=tol) or not np.allclose(
|
|
279
|
+
y_spacing, y_spacing[0], atol=tol
|
|
280
|
+
):
|
|
281
|
+
raise ValueError("The 'x' and 'y' grid vectors must have a uniform spacing")
|
|
282
|
+
|
|
283
|
+
assert x.dtype == y.dtype
|
|
284
|
+
|
|
285
|
+
shape = (len(x), len(y))
|
|
286
|
+
unit_vectors = np.eye(2).astype(x.dtype)
|
|
287
|
+
|
|
288
|
+
return cls(
|
|
289
|
+
shape=shape,
|
|
290
|
+
origin=np.array([x[0], y[0]]),
|
|
291
|
+
spacing=np.array([x_spacing[0], y_spacing[0]]),
|
|
292
|
+
unit_vectors=unit_vectors,
|
|
293
|
+
crs_angle=crs_angle,
|
|
294
|
+
crs_offset=crs_offset,
|
|
295
|
+
)
|