Skip to content

Commit ccd87dc

Browse files
authored
Merge pull request #314 from ax3l/topic-extendedWritePyExample
Python: Extended Write Example
2 parents 70c8065 + 4958114 commit ccd87dc

File tree

6 files changed

+235
-14
lines changed

6 files changed

+235
-14
lines changed

CHANGELOG.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ Features
2020
Bug Fixes
2121
"""""""""
2222

23-
- Python: stride check too strict #369
23+
- Python:
24+
25+
- stride check too strict #369
26+
- allow one-element n-d arrays for scalars in ``store``, ``make_constant`` #314
2427

2528
Other
2629
"""""
2730

2831
- dependency change: Catch2 2.3.0+
32+
- Python: add extended write example #314
2933

3034

3135
0.6.0-alpha

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,7 @@ set(openPMD_EXAMPLE_NAMES
529529
set(openPMD_PYTHON_EXAMPLE_NAMES
530530
2_read_serial
531531
3_write_serial
532+
7_extended_write_serial
532533
)
533534

534535
if(openPMD_USE_INVASIVE_TESTS)

examples/7_extended_write_serial.cpp

+15-11
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ write()
1717
std::generate(position_global.begin(), position_global.end(), [&pos]{ return pos++; });
1818
std::shared_ptr< double > position_local(new double);
1919
e["position"]["x"].resetDataset(Dataset(determineDatatype(position_local), {4}));
20-
20+
2121
for( uint64_t i = 0; i < 4; ++i )
2222
{
2323
*position_local = position_global[i];
@@ -54,11 +54,13 @@ write2()
5454
f.setMeshesPath("custom_meshes_path");
5555
f.setParticlesPath("long_and_very_custom_particles_path");
5656

57-
// while it is possible to add and remove attributes, it is discouraged
58-
// removing attributes required by the standard typically makes the file unusable for post-processing
57+
// it is possible to add and remove attributes
5958
f.setComment("This is fine and actually encouraged by the standard");
60-
f.setAttribute("custom_attribute_name",
61-
std::string("This attribute is manually added and can contain about any datatype you would want"));
59+
f.setAttribute(
60+
"custom_attribute_name",
61+
std::string("This attribute is manually added and can contain about any datatype you would want")
62+
);
63+
// note that removing attributes required by the standard typically makes the file unusable for post-processing
6264
f.deleteAttribute("custom_attribute_name");
6365

6466
// everything that is accessed with [] should be interpreted as permanent storage
@@ -79,7 +81,7 @@ write2()
7981
reference.setComment("Modifications to a reference will always be visible in the output");
8082

8183
// alternatively, a copy may be created and later re-assigned to f.iterations[1]
82-
Iteration copy = f.iterations[1];
84+
Iteration copy = f.iterations[1]; // TODO .copy()
8385
copy.setComment("Modifications to copies will only take effect after you reassign the copy");
8486
f.iterations[1] = copy;
8587
}
@@ -90,9 +92,10 @@ write2()
9092
// the underlying concept for numeric data is the openPMD Record
9193
// https://github.com/openPMD/openPMD-standard/blob/upcoming-1.0.1/STANDARD.md#scalar-vector-and-tensor-records
9294
// Meshes are specialized records
93-
cur_it.meshes["generic_2D_field"].setUnitDimension({{UnitDimension::L, -3}});
95+
cur_it.meshes["generic_2D_field"].setUnitDimension({{UnitDimension::L, -3}, {UnitDimension::M, 1}});
9496

9597
{
98+
// TODO outdated!
9699
// as this is a copy, it does not modify the sunk resource and can be modified independently
97100
Mesh lowRez = cur_it.meshes["generic_2D_field"];
98101
lowRez.setGridSpacing(std::vector< double >{6, 1}).setGridGlobalOffset({0, 600});
@@ -107,9 +110,9 @@ write2()
107110
cur_it.meshes.erase("highRez_2D_field");
108111

109112
{
110-
// particles handle very similar
111-
ParticleSpecies& electrons = cur_it.particles["electrons"];
112-
electrons.setAttribute("NoteWorthyParticleSpeciesProperty",
113+
// particles are handled very similar
114+
ParticleSpecies& electrons = cur_it.particles["electrons"];
115+
electrons.setAttribute("NoteWorthyParticleSpeciesProperty",
113116
std::string("Observing this species was a blast."));
114117
electrons["displacement"].setUnitDimension({{UnitDimension::M, 1}});
115118
electrons["displacement"]["x"].setUnitSI(1e-6);
@@ -201,7 +204,8 @@ write2()
201204
mesh["y"].resetDataset(d);
202205
mesh["y"].setUnitSI(4);
203206
double constant_value = 0.3183098861837907;
204-
// for datasets that only contain one unique value, openPMD offers constant records
207+
// for datasets that contain a single unique value, openPMD offers
208+
// constant records
205209
mesh["y"].makeConstant(constant_value);
206210

207211
/* The files in 'f' are still open until the object is destroyed, on

examples/7_extended_write_serial.py

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python
2+
"""
3+
This file is part of the openPMD-api.
4+
5+
Copyright 2018 openPMD contributors
6+
Authors: Axel Huebl, Fabian Koller
7+
License: LGPLv3+
8+
"""
9+
from openPMD import Series, Access_Type, Dataset, Mesh_Record_Component, \
10+
Unit_Dimension
11+
import numpy as np
12+
13+
14+
SCALAR = Mesh_Record_Component.SCALAR
15+
16+
17+
if __name__ == "__main__":
18+
# open file for writing
19+
f = Series(
20+
"working/directory/2D_simData_py.h5",
21+
Access_Type.create
22+
)
23+
24+
# all required openPMD attributes will be set to reasonable default values
25+
# (all ones, all zeros, empty strings,...)
26+
# manually setting them enforces the openPMD standard
27+
f.set_meshes_path("custom_meshes_path")
28+
f.set_particles_path("long_and_very_custom_particles_path")
29+
30+
# it is possible to add and remove attributes
31+
f.set_comment("This is fine and actually encouraged by the standard")
32+
f.set_attribute(
33+
"custom_attribute_name",
34+
"This attribute is manually added and can contain about any datatype "
35+
"you would want"
36+
)
37+
# note that removing attributes required by the standard typically makes
38+
# the file unusable for post-processing
39+
f.delete_attribute("custom_attribute_name")
40+
41+
# setting attributes can be chained in JS-like syntax for compact code
42+
tmpItObj = f.iterations[1] \
43+
.set_time(42.0) \
44+
.set_dt(1.0) \
45+
.set_time_unit_SI(1.39e-16)
46+
# everything that is accessed with [] should be interpreted as permanent
47+
# storage the objects sunk into these locations are deep copies
48+
f.iterations[2].set_comment("This iteration will not appear in any output")
49+
del f.iterations[2]
50+
51+
# this is a reference to an iteration
52+
reference = f.iterations[1]
53+
reference.set_comment("Modifications to a reference will always be visible"
54+
" in the output")
55+
del reference
56+
57+
# alternatively, a copy may be created and later re-assigned to
58+
# f.iterations[1]
59+
copy = f.iterations[1] # TODO .copy()
60+
copy.set_comment("Modifications to copies will only take effect after you "
61+
"reassign the copy")
62+
f.iterations[1] = copy
63+
del copy
64+
65+
f.iterations[1].delete_attribute("comment")
66+
67+
cur_it = f.iterations[1]
68+
69+
# the underlying concept for numeric data is the openPMD Record
70+
# https://github.com/openPMD/openPMD-standard/blob/upcoming-1.0.1/STANDARD.md#scalar-vector-and-tensor-records
71+
# Meshes are specialized records
72+
cur_it.meshes["generic_2D_field"].set_unit_dimension(
73+
{Unit_Dimension.L: -3, Unit_Dimension.M: 1})
74+
75+
# as this is a reference, it modifies the original resource
76+
lowRez = cur_it.meshes["generic_2D_field"]
77+
lowRez.set_grid_spacing([1, 1]) \
78+
.set_grid_global_offset([0, 600])
79+
80+
# del cur_it.meshes["generic_2D_field"]
81+
cur_it.meshes["lowRez_2D_field"] = lowRez
82+
del lowRez
83+
84+
# particles are handled very similar
85+
electrons = cur_it.particles["electrons"]
86+
electrons.set_attribute(
87+
"NoteWorthyParticleSpeciesProperty",
88+
"Observing this species was a blast.")
89+
electrons["displacement"].set_unit_dimension({Unit_Dimension.M: 1})
90+
electrons["displacement"]["x"].set_unit_SI(1.e-6)
91+
del electrons["displacement"]
92+
electrons["weighting"][SCALAR].set_unit_SI(1.e-5)
93+
94+
mesh = cur_it.meshes["lowRez_2D_field"]
95+
mesh.set_axis_labels(["x", "y"])
96+
97+
# data is assumed to reside behind a pointer as a contiguous column-major
98+
# array shared data ownership during IO is indicated with a smart pointer
99+
partial_mesh = np.arange(5, dtype=np.double)
100+
101+
# before storing record data, you must specify the dataset once per
102+
# component this describes the datatype and shape of data as it should be
103+
# written to disk
104+
d = Dataset(partial_mesh.dtype, extent=[2, 5])
105+
d.set_compression("zlib", 9)
106+
d.set_custom_transform("blosc:compressor=zlib,shuffle=bit,lvl=1;nometa")
107+
mesh["x"].reset_dataset(d)
108+
109+
electrons = cur_it.particles["electrons"]
110+
111+
mpiDims = [4]
112+
partial_particlePos = np.arange(2, dtype=np.float32)
113+
d = Dataset(partial_particlePos.dtype, extent=mpiDims)
114+
electrons["position"]["x"].reset_dataset(d)
115+
116+
partial_particleOff = np.arange(2, dtype=np.uint)
117+
d = Dataset(partial_particleOff.dtype, mpiDims)
118+
electrons["positionOffset"]["x"].reset_dataset(d)
119+
120+
dset = Dataset(np.dtype("uint64"), extent=[2])
121+
electrons.particle_patches["numParticles"][SCALAR].reset_dataset(dset)
122+
electrons.particle_patches["numParticlesOffset"][SCALAR]. \
123+
reset_dataset(dset)
124+
125+
dset = Dataset(partial_particlePos.dtype, extent=[2])
126+
electrons.particle_patches["offset"].set_unit_dimension(
127+
{Unit_Dimension.L: 1})
128+
electrons.particle_patches["offset"]["x"].reset_dataset(dset)
129+
electrons.particle_patches["extent"].set_unit_dimension(
130+
{Unit_Dimension.L: 1})
131+
electrons.particle_patches["extent"]["x"].reset_dataset(dset)
132+
133+
# at any point in time you may decide to dump already created output to
134+
# disk note that this will make some operations impossible (e.g. renaming
135+
# files)
136+
f.flush()
137+
138+
# chunked writing of the final dataset at a time is supported
139+
# this loop writes one row at a time
140+
mesh_x = np.array([
141+
[1, 3, 5, 7, 9],
142+
[11, 13, 15, 17, 19]
143+
])
144+
particle_position = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32)
145+
particle_position_offset = [0, 1, 2, 3]
146+
for i in [0, 1]:
147+
for col in [0, 1, 2, 3, 4]:
148+
partial_mesh[col] = mesh_x[i, col]
149+
150+
o = [i, 0]
151+
e = [1, 5]
152+
mesh["x"].store_chunk(o, e, partial_mesh)
153+
# operations between store and flush MUST NOT modify the pointed-to
154+
# data
155+
f.flush()
156+
# after the flush completes successfully, access to the shared
157+
# resource is returned to the caller
158+
159+
for idx in [0, 1]:
160+
partial_particlePos[idx] = particle_position[idx + 2*i]
161+
partial_particleOff[idx] = particle_position_offset[idx + 2*i]
162+
163+
numParticlesOffset = 2*i
164+
numParticles = 2
165+
166+
o = [numParticlesOffset]
167+
e = [numParticles]
168+
electrons["position"]["x"].store_chunk(o, e, partial_particlePos)
169+
electrons["positionOffset"]["x"].store_chunk(o, e, partial_particleOff)
170+
171+
electrons.particle_patches["numParticles"][SCALAR].store(
172+
i, np.array([numParticles], dtype=np.uint64))
173+
electrons.particle_patches["numParticlesOffset"][SCALAR].store(
174+
i, np.array([numParticlesOffset], dtype=np.uint64))
175+
176+
electrons.particle_patches["offset"]["x"].store(
177+
i,
178+
np.array([particle_position[numParticlesOffset]],
179+
dtype=np.float32))
180+
electrons.particle_patches["extent"]["x"].store(
181+
i,
182+
np.array([
183+
particle_position[numParticlesOffset + numParticles - 1] -
184+
particle_position[numParticlesOffset]
185+
], dtype=np.float32))
186+
187+
mesh["y"].reset_dataset(d)
188+
mesh["y"].set_unit_SI(4)
189+
constant_value = 0.3183098861837907
190+
# for datasets that contain a single unique value, openPMD offers
191+
# constant records
192+
mesh["y"].make_constant(constant_value)
193+
194+
# The files in 'f' are still open until the object is destroyed, on
195+
# which it cleanly flushes and closes all open file handles.
196+
# One can delete the object explicitly (or let it run out of scope) to
197+
# trigger this.
198+
del f

src/binding/python/PatchRecordComponent.cpp

+8-1
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,21 @@ void init_PatchRecordComponent(py::module &m) {
8383

8484
using DT = Datatype;
8585

86+
// allow one-element n-dimensional buffers as well
87+
py::ssize_t numElements = 1;
88+
if( buf.ndim > 0 ) {
89+
for( auto d = 0; d < buf.ndim; ++d )
90+
numElements *= buf.shape.at(d);
91+
}
92+
8693
// Numpy: Handling of arrays and scalars
8794
// work-around for https://github.com/pybind/pybind11/issues/1224
8895
// -> passing numpy scalars as buffers needs numpy 1.15+
8996
// https://github.com/numpy/numpy/issues/10265
9097
// https://github.com/pybind/pybind11/issues/1224#issuecomment-354357392
9198
// scalars, see PEP 3118
9299
// requires Numpy 1.15+
93-
if( buf.ndim == 0 ) {
100+
if( numElements == 1 ) {
94101
// refs:
95102
// https://docs.scipy.org/doc/numpy-1.15.0/reference/arrays.interface.html
96103
// https://docs.python.org/3/library/struct.html#format-characters

src/binding/python/RecordComponent.cpp

+8-1
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,21 @@ void init_RecordComponent(py::module &m) {
5858

5959
using DT = Datatype;
6060

61+
// allow one-element n-dimensional buffers as well
62+
py::ssize_t numElements = 1;
63+
if( buf.ndim > 0 ) {
64+
for( auto d = 0; d < buf.ndim; ++d )
65+
numElements *= buf.shape.at(d);
66+
}
67+
6168
// Numpy: Handling of arrays and scalars
6269
// work-around for https://github.com/pybind/pybind11/issues/1224
6370
// -> passing numpy scalars as buffers needs numpy 1.15+
6471
// https://github.com/numpy/numpy/issues/10265
6572
// https://github.com/pybind/pybind11/issues/1224#issuecomment-354357392
6673
// scalars, see PEP 3118
6774
// requires Numpy 1.15+
68-
if( buf.ndim == 0 ) {
75+
if( numElements == 1 ) {
6976
// refs:
7077
// https://docs.scipy.org/doc/numpy-1.15.0/reference/arrays.interface.html
7178
// https://docs.python.org/3/library/struct.html#format-characters

0 commit comments

Comments
 (0)