diff --git a/.github/workflows/build_wheels.yaml b/.github/workflows/build_wheels.yaml index 4fbe2ddb..b847e5ca 100644 --- a/.github/workflows/build_wheels.yaml +++ b/.github/workflows/build_wheels.yaml @@ -38,11 +38,11 @@ jobs: python: - version: 'cp310' - oldest_numpy: '1.21.6' + oldest_numpy: '2.0.0' - version: 'cp311' - oldest_numpy: '1.23.2' + oldest_numpy: '2.0.0' - version: 'cp312' - oldest_numpy: '1.26.2' + oldest_numpy: '2.0.0' - version: 'cp313' oldest_numpy: '2.1.1' @@ -53,7 +53,7 @@ jobs: uses: pypa/cibuildwheel@d4a2945fcc8d13f20a1b99d461b8e844d5fc6e23 # v2.21.1 env: CIBW_BUILD: "${{ matrix.python.version }}-*" - CIBW_TEST_REQUIRES: pytest==8.2.1 numpy==${{ matrix.python.oldest_numpy }} + CIBW_TEST_REQUIRES: pytest==8.3.3 numpy==${{ matrix.python.oldest_numpy }} - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: diff --git a/.github/workflows/unit_test.yaml b/.github/workflows/unit_test.yaml index f0d0d43a..e9b8da77 100644 --- a/.github/workflows/unit_test.yaml +++ b/.github/workflows/unit_test.yaml @@ -30,7 +30,7 @@ jobs: - os: windows-2019 python: '3.12' - os: windows-2022 - python: '3.13.0-rc.1' + python: '3.13' ############## # Mac # macos-x86_64 @@ -38,7 +38,7 @@ jobs: python: '3.12' # macos-arm64 - os: macos-14 - python: '3.13.0-rc.1' + python: '3.13' ############## # Ubuntu 24.04 - os: ubuntu-24.04 @@ -58,7 +58,7 @@ jobs: c_compiler: clang-18 cxx_compiler: clang++-18 - os: ubuntu-24.04 - python: '3.13.0-rc.1' + python: '3.13.0' c_compiler: clang-18 cxx_compiler: clang++-18 ############## diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d0e30b4..e0a0636e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,19 @@ Change Log 3.x --- +3.4.0 (not yet released) +^^^^^^^^^^^^^^^^^^^^^^^^ + +*Added:* + +* New chunk type for string data - valid in file layer versions 2.1 and later + (`#391 `__). + +*Changed:* + +* Require NumPy >= 2.0 + (`#391 `__). + 3.3.2 (2024-09-06) ^^^^^^^^^^^^^^^^^^ diff --git a/INSTALLING.rst b/INSTALLING.rst index 712ac972..af2f9472 100644 --- a/INSTALLING.rst +++ b/INSTALLING.rst @@ -109,7 +109,7 @@ Install prerequisites * **C compiler** (tested with gcc 10-14, clang 10-18, Visual Studio 2019-2022) * **Python** >= 3.10 -* **numpy** >= 1.19.0 +* **numpy** >= 2.0.0 * **Cython** >= 0.22 **To execute unit tests:** diff --git a/doc/credits.rst b/doc/credits.rst index 2d7c5237..18f9f19a 100644 --- a/doc/credits.rst +++ b/doc/credits.rst @@ -18,3 +18,4 @@ The following people contributed to GSD. * Alexander Stukowski, OVITO GmbH * Charlotte Shiqi Zhao, University of Michigan * Tim Moore, University of Michigan +* Joseph Burkhart, University of Michigan diff --git a/doc/file-layer.rst b/doc/file-layer.rst index 78431216..a79fb1dc 100644 --- a/doc/file-layer.rst +++ b/doc/file-layer.rst @@ -6,7 +6,7 @@ File layer .. highlight:: c -**Version: 2.0** +**Version: 2.x** General simulation data (GSD) **file layer** design and rationale. These use cases and design specifications define the low level GSD file format. @@ -128,7 +128,7 @@ There are four types of data blocks in a GSD file. * List of string names used by index entries. * v1.0 files: Each name is a 64-byte character string. - * v2.0 files: Names may have any length and are separated by 0 terminators. + * v2.x files: Names may have any length and are separated by 0 terminators. * The first name that starts with the 0 byte marks the end of the list * The header stores the total size of the name list block. @@ -215,13 +215,13 @@ non-standard packing attributes or pragmas to enforce this. In v1.0 files, the frame index must monotonically increase from one index entry to the next. The GSD API ensures this. -In v2.0 files, the entire index block is stored sorted first by frame, then +In v2.x files, the entire index block is stored sorted first by frame, then by *id*. Namelist block ^^^^^^^^^^^^^^ -In v2.0 files, the namelist block stores a list of strings separated by 0 +In v2.x files, the namelist block stores a list of strings separated by 0 terminators. In v1.0 files, the namelist block stores a list of 0-terminated strings in @@ -235,3 +235,9 @@ Data block A data block stores raw data bytes on the disk. For a given index entry ``entry``, the data starts at location ``entry.location`` and is the next ``entry.N * entry.M * gsd_sizeof_type(entry.type)`` bytes. + +Added in version 2.1 +-------------------- + +* The ``GSD_CHARACTER`` chunk type represents a UTF-8 string (null termination is allowed, but not + required). diff --git a/doc/fl-examples.rst b/doc/fl-examples.rst index d66a47e3..e61b84c3 100644 --- a/doc/fl-examples.rst +++ b/doc/fl-examples.rst @@ -198,21 +198,15 @@ Store string chunks application="My application", schema="My Schema", schema_version=[1,0]) - f.mode - s = "This is a string" - b = numpy.array([s], dtype=numpy.dtype((bytes, len(s)+1))) - b = b.view(dtype=numpy.int8) - b - f.write_chunk(name='string', data=b) + f.write_chunk(name='string', data="This is a string") f.end_frame() r = f.read_chunk(frame=0, name='string') r - r = r.view(dtype=numpy.dtype((bytes, r.shape[0]))); - r[0].decode('UTF-8') f.close() -To store a string in a gsd file, convert it to a numpy array of bytes and store -that data in the file. Decode the byte sequence to get back a string. +Staring with GSD 3.4.0, the file layer can natively store strings in the file. +In previous versions, you need to convert strings to a numpy array of bytes and store +that data in the file. Truncate ^^^^^^^^ diff --git a/gsd/fl.pyx b/gsd/fl.pyx index 03849449..bc65046d 100644 --- a/gsd/fl.pyx +++ b/gsd/fl.pyx @@ -557,59 +557,78 @@ cdef class GSDFile: if not self.__is_open: raise ValueError("File is not open") - data_array = numpy.ascontiguousarray(data) - if data_array is not data: - logger.warning('implicit data copy when writing chunk: ' + name) - data_array = data_array.view() - cdef uint64_t N cdef uint32_t M - if len(data_array.shape) > 2: - raise ValueError("GSD can only write 1 or 2 dimensional arrays: " - + name) + cdef libgsd.gsd_type gsd_type + cdef void *data_ptr - if len(data_array.shape) == 1: - data_array = data_array.reshape([data_array.shape[0], 1]) + # Special behavior for handling strings + if type(data) is str: + bytes_array = numpy.array([data], dtype=numpy.dtype((bytes, len(data)))) + bytes_view = bytes_array.view(dtype=numpy.int8).reshape((len(data),1)) - N = data_array.shape[0] - M = data_array.shape[1] + N = len(data) + M = 1 - cdef libgsd.gsd_type gsd_type - cdef void *data_ptr - if data_array.dtype == numpy.uint8: - gsd_type = libgsd.GSD_TYPE_UINT8 - data_ptr = __get_ptr_uint8(data_array) - elif data_array.dtype == numpy.uint16: - gsd_type = libgsd.GSD_TYPE_UINT16 - data_ptr = __get_ptr_uint16(data_array) - elif data_array.dtype == numpy.uint32: - gsd_type = libgsd.GSD_TYPE_UINT32 - data_ptr = __get_ptr_uint32(data_array) - elif data_array.dtype == numpy.uint64: - gsd_type = libgsd.GSD_TYPE_UINT64 - data_ptr = __get_ptr_uint64(data_array) - elif data_array.dtype == numpy.int8: - gsd_type = libgsd.GSD_TYPE_INT8 - data_ptr = __get_ptr_int8(data_array) - elif data_array.dtype == numpy.int16: - gsd_type = libgsd.GSD_TYPE_INT16 - data_ptr = __get_ptr_int16(data_array) - elif data_array.dtype == numpy.int32: - gsd_type = libgsd.GSD_TYPE_INT32 - data_ptr = __get_ptr_int32(data_array) - elif data_array.dtype == numpy.int64: - gsd_type = libgsd.GSD_TYPE_INT64 - data_ptr = __get_ptr_int64(data_array) - elif data_array.dtype == numpy.float32: - gsd_type = libgsd.GSD_TYPE_FLOAT - data_ptr = __get_ptr_float32(data_array) - elif data_array.dtype == numpy.float64: - gsd_type = libgsd.GSD_TYPE_DOUBLE - data_ptr = __get_ptr_float64(data_array) + gsd_type = libgsd.GSD_TYPE_CHARACTER + data_ptr = __get_ptr_int8(bytes_view) + + # Non-string behavior else: - raise ValueError("invalid type for chunk: " + name) + data_array = numpy.ascontiguousarray(data) + + if data_array is not data: + logger.warning('implicit data copy when writing chunk: ' + name) + data_array = data_array.view() + + + if len(data_array.shape) > 2: + raise ValueError("GSD can only write 1 or 2 dimensional arrays: " + + name) + + if len(data_array.shape) == 1: + data_array = data_array.reshape([data_array.shape[0], 1]) + + N = data_array.shape[0] + M = data_array.shape[1] + + if data_array.dtype == numpy.uint8: + gsd_type = libgsd.GSD_TYPE_UINT8 + data_ptr = __get_ptr_uint8(data_array) + elif data_array.dtype == numpy.uint16: + gsd_type = libgsd.GSD_TYPE_UINT16 + data_ptr = __get_ptr_uint16(data_array) + elif data_array.dtype == numpy.uint32: + gsd_type = libgsd.GSD_TYPE_UINT32 + data_ptr = __get_ptr_uint32(data_array) + elif data_array.dtype == numpy.uint64: + gsd_type = libgsd.GSD_TYPE_UINT64 + data_ptr = __get_ptr_uint64(data_array) + elif data_array.dtype == numpy.int8: + gsd_type = libgsd.GSD_TYPE_INT8 + data_ptr = __get_ptr_int8(data_array) + elif data_array.dtype == numpy.int16: + gsd_type = libgsd.GSD_TYPE_INT16 + data_ptr = __get_ptr_int16(data_array) + elif data_array.dtype == numpy.int32: + gsd_type = libgsd.GSD_TYPE_INT32 + data_ptr = __get_ptr_int32(data_array) + elif data_array.dtype == numpy.int64: + gsd_type = libgsd.GSD_TYPE_INT64 + data_ptr = __get_ptr_int64(data_array) + elif data_array.dtype == numpy.float32: + gsd_type = libgsd.GSD_TYPE_FLOAT + data_ptr = __get_ptr_float32(data_array) + elif data_array.dtype == numpy.float64: + gsd_type = libgsd.GSD_TYPE_DOUBLE + data_ptr = __get_ptr_float64(data_array) + else: + raise ValueError("invalid type for chunk: " + name) + + # Once we have the data pointer, the behavior should be identical + # for all data types logger.debug('write chunk: ' + self.name + ' - ' + name) cdef char * c_name @@ -787,6 +806,9 @@ cdef class GSDFile: elif gsd_type == libgsd.GSD_TYPE_DOUBLE: data_array = numpy.empty(dtype=numpy.float64, shape=[index_entry.N, index_entry.M]) + elif gsd_type == libgsd.GSD_TYPE_CHARACTER: + data_array = numpy.empty(dtype=numpy.int8, + shape=[index_entry.M, index_entry.N]) else: raise ValueError("invalid type for chunk: " + name) @@ -815,6 +837,8 @@ cdef class GSDFile: data_ptr = __get_ptr_float32(data_array) elif gsd_type == libgsd.GSD_TYPE_DOUBLE: data_ptr = __get_ptr_float64(data_array) + elif gsd_type == libgsd.GSD_TYPE_CHARACTER: + data_ptr = __get_ptr_int8(data_array) else: raise ValueError("invalid type for chunk: " + name) @@ -826,6 +850,11 @@ cdef class GSDFile: __raise_on_error(retval, self.name) if index_entry.M == 1: + if gsd_type == libgsd.GSD_TYPE_CHARACTER: + data_array = data_array.flatten() + bytes_array = data_array.view(dtype=numpy.dtype((bytes, data_array.shape[0]))) + return bytes_array[0].decode("UTF-8") + return data_array.reshape([index_entry.N]) else: return data_array diff --git a/gsd/gsd.c b/gsd/gsd.c index 2e8d7667..07997408 100644 --- a/gsd/gsd.c +++ b/gsd/gsd.c @@ -86,7 +86,12 @@ enum /// Current GSD file specification enum { - GSD_CURRENT_FILE_VERSION = 2 + GSD_CURRENT_FILE_VERSION_MAJOR = 2 + }; + +enum + { + GSD_CURRENT_FILE_VERSION_MINOR = 1 }; // define windows wrapper functions @@ -1384,7 +1389,8 @@ gsd_initialize_file(int fd, const char* application, const char* schema, uint32_ gsd_util_zero_memory(&header, sizeof(header)); header.magic = GSD_MAGIC_ID; - header.gsd_version = gsd_make_version(GSD_CURRENT_FILE_VERSION, 0); + header.gsd_version + = gsd_make_version(GSD_CURRENT_FILE_VERSION_MAJOR, GSD_CURRENT_FILE_VERSION_MINOR); strncpy(header.application, application, sizeof(header.application) - 1); header.application[sizeof(header.application) - 1] = 0; strncpy(header.schema, schema, sizeof(header.schema) - 1); @@ -1607,6 +1613,24 @@ inline static int gsd_initialize_handle(struct gsd_handle* handle) handle->maximum_write_buffer_size = GSD_DEFAULT_MAXIMUM_WRITE_BUFFER_SIZE; handle->index_entries_to_buffer = GSD_DEFAULT_INDEX_ENTRIES_TO_BUFFER; + // Silently upgrade writable files from a previous matching major version to the latest + // minor version. + if ((handle->open_flags == GSD_OPEN_READWRITE || handle->open_flags == GSD_OPEN_APPEND) + && (handle->header.gsd_version + != gsd_make_version(GSD_CURRENT_FILE_VERSION_MAJOR, GSD_CURRENT_FILE_VERSION_MINOR)) + && (handle->header.gsd_version >> (sizeof(uint32_t) * 4) == GSD_CURRENT_FILE_VERSION_MAJOR)) + { + handle->header.gsd_version + = gsd_make_version(GSD_CURRENT_FILE_VERSION_MAJOR, GSD_CURRENT_FILE_VERSION_MINOR); + size_t bytes_written + = gsd_io_pwrite_retry(handle->fd, &(handle->header), sizeof(struct gsd_header), 0); + + if (bytes_written != sizeof(struct gsd_header)) + { + return GSD_ERROR_IO; + } + } + return GSD_SUCCESS; } @@ -2342,6 +2366,10 @@ size_t gsd_sizeof_type(enum gsd_type type) { val = sizeof(double); } + else if (type == GSD_TYPE_CHARACTER) + { + val = sizeof(char); + } else { return 0; @@ -2554,8 +2582,9 @@ int gsd_upgrade(struct gsd_handle* handle) } } - // label the file as a v2.0 file - handle->header.gsd_version = gsd_make_version(GSD_CURRENT_FILE_VERSION, 0); + // GSD always writes files matching the current major and minor version. + handle->header.gsd_version + = gsd_make_version(GSD_CURRENT_FILE_VERSION_MAJOR, GSD_CURRENT_FILE_VERSION_MINOR); // write the new header out ssize_t bytes_written diff --git a/gsd/gsd.h b/gsd/gsd.h index 194b3750..388e4201 100644 --- a/gsd/gsd.h +++ b/gsd/gsd.h @@ -48,7 +48,10 @@ extern "C" GSD_TYPE_FLOAT, /// 64-bit floating point number. - GSD_TYPE_DOUBLE + GSD_TYPE_DOUBLE, + + /// 8-bit character. + GSD_TYPE_CHARACTER }; /// Flag for GSD file open options diff --git a/gsd/hoomd.py b/gsd/hoomd.py index 7af28a29..3934a8f3 100644 --- a/gsd/hoomd.py +++ b/gsd/hoomd.py @@ -1195,12 +1195,17 @@ def read_log(name, scalar_only=False): tmp = numpy.array([0], dtype=numpy.uint64) else: tmp = gsdfileobj.read_chunk(frame=0, name=log) + # if chunk contains string, put it in the numpy array + if isinstance(tmp, str): + tmp = numpy.array([tmp], dtype=numpy.dtypes.StringDType) if scalar_only and not tmp.shape[0] == 1: continue if tmp.shape[0] == 1: logged_data_dict[log] = numpy.full( - fill_value=tmp[0], shape=(gsdfileobj.nframes,) + fill_value=tmp[0], + shape=(gsdfileobj.nframes,), + dtype=tmp.dtype, ) else: logged_data_dict[log] = numpy.tile( @@ -1212,7 +1217,12 @@ def read_log(name, scalar_only=False): if not gsdfileobj.chunk_exists(frame=idx, name=key): continue data = gsdfileobj.read_chunk(frame=idx, name=key) - if len(logged_data_dict[key][idx].shape) == 0: + if ( + not isinstance( + logged_data_dict[key].dtype, numpy.dtypes.StringDType + ) + and len(logged_data_dict[key][idx].shape) == 0 + ): logged_data_dict[key][idx] = data[0] else: logged_data_dict[key][idx] = data diff --git a/gsd/libgsd.pxd b/gsd/libgsd.pxd index e8bb79d8..1d42d078 100644 --- a/gsd/libgsd.pxd +++ b/gsd/libgsd.pxd @@ -16,6 +16,7 @@ cdef extern from "gsd.h" nogil: GSD_TYPE_INT64 GSD_TYPE_FLOAT GSD_TYPE_DOUBLE + GSD_TYPE_CHARACTER cdef enum gsd_open_flag: GSD_OPEN_READWRITE=1 diff --git a/gsd/pygsd.py b/gsd/pygsd.py index 5abd8b5f..9e1adfec 100644 --- a/gsd/pygsd.py +++ b/gsd/pygsd.py @@ -53,16 +53,17 @@ gsd_index_entry_struct = struct.Struct('QQqIHBB') gsd_type_mapping = { - 1: numpy.dtype('uint8'), - 2: numpy.dtype('uint16'), - 3: numpy.dtype('uint32'), - 4: numpy.dtype('uint64'), - 5: numpy.dtype('int8'), - 6: numpy.dtype('int16'), - 7: numpy.dtype('int32'), - 8: numpy.dtype('int64'), - 9: numpy.dtype('float32'), - 10: numpy.dtype('float64'), + 1: ('uint8', numpy.dtype('uint8')), + 2: ('uint16', numpy.dtype('uint16')), + 3: ('uint32', numpy.dtype('uint32')), + 4: ('uint64', numpy.dtype('uint64')), + 5: ('int8', numpy.dtype('int8')), + 6: ('int16', numpy.dtype('int16')), + 7: ('int32', numpy.dtype('int32')), + 8: ('int64', numpy.dtype('int64')), + 9: ('float32', numpy.dtype('float32')), + 10: ('float64', numpy.dtype('float64')), + 11: ('str', numpy.dtype('int8')), } @@ -333,7 +334,7 @@ def read_chunk(self, frame, name): 'read chunk: ' + str(self.__file) + ' - ' + str(frame) + ' - ' + name ) - size = chunk.N * chunk.M * gsd_type_mapping[chunk.type].itemsize + size = chunk.N * chunk.M * gsd_type_mapping[chunk.type][1].itemsize if chunk.location == 0: raise RuntimeError( 'Corrupt chunk: ' @@ -345,7 +346,7 @@ def read_chunk(self, frame, name): ) if size == 0: - return numpy.array([], dtype=gsd_type_mapping[chunk.type]) + return numpy.array([], dtype=gsd_type_mapping[chunk.type][1]) self.__file.seek(chunk.location, 0) data_raw = self.__file.read(size) @@ -353,7 +354,11 @@ def read_chunk(self, frame, name): if len(data_raw) != size: raise OSError - data_npy = numpy.frombuffer(data_raw, dtype=gsd_type_mapping[chunk.type]) + # If gsd type is character, decode it here + if gsd_type_mapping[chunk.type][0] == 'str': + data_npy = data_raw.decode('utf-8') + else: + data_npy = numpy.frombuffer(data_raw, dtype=gsd_type_mapping[chunk.type][1]) if chunk.M == 1: return data_npy diff --git a/gsd/test/test_fl.py b/gsd/test/test_fl.py index 57a18484..fe92c853 100644 --- a/gsd/test/test_fl.py +++ b/gsd/test/test_fl.py @@ -17,6 +17,7 @@ import gsd.pygsd test_path = pathlib.Path(os.path.realpath(__file__)).parent +current_gsd_version = (2, 1) def test_create(tmp_path, open_mode): @@ -45,8 +46,8 @@ def test_create(tmp_path, open_mode): numpy.float64, ], ) -def test_dtype(tmp_path, typ): - """Test all supported data types.""" +def test_nonstring_dtypes(tmp_path, typ): + """Test all supported data types except for strings.""" data1d = numpy.array([1, 2, 3, 4, 5, 127], dtype=typ) data2d = numpy.array([[10, 20], [30, 40], [50, 80]], dtype=typ) data_zero = numpy.array([], dtype=typ) @@ -82,11 +83,11 @@ def test_dtype(tmp_path, typ): read_data2d = f.read_chunk(frame=0, name='data2d') read_data_zero = f.read_chunk(frame=0, name='data_zero') - assert data1d.dtype == read_data1d.dtype + assert data1d.dtype.type == read_data1d.dtype.type numpy.testing.assert_array_equal(data1d, read_data1d) - assert data2d.dtype == read_data2d.dtype + assert data2d.dtype.type == read_data2d.dtype.type numpy.testing.assert_array_equal(data2d, read_data2d) - assert data_zero.dtype == read_data_zero.dtype + assert data_zero.dtype.type == read_data_zero.dtype.type assert data_zero.shape == (0,) # test again with pygsd @@ -94,12 +95,55 @@ def test_dtype(tmp_path, typ): read_data1d = f.read_chunk(frame=0, name='data1d') read_data2d = f.read_chunk(frame=0, name='data2d') - assert data1d.dtype == read_data1d.dtype + assert data1d.dtype.type == read_data1d.dtype.type numpy.testing.assert_array_equal(data1d, read_data1d) - assert data2d.dtype == read_data2d.dtype + assert data2d.dtype.type == read_data2d.dtype.type numpy.testing.assert_array_equal(data2d, read_data2d) +def test_string_dtype(tmp_path): + """Test string datatype. + + Note that the string datatype does not support 0-D or 2-D data. + """ + data1d = 'test' + + gsd.fl.open( + mode='x', + name=tmp_path / 'test_dtype.gsd', + application='test_dtype', + schema='none', + schema_version=[1, 2], + ) + + with gsd.fl.open( + name=tmp_path / 'test_dtype.gsd', + mode='w', + application='test_dtype', + schema='none', + schema_version=[1, 2], + ) as f: + f.write_chunk(name='data1d', data=data1d) + f.end_frame() + + with gsd.fl.open( + name=tmp_path / 'test_dtype.gsd', + mode='r', + application='test_dtype', + schema='none', + schema_version=[1, 2], + ) as f: + read_data1d = f.read_chunk(frame=0, name='data1d') + + numpy.testing.assert_string_equal(data1d, read_data1d) + + # test again with pygsd + with gsd.pygsd.GSDFile(file=open(str(tmp_path / 'test_dtype.gsd'), mode='rb')) as f: + read_data1d = f.read_chunk(frame=0, name='data1d') + + numpy.testing.assert_string_equal(data1d, read_data1d) + + def test_metadata(tmp_path, open_mode): """Test file metadata.""" data = numpy.array([1, 2, 3, 4, 5, 10012], dtype=numpy.int64) @@ -129,7 +173,7 @@ def test_metadata(tmp_path, open_mode): assert f.schema == 'none' assert f.schema_version == (1, 2) assert f.nframes == 150 - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version # test again with pygsd with gsd.pygsd.GSDFile( @@ -141,7 +185,7 @@ def test_metadata(tmp_path, open_mode): assert f.schema == 'none' assert f.schema_version == (1, 2) assert f.nframes == 150 - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version def test_append(tmp_path, open_mode): @@ -772,14 +816,14 @@ def check_v1_file_read(f): schema='none', schema_version=[1, 2], ) as f: - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version check_v1_file_read(f) with gsd.pygsd.GSDFile( file=open(str(tmp_path / 'test_gsd_v1.gsd'), mode='rb') ) as f: - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version check_v1_file_read(f) @@ -912,7 +956,7 @@ def check_v1_file_read(f): f.upgrade() - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version for value in values: if isinstance(value, int): @@ -932,7 +976,7 @@ def check_v1_file_read(f): schema='none', schema_version=[1, 2], ) as f: - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version check_v1_file_read(f) @@ -940,7 +984,7 @@ def check_v1_file_read(f): with gsd.pygsd.GSDFile( file=open(str(tmp_path / 'test_gsd_v1.gsd'), mode='rb') ) as f: - assert f.gsd_version == (2, 0) + assert f.gsd_version == current_gsd_version check_v1_file_read(f) diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index 169180bb..3b813c17 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -794,11 +794,13 @@ def test_log(tmp_path, open_mode): frame0.log['particles/pair_lj_energy'] = [0, -5, -8, -3] frame0.log['value/potential_energy'] = [10] frame0.log['value/pressure'] = [-3] + frame0.log['category'] = 'A' frame1 = gsd.hoomd.Frame() frame1.log['particles/pair_lj_energy'] = [1, 2, -4, -10] frame1.log['value/pressure'] = [5] + frame1.log['category'] = 'BBB' with gsd.hoomd.open(name=tmp_path / 'test_log.gsd', mode=open_mode.write) as hf: hf.extend([frame0, frame1]) @@ -819,6 +821,7 @@ def test_log(tmp_path, open_mode): numpy.testing.assert_array_equal( s.log['value/pressure'], frame0.log['value/pressure'] ) + assert s.log['category'] == frame0.log['category'] s = hf[1] @@ -837,6 +840,7 @@ def test_log(tmp_path, open_mode): numpy.testing.assert_array_equal( s.log['value/pressure'], frame1.log['value/pressure'] ) + assert s.log['category'] == frame1.log['category'] def test_pickle(tmp_path): @@ -877,6 +881,7 @@ def test_read_log(tmp_path): ] frame0.log['value/potential_energy'] = [10] frame0.log['value/pressure'] = [-3] + frame0.log['category'] = 'A' frame1 = gsd.hoomd.Frame() frame1.configuration.step = 1 @@ -888,6 +893,7 @@ def test_read_log(tmp_path): (4, 4, 4), ] frame1.log['value/pressure'] = [5] + frame1.log['category'] = 'BBB' with gsd.hoomd.open(name=tmp_path / 'test_log.gsd', mode='w') as hf: hf.extend([frame0, frame1]) @@ -897,13 +903,14 @@ def test_read_log(tmp_path): name=tmp_path / 'test_log.gsd', scalar_only=False ) - assert len(logged_data_dict) == 5 + assert len(logged_data_dict) == 6 assert list(logged_data_dict.keys()) == [ 'configuration/step', 'log/particles/pair_lj_energy', 'log/particles/pair_lj_force', 'log/value/potential_energy', 'log/value/pressure', + 'log/category', ] numpy.testing.assert_array_equal(logged_data_dict['configuration/step'], [0, 1]) @@ -926,16 +933,24 @@ def test_read_log(tmp_path): logged_data_dict['log/value/pressure'], [*frame0.log['value/pressure'], *frame1.log['value/pressure']], ) + numpy.testing.assert_array_equal( + logged_data_dict['log/category'], + numpy.array( + [frame0.log['category'], frame1.log['category']], + dtype=numpy.dtypes.StringDType, + ), + ) # Test scalar_only = True logged_data_dict = gsd.hoomd.read_log( name=tmp_path / 'test_log.gsd', scalar_only=True ) - assert len(logged_data_dict) == 3 + assert len(logged_data_dict) == 4 assert list(logged_data_dict.keys()) == [ 'configuration/step', 'log/value/potential_energy', 'log/value/pressure', + 'log/category', ] numpy.testing.assert_array_equal(logged_data_dict['configuration/step'], [0, 1]) numpy.testing.assert_array_equal( @@ -946,6 +961,13 @@ def test_read_log(tmp_path): logged_data_dict['log/value/pressure'], [*frame0.log['value/pressure'], *frame1.log['value/pressure']], ) + numpy.testing.assert_array_equal( + logged_data_dict['log/category'], + numpy.array( + [frame0.log['category'], frame1.log['category']], + dtype=numpy.dtypes.StringDType, + ), + ) def test_read_log_warning(tmp_path): diff --git a/pyproject.toml b/pyproject.toml index dfb43604..700919f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers=[ "License :: OSI Approved :: BSD License", "Topic :: Scientific/Engineering :: Physics", ] -dependencies = ["numpy>=1.19.0"] +dependencies = ["numpy>=2.0.0"] [project.scripts] gsd = "gsd.__main__:main"