Skip to content

Commit ce51f2a

Browse files
committed
Expose this to the sliced Python API
1 parent 36b116f commit ce51f2a

File tree

3 files changed

+119
-10
lines changed

3 files changed

+119
-10
lines changed

docs/source/usage/workflow.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Storing and reading chunks
2525
The chunk is then stored by specifying an empty offset vector ``{}``.
2626
The chunk extent vector must be equivalent to the global extent in all non-joined dimensions (i.e. joined arrays allow no further sub-chunking other than concatenation along the joined dimension).
2727
The joined dimension of the extent vector specifies the extent that this piece should have along the joined dimension.
28+
In the Python API, the slice-based setter syntax can be used as an abbreviation since the necessary information is determined from the passed array, e.g. ``record_component[()] = local_data``.
2829
The global extent of the dataset along the joined dimension will then be the sum of all local chunk extents along the joined dimension.
2930

3031
Since openPMD follows a struct-of-array layout of data, it is important not to lose correlation of data between components. E.g., joining an array must take care that ``particles/e/position/x`` and ``particles/e/position/y`` are joined in uniform way.

examples/5_write_parallel.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
version.parse(adios2.__version__) >= version.parse('2.9.0')
2222
except ImportError:
2323
USE_JOINED_DIMENSION = False
24+
2425
if __name__ == "__main__":
2526
# also works with any other MPI communicator
2627
comm = MPI.COMM_WORLD
2728

2829
# global data set to write: [MPI_Size * 10, 300]
2930
# each rank writes a 10x300 slice with its MPI rank as values
30-
local_value = comm.size
31+
local_value = comm.rank
3132
local_data = np.ones(10 * 300,
3233
dtype=np.double).reshape(10, 300) * local_value
3334
if 0 == comm.rank:
@@ -77,7 +78,11 @@
7778
# example shows a 1D domain decomposition in first index
7879

7980
if USE_JOINED_DIMENSION:
80-
mymesh.store_chunk(local_data, [], [10, 300])
81+
# explicit API
82+
# mymesh.store_chunk(local_data, [], [10, 300])
83+
mymesh[:, :] = local_data
84+
# or short:
85+
# mymesh[()] = local_data
8186
else:
8287
mymesh[comm.rank*10:(comm.rank+1)*10, :] = local_data
8388
if 0 == comm.rank:

src/binding/python/RecordComponent.cpp

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
* and the GNU Lesser General Public License along with openPMD-api.
1919
* If not, see <http://www.gnu.org/licenses/>.
2020
*/
21+
#include <limits>
22+
#include <pybind11/detail/common.h>
2123
#include <pybind11/numpy.h>
2224
#include <pybind11/pybind11.h>
2325
#include <pybind11/stl.h>
2426

2527
#include "openPMD/DatatypeHelpers.hpp"
2628
#include "openPMD/Error.hpp"
29+
#include "openPMD/RecordComponent.hpp"
2730
#include "openPMD/Series.hpp"
2831
#include "openPMD/backend/BaseRecordComponent.hpp"
2932

@@ -40,6 +43,7 @@
4043
#include <exception>
4144
#include <iostream>
4245
#include <sstream>
46+
#include <stdexcept>
4347
#include <string>
4448
#include <tuple>
4549
#include <type_traits>
@@ -111,14 +115,48 @@ inline std::tuple<Offset, Extent, std::vector<bool>> parseTupleSlices(
111115
py::slice slice = py::cast<py::slice>(slices[i]);
112116

113117
size_t start, stop, step, slicelength;
118+
auto mocked_extent = full_extent.at(curAxis);
119+
// py::ssize_t is a signed type, so we will need to use another
120+
// magic number for JOINED_DIMENSION in this computation, since the
121+
// C++ API's JOINED_DIMENSION would be interpreted as a negative
122+
// index
123+
bool undo_mocked_extent = false;
124+
constexpr auto PYTHON_JOINED_DIMENSION =
125+
std::numeric_limits<py::ssize_t>::max() - 1;
126+
if (mocked_extent == Dataset::JOINED_DIMENSION)
127+
{
128+
undo_mocked_extent = true;
129+
mocked_extent = PYTHON_JOINED_DIMENSION;
130+
}
114131
if (!slice.compute(
115-
full_extent.at(curAxis),
116-
&start,
117-
&stop,
118-
&step,
119-
&slicelength))
132+
mocked_extent, &start, &stop, &step, &slicelength))
120133
throw py::error_already_set();
121134

135+
if (undo_mocked_extent)
136+
{
137+
// do the same calculation again, but with another global extent
138+
// (that is not smaller than the previous in order to avoid
139+
// cutting off the range)
140+
// this is to avoid the unlikely case
141+
// that the mocked alternative value is actually the intended
142+
// one
143+
size_t start2, stop2, step2, slicelength2;
144+
if (!slice.compute(
145+
mocked_extent + 1,
146+
&start2,
147+
&stop2,
148+
&step2,
149+
&slicelength2))
150+
throw py::error_already_set();
151+
if (slicelength == slicelength2)
152+
{
153+
// slicelength was given as an absolute value and
154+
// accidentally hit our mocked value
155+
// --> keep that value
156+
undo_mocked_extent = false;
157+
}
158+
}
159+
122160
// TODO PySlice_AdjustIndices: Python 3.6.1+
123161
// Adjust start/end slice indices assuming a sequence of the
124162
// specified length. Out of bounds indices are clipped in a
@@ -132,7 +170,10 @@ inline std::tuple<Offset, Extent, std::vector<bool>> parseTupleSlices(
132170

133171
// verified for size later in C++ API
134172
offset.at(curAxis) = start;
135-
extent.at(curAxis) = slicelength; // stop - start;
173+
extent.at(curAxis) =
174+
undo_mocked_extent && slicelength == PYTHON_JOINED_DIMENSION
175+
? Dataset::JOINED_DIMENSION
176+
: slicelength; // stop - start;
136177

137178
continue;
138179
}
@@ -187,6 +228,59 @@ inline std::tuple<Offset, Extent, std::vector<bool>> parseTupleSlices(
187228
return std::make_tuple(offset, extent, flatten);
188229
}
189230

231+
inline std::tuple<Offset, Extent, std::vector<bool>> parseJoinedTupleSlices(
232+
uint8_t const ndim,
233+
Extent const &full_extent,
234+
py::tuple const &slices,
235+
size_t joined_dim,
236+
py::array const &a)
237+
{
238+
239+
std::vector<bool> flatten;
240+
Offset offset;
241+
Extent extent;
242+
std::tie(offset, extent, flatten) =
243+
parseTupleSlices(ndim, full_extent, slices);
244+
for (size_t i = 0; i < ndim; ++i)
245+
{
246+
if (offset.at(i) != 0)
247+
{
248+
throw std::runtime_error(
249+
"Joined array: Cannot use non-zero offset in store_chunk "
250+
"(offset[" +
251+
std::to_string(i) + "] = " + std::to_string(offset[i]) + ").");
252+
}
253+
if (flatten.at(i))
254+
{
255+
throw std::runtime_error(
256+
"Flattened slices unimplemented for joined arrays.");
257+
}
258+
259+
if (i == joined_dim)
260+
{
261+
if (extent.at(i) == 0 || extent.at(i) == Dataset::JOINED_DIMENSION)
262+
{
263+
extent[i] = a.shape()[i];
264+
}
265+
}
266+
else
267+
{
268+
if (extent.at(i) != full_extent.at(i))
269+
{
270+
throw std::runtime_error(
271+
"Joined array: Must use full extent in store_chunk for "
272+
"non-joined dimension "
273+
"(local_extent[" +
274+
std::to_string(i) + "] = " + std::to_string(extent[i]) +
275+
" != global_extent[" + std::to_string(i) +
276+
"] = " + std::to_string(full_extent[i]) + ").");
277+
}
278+
}
279+
}
280+
offset.clear();
281+
return std::make_tuple(offset, extent, flatten);
282+
}
283+
190284
/** Check an array is a contiguous buffer
191285
*
192286
* Required are contiguous buffers for store and load
@@ -388,8 +482,17 @@ store_chunk(RecordComponent &r, py::array &a, py::tuple const &slices)
388482
Offset offset;
389483
Extent extent;
390484
std::vector<bool> flatten;
391-
std::tie(offset, extent, flatten) =
392-
parseTupleSlices(ndim, full_extent, slices);
485+
if (auto joined_dimension = r.joinedDimension();
486+
joined_dimension.has_value())
487+
{
488+
std::tie(offset, extent, flatten) = parseJoinedTupleSlices(
489+
ndim, full_extent, slices, *joined_dimension, a);
490+
}
491+
else
492+
{
493+
std::tie(offset, extent, flatten) =
494+
parseTupleSlices(ndim, full_extent, slices);
495+
}
393496

394497
store_chunk(r, a, offset, extent, flatten);
395498
}

0 commit comments

Comments
 (0)