evo-blockmodels 0.0.1__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.
- evo/blockmodels/__init__.py +16 -0
- evo/blockmodels/_model_config.py +21 -0
- evo/blockmodels/_types.py +82 -0
- evo/blockmodels/_utils.py +63 -0
- evo/blockmodels/client.py +448 -0
- evo/blockmodels/data.py +174 -0
- evo/blockmodels/endpoints/__init__.py +43 -0
- evo/blockmodels/endpoints/api/__init__.py +18 -0
- evo/blockmodels/endpoints/api/column_operations_api.py +200 -0
- evo/blockmodels/endpoints/api/jobs_api.py +126 -0
- evo/blockmodels/endpoints/api/metadata_api.py +291 -0
- evo/blockmodels/endpoints/api/operations_api.py +340 -0
- evo/blockmodels/endpoints/api/reports_api.py +801 -0
- evo/blockmodels/endpoints/api/units_api.py +111 -0
- evo/blockmodels/endpoints/api/versions_api.py +377 -0
- evo/blockmodels/endpoints/models.py +2266 -0
- evo/blockmodels/exceptions.py +47 -0
- evo/blockmodels/io.py +145 -0
- evo_blockmodels-0.0.1.dist-info/METADATA +93 -0
- evo_blockmodels-0.0.1.dist-info/RECORD +22 -0
- evo_blockmodels-0.0.1.dist-info/WHEEL +4 -0
- evo_blockmodels-0.0.1.dist-info/licenses/LICENSE.md +190 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from .client import BlockModelAPIClient
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BlockModelAPIClient",
|
|
16
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CustomBaseModel(BaseModel):
|
|
16
|
+
"""Custom base model for providing a global configuration to generated models."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(
|
|
19
|
+
extra="allow",
|
|
20
|
+
protected_namespaces=(),
|
|
21
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Protocol
|
|
15
|
+
|
|
16
|
+
# `evo-blockmodels` uses protocols for annotating some pyarrow types, because:
|
|
17
|
+
# - pyarrow is optional, but type annotations are not.
|
|
18
|
+
# - pyarrow has poor type checker support.
|
|
19
|
+
#
|
|
20
|
+
# These protocols should be treated as aliases for the corresponding pyarrow types.
|
|
21
|
+
# Any required interfaces from the corresponding pyarrow types should be added to these protocols as needed.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DataType(Protocol):
|
|
25
|
+
"""Pyarrow data type.
|
|
26
|
+
|
|
27
|
+
https://arrow.apache.org/docs/python/generated/pyarrow.DataType.html
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Schema(Protocol):
|
|
34
|
+
"""Pyarrow schema.
|
|
35
|
+
|
|
36
|
+
https://arrow.apache.org/docs/python/generated/pyarrow.Schema.html
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def names(self) -> list[str]:
|
|
41
|
+
"""The schema's field names."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def types(self) -> list[DataType]:
|
|
46
|
+
"""The schema's field types."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Table(Protocol):
|
|
51
|
+
"""Pyarrow table.
|
|
52
|
+
|
|
53
|
+
https://arrow.apache.org/docs/python/generated/pyarrow.Table.html
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def schema(self) -> Schema:
|
|
58
|
+
"""Schema of the table and its columns."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def num_columns(self) -> int:
|
|
63
|
+
"""Number of columns in this table."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def num_rows(self) -> int:
|
|
68
|
+
"""Number of rows in this table.
|
|
69
|
+
|
|
70
|
+
Due to the definition of a table, all columns have the same number of rows.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def to_pandas(self) -> DataFrame:
|
|
75
|
+
"""Convert to a pandas-compatible NumPy array or DataFrame, as appropriate"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DataFrame(Protocol):
|
|
79
|
+
"""Pandas DataFrame.
|
|
80
|
+
|
|
81
|
+
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html
|
|
82
|
+
"""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from ._types import DataType
|
|
18
|
+
from .endpoints import models
|
|
19
|
+
from .endpoints.models import JobResponse
|
|
20
|
+
from .exceptions import UnknownJobPayload
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_payload(job_id: UUID, job_response: JobResponse, payload_type: type[T]) -> T:
|
|
26
|
+
payload = job_response.payload
|
|
27
|
+
if isinstance(payload, payload_type):
|
|
28
|
+
return payload
|
|
29
|
+
else:
|
|
30
|
+
raise UnknownJobPayload(
|
|
31
|
+
job_id, f"Expected {payload_type.__name__} for job payload, got {type(payload).__name__} instead"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
import pyarrow
|
|
37
|
+
except ImportError:
|
|
38
|
+
data_type_mapping = {}
|
|
39
|
+
else:
|
|
40
|
+
data_type_mapping = {
|
|
41
|
+
pyarrow.bool_(): models.DataType.Boolean,
|
|
42
|
+
pyarrow.int8(): models.DataType.Int8,
|
|
43
|
+
pyarrow.int16(): models.DataType.Int16,
|
|
44
|
+
pyarrow.int32(): models.DataType.Int32,
|
|
45
|
+
pyarrow.int64(): models.DataType.Int64,
|
|
46
|
+
pyarrow.uint8(): models.DataType.UInt8,
|
|
47
|
+
pyarrow.uint16(): models.DataType.UInt16,
|
|
48
|
+
pyarrow.uint32(): models.DataType.UInt32,
|
|
49
|
+
pyarrow.uint64(): models.DataType.UInt64,
|
|
50
|
+
pyarrow.float16(): models.DataType.Float16,
|
|
51
|
+
pyarrow.float32(): models.DataType.Float32,
|
|
52
|
+
pyarrow.float64(): models.DataType.Float64,
|
|
53
|
+
pyarrow.utf8(): models.DataType.Utf8,
|
|
54
|
+
pyarrow.date32(): models.DataType.Date32,
|
|
55
|
+
pyarrow.timestamp("us", tz="UTC"): models.DataType.Timestamp,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def convert_dtype(data_type: DataType) -> models.DataType:
|
|
60
|
+
try:
|
|
61
|
+
return data_type_mapping[data_type]
|
|
62
|
+
except KeyError:
|
|
63
|
+
raise ValueError(f"Unsupported data type: {data_type}")
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import uuid
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from evo import logging
|
|
19
|
+
from evo.common import APIConnector, BaseAPIClient, Environment, HealthCheckType, ICache, ServiceHealth
|
|
20
|
+
from evo.common.data import ServiceUser
|
|
21
|
+
from evo.common.utils import get_service_health
|
|
22
|
+
|
|
23
|
+
from ._types import Table
|
|
24
|
+
from ._utils import convert_dtype, extract_payload
|
|
25
|
+
from .data import BaseGridDefinition, BlockModel, RegularGridDefinition, Version
|
|
26
|
+
from .endpoints import models
|
|
27
|
+
from .endpoints.api import ColumnOperationsApi, JobsApi, OperationsApi, VersionsApi
|
|
28
|
+
from .endpoints.models import (
|
|
29
|
+
AnyUrl,
|
|
30
|
+
BBox,
|
|
31
|
+
BBoxXYZ,
|
|
32
|
+
BlockSize,
|
|
33
|
+
ColumnHeaderType,
|
|
34
|
+
CreateData,
|
|
35
|
+
GeometryColumns,
|
|
36
|
+
JobErrorPayload,
|
|
37
|
+
JobResponse,
|
|
38
|
+
JobStatus,
|
|
39
|
+
Location,
|
|
40
|
+
OutputOptionsParquet,
|
|
41
|
+
QueryCriteria,
|
|
42
|
+
QueryDownload,
|
|
43
|
+
Rotation,
|
|
44
|
+
RotationAxis,
|
|
45
|
+
Size3D,
|
|
46
|
+
SizeOptionsRegular,
|
|
47
|
+
)
|
|
48
|
+
from .exceptions import CacheNotConfiguredException, JobFailedException, MissingColumnInTable
|
|
49
|
+
from .io import BlockModelDownload, BlockModelUpload, get_cache_location_for_upload
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger("blockmodel.client")
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"BlockModelAPIClient",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _job_id_from_url(job_url: AnyUrl) -> UUID:
|
|
59
|
+
job_id = job_url.path.split("/")[-1]
|
|
60
|
+
return UUID(job_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _version_from_model(version: models.Version) -> Version:
|
|
64
|
+
return Version(
|
|
65
|
+
bm_uuid=version.bm_uuid,
|
|
66
|
+
version_id=version.version_id,
|
|
67
|
+
version_uuid=version.version_uuid,
|
|
68
|
+
created_at=version.created_at,
|
|
69
|
+
created_by=ServiceUser.from_model(version.created_by),
|
|
70
|
+
comment=version.comment,
|
|
71
|
+
bbox=version.bbox,
|
|
72
|
+
base_version_id=version.base_version_id,
|
|
73
|
+
parent_version_id=version.parent_version_id,
|
|
74
|
+
columns=version.mapping.columns,
|
|
75
|
+
geoscience_version_id=version.geoscience_version_id,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_GEOMETRY_COLUMNS = {"i", "j", "k", "x", "y", "z"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BlockModelAPIClient(BaseAPIClient):
|
|
83
|
+
def __init__(self, environment: Environment, connector: APIConnector, cache: ICache | None = None) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Constructor for the Block Model Service client.
|
|
86
|
+
|
|
87
|
+
Some methods need a cache to store temporary files. If you want to use these methods, you must provide a cache.
|
|
88
|
+
|
|
89
|
+
:param environment: The environment object.
|
|
90
|
+
:param connector: The connector object.
|
|
91
|
+
:param cache: The cache to use for storing temporary files.
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(environment, connector)
|
|
94
|
+
self._versions_api = VersionsApi(connector)
|
|
95
|
+
self._jobs_api = JobsApi(connector)
|
|
96
|
+
self._operations_api = OperationsApi(connector)
|
|
97
|
+
self._column_operations_api = ColumnOperationsApi(connector)
|
|
98
|
+
self._cache = cache
|
|
99
|
+
|
|
100
|
+
async def get_service_health(self, check_type: HealthCheckType = HealthCheckType.FULL) -> ServiceHealth:
|
|
101
|
+
"""Get the health of the service.
|
|
102
|
+
|
|
103
|
+
:param check_type: The type of health check to perform.
|
|
104
|
+
|
|
105
|
+
:return: A ServiceHealth object.
|
|
106
|
+
|
|
107
|
+
:raises EvoAPIException: If the API returns an unexpected status code.
|
|
108
|
+
:raises ClientValueError: If the response is not a valid service health check response.
|
|
109
|
+
"""
|
|
110
|
+
return await get_service_health(self._connector, "blockmodel", check_type=check_type)
|
|
111
|
+
|
|
112
|
+
def _bm_from_model(self, model: models.BlockModel | models.BlockModelAndJobURL) -> BlockModel:
|
|
113
|
+
match model.size_options:
|
|
114
|
+
case SizeOptionsRegular(n_blocks=n_blocks, block_size=block_size):
|
|
115
|
+
grid_definition = RegularGridDefinition(
|
|
116
|
+
model_origin=[model.model_origin.x, model.model_origin.y, model.model_origin.z],
|
|
117
|
+
rotations=[(rotation.axis, rotation.angle) for rotation in model.block_rotation],
|
|
118
|
+
n_blocks=[n_blocks.nx, n_blocks.ny, n_blocks.nz],
|
|
119
|
+
block_size=[block_size.x, block_size.y, block_size.z],
|
|
120
|
+
)
|
|
121
|
+
case _:
|
|
122
|
+
raise NotImplementedError("Only regular models are supported at the moment")
|
|
123
|
+
|
|
124
|
+
return BlockModel(
|
|
125
|
+
environment=self._environment,
|
|
126
|
+
id=model.bm_uuid,
|
|
127
|
+
name=model.name,
|
|
128
|
+
created_at=model.created_at,
|
|
129
|
+
created_by=ServiceUser.from_model(model.created_by),
|
|
130
|
+
description=model.description,
|
|
131
|
+
grid_definition=grid_definition,
|
|
132
|
+
coordinate_reference_system=model.coordinate_reference_system,
|
|
133
|
+
size_unit_id=model.size_unit_id,
|
|
134
|
+
bbox=model.bbox,
|
|
135
|
+
last_updated_at=model.last_updated_at,
|
|
136
|
+
last_updated_by=ServiceUser.from_model(model.last_updated_by),
|
|
137
|
+
geoscience_object_id=model.geoscience_object_id,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def _poll_job_url(self, bm_id: UUID, job_id: UUID) -> JobResponse:
|
|
141
|
+
while True:
|
|
142
|
+
response = await self._jobs_api.get_job_status(
|
|
143
|
+
job_id=str(job_id),
|
|
144
|
+
workspace_id=str(self._environment.workspace_id),
|
|
145
|
+
org_id=str(self._environment.org_id),
|
|
146
|
+
bm_id=str(bm_id),
|
|
147
|
+
)
|
|
148
|
+
if response.job_status == JobStatus.COMPLETE:
|
|
149
|
+
return response
|
|
150
|
+
elif response.job_status == JobStatus.FAILED:
|
|
151
|
+
payload = extract_payload(job_id, response, JobErrorPayload)
|
|
152
|
+
raise JobFailedException(job_id, payload)
|
|
153
|
+
await asyncio.sleep(1)
|
|
154
|
+
|
|
155
|
+
async def _create_block_model(
|
|
156
|
+
self,
|
|
157
|
+
name: str,
|
|
158
|
+
grid_definition: BaseGridDefinition,
|
|
159
|
+
description: str | None = None,
|
|
160
|
+
object_path: str | None = None,
|
|
161
|
+
coordinate_reference_system: str | None = None,
|
|
162
|
+
size_unit_id: str | None = None,
|
|
163
|
+
):
|
|
164
|
+
match grid_definition:
|
|
165
|
+
case RegularGridDefinition(n_blocks=n_blocks, block_size=block_size):
|
|
166
|
+
size_option = SizeOptionsRegular(
|
|
167
|
+
n_blocks=Size3D(nx=n_blocks[0], ny=n_blocks[1], nz=n_blocks[2]),
|
|
168
|
+
block_size=BlockSize(x=block_size[0], y=block_size[1], z=block_size[2]),
|
|
169
|
+
)
|
|
170
|
+
case _:
|
|
171
|
+
raise NotImplementedError("Only regular models are supported at the moment")
|
|
172
|
+
create_result = await self._operations_api.create_block_model(
|
|
173
|
+
workspace_id=str(self._environment.workspace_id),
|
|
174
|
+
org_id=str(self._environment.org_id),
|
|
175
|
+
create_data=CreateData(
|
|
176
|
+
name=name,
|
|
177
|
+
description=description,
|
|
178
|
+
object_path=object_path,
|
|
179
|
+
coordinate_reference_system=coordinate_reference_system,
|
|
180
|
+
size_unit_id=size_unit_id,
|
|
181
|
+
model_origin=Location(
|
|
182
|
+
x=grid_definition.model_origin[0],
|
|
183
|
+
y=grid_definition.model_origin[1],
|
|
184
|
+
z=grid_definition.model_origin[2],
|
|
185
|
+
),
|
|
186
|
+
block_rotation=[
|
|
187
|
+
Rotation(axis=RotationAxis(axis), angle=angle) for axis, angle in grid_definition.rotations
|
|
188
|
+
],
|
|
189
|
+
size_options=size_option,
|
|
190
|
+
),
|
|
191
|
+
# We are setting both of the 'object_path' and 'coordinate_reference_system' fields, which are in preview.
|
|
192
|
+
additional_headers={"API-Preview": "opt-in"},
|
|
193
|
+
)
|
|
194
|
+
job_id = _job_id_from_url(create_result.job_url)
|
|
195
|
+
job_status = await self._poll_job_url(create_result.bm_uuid, job_id)
|
|
196
|
+
version = extract_payload(job_id, job_status, models.Version)
|
|
197
|
+
return create_result, _version_from_model(version)
|
|
198
|
+
|
|
199
|
+
async def _upload_data(self, bm_id: uuid.UUID, job_id: uuid.UUID, upload_url: str, data: Table) -> models.Version:
|
|
200
|
+
"""Upload data to a block model service, marks the upload as complete and waits for the job to complete."""
|
|
201
|
+
# Write the data to a temporary file
|
|
202
|
+
import pyarrow.parquet
|
|
203
|
+
|
|
204
|
+
cache_location = get_cache_location_for_upload(self._cache, self._environment, job_id)
|
|
205
|
+
pyarrow.parquet.write_table(data, cache_location)
|
|
206
|
+
|
|
207
|
+
# Upload the data
|
|
208
|
+
upload = BlockModelUpload(self._connector, self._environment, bm_id, job_id, upload_url)
|
|
209
|
+
await upload.upload_from_path(cache_location, self._connector.transport)
|
|
210
|
+
|
|
211
|
+
# Notify the service that the upload is complete
|
|
212
|
+
await self._column_operations_api.notify_upload_complete(
|
|
213
|
+
org_id=str(self._environment.org_id),
|
|
214
|
+
workspace_id=str(self._environment.workspace_id),
|
|
215
|
+
bm_id=str(bm_id),
|
|
216
|
+
job_id=str(job_id),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Poll the job URL until it is complete
|
|
220
|
+
job_status = await self._poll_job_url(bm_id, job_id)
|
|
221
|
+
return extract_payload(job_id, job_status, models.Version)
|
|
222
|
+
|
|
223
|
+
async def create_block_model(
|
|
224
|
+
self,
|
|
225
|
+
name: str,
|
|
226
|
+
grid_definition: BaseGridDefinition,
|
|
227
|
+
description: str | None = None,
|
|
228
|
+
object_path: str | None = None,
|
|
229
|
+
coordinate_reference_system: str | None = None,
|
|
230
|
+
size_unit_id: str | None = None,
|
|
231
|
+
initial_data: Table | None = None,
|
|
232
|
+
units: dict[str, str] | None = None,
|
|
233
|
+
) -> tuple[BlockModel, Version]:
|
|
234
|
+
r"""Create a block model.
|
|
235
|
+
|
|
236
|
+
Optionally, takes initial data to populate the block model with. Units for the columns within the initial data
|
|
237
|
+
can be provided in the `units` dictionary. This requires the `pyarrow` package to be installed, and the 'cache'
|
|
238
|
+
parameter to be set in the constructor.
|
|
239
|
+
|
|
240
|
+
If `initial_data` is provided, this method will wait for the initial data to be successfully uploaded and
|
|
241
|
+
processed before returning. This then returns both the block model and the version created from the initial data
|
|
242
|
+
update.
|
|
243
|
+
|
|
244
|
+
Otherwise, if `initial_data` is not provided, this waits for the block model creation job to complete before
|
|
245
|
+
returning. This then returns both the block model and the initial block model version.
|
|
246
|
+
|
|
247
|
+
:param name: Name of the block model. This may not contain `/` nor `\`.
|
|
248
|
+
:param grid_definition: Definition of the block model grid.
|
|
249
|
+
:param description: Description of the block model.
|
|
250
|
+
:param object_path: Path of the folder in Geoscience Object Service to create the reference object in.
|
|
251
|
+
:param coordinate_reference_system: Coordinate reference system used in the block model.
|
|
252
|
+
:param size_unit_id: Unit ID denoting the length unit used for the block model's blocks.
|
|
253
|
+
:param initial_data: The initial data to populate the block model with.
|
|
254
|
+
:param units: A dictionary mapping column names within `initial_data` to units.
|
|
255
|
+
:return: A tuple containing the created block model and the version of the block model.
|
|
256
|
+
"""
|
|
257
|
+
if units is not None and initial_data is None:
|
|
258
|
+
raise ValueError("units can only be provided if initial_data is provided")
|
|
259
|
+
if initial_data is not None and self._cache is None:
|
|
260
|
+
raise CacheNotConfiguredException(
|
|
261
|
+
"Cache must be configured to use this method. Please set the 'cache' parameter in the constructor."
|
|
262
|
+
)
|
|
263
|
+
create_result, version = await self._create_block_model(
|
|
264
|
+
name, grid_definition, description, object_path, coordinate_reference_system, size_unit_id
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if initial_data is not None:
|
|
268
|
+
version = await self.add_new_columns(create_result.bm_uuid, initial_data, units)
|
|
269
|
+
return self._bm_from_model(create_result), version
|
|
270
|
+
|
|
271
|
+
async def add_new_columns(
|
|
272
|
+
self,
|
|
273
|
+
bm_id: UUID,
|
|
274
|
+
data: Table,
|
|
275
|
+
units: dict[str, str] | None = None,
|
|
276
|
+
):
|
|
277
|
+
"""Add new columns to an existing block model.
|
|
278
|
+
|
|
279
|
+
Units for the columns can be provided in the `units` dictionary.
|
|
280
|
+
|
|
281
|
+
This method requires the `pyarrow` package to be installed, and the 'cache' parameter to be set in the constructor.
|
|
282
|
+
|
|
283
|
+
:param bm_id: The ID of the block model to add columns to.
|
|
284
|
+
:param data: The data containing the new columns to add.
|
|
285
|
+
:param units: A dictionary mapping column names within `data` to units.
|
|
286
|
+
:raises CacheNotConfiguredException: If the cache is not configured.
|
|
287
|
+
:return: The new version of the block model with the added columns.
|
|
288
|
+
"""
|
|
289
|
+
if self._cache is None:
|
|
290
|
+
raise CacheNotConfiguredException(
|
|
291
|
+
"Cache must be configured to use this method. Please set the 'cache' parameter in the constructor."
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
schema = data.schema
|
|
295
|
+
if units is None:
|
|
296
|
+
units = {}
|
|
297
|
+
columns = models.UpdateColumnsLiteInput(
|
|
298
|
+
new=[
|
|
299
|
+
models.ColumnLite(title=name, data_type=convert_dtype(data_type), unit_id=units.get(name))
|
|
300
|
+
for name, data_type in zip(schema.names, schema.types)
|
|
301
|
+
if name not in _GEOMETRY_COLUMNS
|
|
302
|
+
],
|
|
303
|
+
update=[],
|
|
304
|
+
delete=[],
|
|
305
|
+
rename=[],
|
|
306
|
+
)
|
|
307
|
+
update_response = await self._column_operations_api.update_block_model_from_latest_version(
|
|
308
|
+
org_id=str(self._environment.org_id),
|
|
309
|
+
workspace_id=str(self._environment.workspace_id),
|
|
310
|
+
bm_id=str(bm_id),
|
|
311
|
+
update_data_lite_input=models.UpdateDataLiteInput(
|
|
312
|
+
columns=columns,
|
|
313
|
+
update_type=models.UpdateType.replace,
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
version = await self._upload_data(bm_id, update_response.job_uuid, str(update_response.upload_url), data)
|
|
317
|
+
return _version_from_model(version)
|
|
318
|
+
|
|
319
|
+
async def update_block_model_columns(
|
|
320
|
+
self,
|
|
321
|
+
bm_id: UUID,
|
|
322
|
+
data: Table,
|
|
323
|
+
new_columns: list[str],
|
|
324
|
+
update_columns: set[str] | None = None,
|
|
325
|
+
delete_columns: set[str] | None = None,
|
|
326
|
+
units: dict[str, str] | None = None,
|
|
327
|
+
) -> Version:
|
|
328
|
+
"""Add, update, or delete block model columns.
|
|
329
|
+
|
|
330
|
+
Units for the columns can be provided in the `units` dictionary.
|
|
331
|
+
|
|
332
|
+
This method requires the `pyarrow` package to be installed, and the 'cache' parameter to be set in the constructor.
|
|
333
|
+
|
|
334
|
+
:param bm_id: The ID of the block model to add columns to.
|
|
335
|
+
:param data: The data containing the new columns to add.
|
|
336
|
+
:param new_columns: A list of new column names to add to the block model.
|
|
337
|
+
:param update_columns: A set of column names to update in the block model.
|
|
338
|
+
:param delete_columns: A set of column names to delete from the block model.
|
|
339
|
+
:param units: A dictionary mapping column names within `data` to units.
|
|
340
|
+
:raises CacheNotConfiguredException: If the cache is not configured.
|
|
341
|
+
:return: The new version of the block model with the added columns.
|
|
342
|
+
"""
|
|
343
|
+
if self._cache is None:
|
|
344
|
+
raise CacheNotConfiguredException(
|
|
345
|
+
"Cache must be configured to use this method. Please set the 'cache' parameter in the constructor."
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
schema = data.schema
|
|
349
|
+
if units is None:
|
|
350
|
+
units = {}
|
|
351
|
+
data_type_map = {name: data_type for name, data_type in zip(schema.names, schema.types)}
|
|
352
|
+
|
|
353
|
+
if update_columns is None:
|
|
354
|
+
update_columns = set()
|
|
355
|
+
|
|
356
|
+
if delete_columns is None:
|
|
357
|
+
delete_columns = set()
|
|
358
|
+
|
|
359
|
+
# Check for any new or updated columns that are not in the data
|
|
360
|
+
missing = (set(new_columns) | update_columns) - data_type_map.keys()
|
|
361
|
+
if missing:
|
|
362
|
+
raise MissingColumnInTable(f"Columns {missing} are not present in the provided table.")
|
|
363
|
+
|
|
364
|
+
columns = models.UpdateColumnsLiteInput(
|
|
365
|
+
new=[
|
|
366
|
+
models.ColumnLite(
|
|
367
|
+
title=new_column, data_type=convert_dtype(data_type_map[new_column]), unit_id=units.get(new_column)
|
|
368
|
+
)
|
|
369
|
+
for new_column in new_columns
|
|
370
|
+
],
|
|
371
|
+
update=list(update_columns),
|
|
372
|
+
delete=list(delete_columns),
|
|
373
|
+
rename=[],
|
|
374
|
+
)
|
|
375
|
+
update_response = await self._column_operations_api.update_block_model_from_latest_version(
|
|
376
|
+
org_id=str(self._environment.org_id),
|
|
377
|
+
workspace_id=str(self._environment.workspace_id),
|
|
378
|
+
bm_id=str(bm_id),
|
|
379
|
+
update_data_lite_input=models.UpdateDataLiteInput(
|
|
380
|
+
columns=columns,
|
|
381
|
+
update_type=models.UpdateType.replace,
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
version = await self._upload_data(bm_id, update_response.job_uuid, str(update_response.upload_url), data)
|
|
385
|
+
return _version_from_model(version)
|
|
386
|
+
|
|
387
|
+
async def query_block_model_as_table(
|
|
388
|
+
self,
|
|
389
|
+
bm_id: UUID,
|
|
390
|
+
columns: list[str | UUID],
|
|
391
|
+
bbox: BBox | BBoxXYZ | None = None,
|
|
392
|
+
version_uuid: UUID | None = None,
|
|
393
|
+
geometry_columns: GeometryColumns = GeometryColumns.coordinates,
|
|
394
|
+
column_headers: ColumnHeaderType = ColumnHeaderType.id,
|
|
395
|
+
exclude_null_rows: bool = True,
|
|
396
|
+
) -> Table:
|
|
397
|
+
"""Query a block model and return the result as a PyArrow Table.
|
|
398
|
+
|
|
399
|
+
This requires the `pyarrow` package to be installed, and the 'cache' parameter to be set in the constructor.
|
|
400
|
+
|
|
401
|
+
:param bm_id: The ID of the block model to query.
|
|
402
|
+
:param columns: The columns to query, can either be the title or the ID of the column.
|
|
403
|
+
:param bbox: The bounding box to query, if None (the default) the entire block model is queried.
|
|
404
|
+
:param version_uuid: The version UUID to query, if None (the default) the latest version is queried.
|
|
405
|
+
:param geometry_columns: Whether rows in the returned table should include coordinates, or block indices of the
|
|
406
|
+
block, that the row belongs to.
|
|
407
|
+
:param column_headers: Whether the names of the columns in the returned column should be the title or the ID of
|
|
408
|
+
the block model column.
|
|
409
|
+
:param exclude_null_rows: Whether to exclude rows where all values are null within the queried columns.
|
|
410
|
+
:return: The result as a PyArrow Table.
|
|
411
|
+
:raises JobFailedException: If the job failed.
|
|
412
|
+
"""
|
|
413
|
+
import pyarrow.parquet
|
|
414
|
+
|
|
415
|
+
if self._cache is None:
|
|
416
|
+
raise CacheNotConfiguredException(
|
|
417
|
+
"Cache must be configured to use this method. Please set the 'cache' parameter in the constructor."
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
query_result = await self._versions_api.query_block_model_latest_as_post(
|
|
421
|
+
bm_id=str(bm_id),
|
|
422
|
+
workspace_id=str(self._environment.workspace_id),
|
|
423
|
+
org_id=str(self._environment.org_id),
|
|
424
|
+
query_criteria=QueryCriteria(
|
|
425
|
+
columns=[str(c) for c in columns], # Convert any UUIDs to strings
|
|
426
|
+
bbox=bbox,
|
|
427
|
+
version_uuid=version_uuid,
|
|
428
|
+
geometry_columns=geometry_columns,
|
|
429
|
+
output_options=OutputOptionsParquet(
|
|
430
|
+
column_headers=column_headers,
|
|
431
|
+
exclude_null_rows=exclude_null_rows,
|
|
432
|
+
),
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Poll the job URL until it is complete
|
|
437
|
+
job_id = _job_id_from_url(query_result.job_url)
|
|
438
|
+
job = await self._poll_job_url(bm_id, job_id)
|
|
439
|
+
payload = extract_payload(job_id, job, QueryDownload)
|
|
440
|
+
|
|
441
|
+
# Download the result to a temporary file
|
|
442
|
+
download = BlockModelDownload(
|
|
443
|
+
self._connector, self._environment, query_result, job_id, str(payload.download_url)
|
|
444
|
+
)
|
|
445
|
+
path = await download.download_to_cache(self._cache, self._connector.transport)
|
|
446
|
+
|
|
447
|
+
# Read the PyArrow Table from the temporary file
|
|
448
|
+
return pyarrow.parquet.read_table(path)
|