Skip to content

Commit 1813445

Browse files
better export with numpy
1 parent 0b3dbf1 commit 1813445

7 files changed

Lines changed: 1583 additions & 424 deletions

File tree

energyml-utils/.gitignore

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,24 @@ gen*/
4848
manip*
4949
*.epc
5050
*.h5
51-
*.off
52-
*.obj
5351
*.log
54-
*.geojson
5552
*.json
5653
*.csv
5754
*.zip
5855

56+
5957
*.xml
6058
*.json
6159
docs/*.md
6260

6361
# DATA
6462
*.obj
63+
*.off
64+
*.mtl
6565
*.geojson
6666
*.vtk
67+
*.vtp
68+
*.vtu
6769
*.stl
6870
rc/specs
6971
rc/**/*.epc
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Copyright (c) 2023-2024 Geosiris.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example: export NumpyMultiMesh objects from an EPC file to all supported formats.
5+
6+
Demonstrates:
7+
- Reading meshes via read_numpy_mesh_object (NumpyMultiMesh)
8+
- Building RepresentationContext per object for colour metadata
9+
- Exporting to OBJ (+.mtl), GeoJSON, VTK Legacy ASCII, VTK Legacy Binary,
10+
VTK XML UnstructuredGrid (.vtu), VTK XML PolyData (.vtp), STL
11+
- Two passes: with and without CRS displacement
12+
13+
Usage::
14+
15+
# from the workspace root
16+
poetry run python example/main_test_numpy_export.py <path/to/file.epc> <output_dir>
17+
18+
# defaults (uses bundled test EPC files when no args are given)
19+
poetry run python example/main_test_numpy_export.py
20+
"""
21+
22+
import datetime
23+
import logging
24+
import os
25+
import re
26+
import sys
27+
import traceback
28+
from pathlib import Path
29+
from typing import Dict, Optional
30+
31+
logging.basicConfig(
32+
level=logging.INFO,
33+
format="%(asctime)s %(levelname)-7s %(message)s",
34+
stream=sys.stdout,
35+
)
36+
log = logging.getLogger(__name__)
37+
38+
# ---------------------------------------------------------------------------
39+
# Lazy import guards — pyvista is strictly optional
40+
# ---------------------------------------------------------------------------
41+
try:
42+
from energyml.utils.data.mesh_numpy import read_numpy_mesh_object
43+
from energyml.utils.data.representation_context import RepresentationContext
44+
from energyml.utils.data.export import (
45+
ExportFormat,
46+
VTKExportOptions,
47+
VTKFormat,
48+
STLExportOptions,
49+
GeoJSONExportOptions,
50+
export_mesh,
51+
)
52+
from energyml.utils.epc_stream import EpcStreamReader
53+
from energyml.utils.epc import Epc
54+
from energyml.utils.exception import NotSupportedError
55+
from energyml.utils.introspection import get_obj_uuid
56+
except ImportError as exc:
57+
log.error("Could not import energyml-utils modules: %s", exc)
58+
sys.exit(1)
59+
60+
61+
# ---------------------------------------------------------------------------
62+
# Core export routine
63+
# ---------------------------------------------------------------------------
64+
65+
66+
def export_all_numpy(
67+
epc_path: str,
68+
output_dir: str,
69+
regex_type_filter: Optional[str] = None,
70+
use_crs_displacement: bool = True,
71+
) -> None:
72+
"""Read every Representation in *epc_path* via the numpy pipeline and
73+
export it to all supported formats.
74+
75+
:param epc_path: Path to the ``.epc`` file.
76+
:param output_dir: Directory where output files are written (created if absent).
77+
:param regex_type_filter: Optional regex; only objects whose type name matches
78+
are exported (case-insensitive).
79+
:param use_crs_displacement: When True, CRS origin/axis offsets are applied to
80+
the exported coordinates. Two passes are run by the top-level script: one
81+
with True and one with False.
82+
"""
83+
tag = "crs" if use_crs_displacement else "nocrs"
84+
# storage = EpcStreamReader(epc_path, keep_open=True)
85+
storage = Epc.read_file(epc_path)
86+
dt = datetime.datetime.now().strftime("%Hh%M_%d-%m-%Y")
87+
88+
not_supported_types: set = set()
89+
exported_count = 0
90+
91+
for mdata in storage.list_objects():
92+
if "Representation" not in mdata.object_type:
93+
continue
94+
if regex_type_filter and not re.search(regex_type_filter, mdata.object_type, flags=re.IGNORECASE):
95+
continue
96+
97+
log.info("Processing %s (%s)", mdata.object_type, mdata.uuid)
98+
energyml_obj = storage.get_object_by_uuid(mdata.uuid)[0]
99+
100+
try:
101+
# ---- 1. Read as NumpyMultiMesh --------------------------------
102+
multi_mesh = read_numpy_mesh_object(
103+
energyml_object=energyml_obj,
104+
workspace=storage,
105+
# Read with displacement=False so the exporter controls it.
106+
use_crs_displacement=False,
107+
)
108+
109+
if multi_mesh is None or multi_mesh.patch_count() == 0:
110+
log.info(" → no patches, skipping.")
111+
continue
112+
113+
# ---- 2. Build RepresentationContext for colour metadata --------
114+
ctx = RepresentationContext(energyml_obj, storage)
115+
source_uuid = get_obj_uuid(energyml_obj)
116+
contexts: Dict[str, RepresentationContext] = {source_uuid: ctx}
117+
118+
# Also index children by their source_uuid for colour lookup
119+
for patch in multi_mesh.flat_patches():
120+
patch_uuid = patch.source_uuid
121+
if patch_uuid and patch_uuid not in contexts:
122+
patch_obj = storage.get_object_by_uuid(patch_uuid)
123+
if patch_obj:
124+
contexts[patch_uuid] = RepresentationContext(patch_obj[0], storage)
125+
126+
# ---- 3. Prepare output directory / base filename ---------------
127+
os.makedirs(output_dir, exist_ok=True)
128+
stem = f"{dt}-{mdata.object_type}_{mdata.uuid}_{tag}"
129+
base = Path(output_dir) / stem
130+
131+
# ---- 4. Export to every format ---------------------------------
132+
formats_to_export = [
133+
(f"{base}.obj", ExportFormat.OBJ, None),
134+
(f"{base}.geojson", ExportFormat.GEOJSON, GeoJSONExportOptions(indent=None)),
135+
(f"{base}.vtk", ExportFormat.VTK, VTKExportOptions(vtk_format=VTKFormat.LEGACY_ASCII)),
136+
(f"{base}_binary.vtk", ExportFormat.VTK, VTKExportOptions(vtk_format=VTKFormat.LEGACY_BINARY)),
137+
(f"{base}.vtu", ExportFormat.VTU, VTKExportOptions(vtk_format=VTKFormat.VTU)),
138+
(f"{base}.vtp", ExportFormat.VTP, VTKExportOptions(vtk_format=VTKFormat.VTP)),
139+
(f"{base}_binary.stl", ExportFormat.STL, STLExportOptions(binary=True)),
140+
(f"{base}_ascii.stl", ExportFormat.STL, STLExportOptions(binary=False)),
141+
]
142+
143+
for path_str, fmt, opts in formats_to_export:
144+
try:
145+
export_mesh(
146+
mesh_list=multi_mesh,
147+
output_path=path_str,
148+
format=fmt,
149+
options=opts,
150+
contexts=contexts,
151+
use_crs_displacement=use_crs_displacement,
152+
)
153+
log.info(" ✓ %s", Path(path_str).name)
154+
except Exception: # noqa: BLE001
155+
log.warning(" ✗ %s — export failed:", Path(path_str).name)
156+
traceback.print_exc()
157+
158+
exported_count += 1
159+
160+
except NotSupportedError as e:
161+
not_supported_types.add(mdata.object_type)
162+
log.debug(" Not supported: %s", e)
163+
except Exception:
164+
traceback.print_exc()
165+
166+
log.info("")
167+
log.info("Done. Exported %d objects -> %s", exported_count, output_dir)
168+
if not_supported_types:
169+
log.info("Unsupported representation types skipped:")
170+
for t in sorted(not_supported_types):
171+
log.info(" - %s", t)
172+
173+
174+
# ---------------------------------------------------------------------------
175+
# Entry point
176+
# ---------------------------------------------------------------------------
177+
178+
if __name__ == "__main__":
179+
# Allow: main_test_numpy_export.py [epc_path] [output_dir]
180+
args = sys.argv[1:]
181+
182+
if len(args) >= 1:
183+
epc_file = args[0]
184+
else:
185+
# Fall back to the bundled test EPC in the workspace
186+
candidates = [
187+
"rc/epc/testingPackageCpp22.epc",
188+
"rc/epc/testingPackageCpp.epc",
189+
]
190+
epc_file = next((p for p in candidates if Path(p).exists()), None)
191+
if epc_file is None:
192+
log.error(
193+
"No EPC file found. Pass a path as the first argument or place a "
194+
".epc file at rc/epc/testingPackageCpp22.epc"
195+
)
196+
sys.exit(1)
197+
198+
base_output = args[1] if len(args) >= 2 else "exported_meshes/numpy_export"
199+
200+
log.info("=" * 60)
201+
log.info("EPC : %s", epc_file)
202+
log.info("OUT : %s", base_output)
203+
log.info("=" * 60)
204+
205+
# Pass 1 — with CRS displacement
206+
log.info("\n--- Pass 1: use_crs_displacement=True ---\n")
207+
export_all_numpy(
208+
epc_path=epc_file,
209+
output_dir=f"{base_output}/with_crs",
210+
use_crs_displacement=True,
211+
)
212+
213+
# Pass 2 — raw coordinates (no CRS displacement)
214+
log.info("\n--- Pass 2: use_crs_displacement=False ---\n")
215+
export_all_numpy(
216+
epc_path=epc_file,
217+
output_dir=f"{base_output}/no_crs",
218+
use_crs_displacement=False,
219+
)

0 commit comments

Comments
 (0)