From 3c7abc0e3f90af524dbb59ba766b779957f50c23 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Sat, 13 Dec 2025 10:39:45 -0600 Subject: [PATCH 1/9] Add support for double precision in write_chunk, passing default and optional values from hoomd for single and double precision --- gsd/fl.pyx | 27 +++++++++++++++++++++--- gsd/hoomd.py | 48 +++++++++++++++++++++++++++++++----------- gsd/test/test_hoomd.py | 32 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/gsd/fl.pyx b/gsd/fl.pyx index 851a5264..b25e9a3e 100644 --- a/gsd/fl.pyx +++ b/gsd/fl.pyx @@ -526,7 +526,7 @@ cdef class GSDFile: __raise_on_error(retval, self.name) - def write_chunk(self, name, data): + def write_chunk(self, name, data, precision=None): """write_chunk(name, data) Write a data chunk to the file. After writing all chunks in the @@ -537,6 +537,8 @@ cdef class GSDFile: data: Data to write into the chunk. Must be a numpy array, or array-like, with 2 or fewer dimensions. + precision (str): Approach for setting the specificity of floats. + If None, values are left as is. (Default ``None``) Warning: :py:meth:`write_chunk()` will implicitly converts array-like and @@ -577,6 +579,12 @@ cdef class GSDFile: cdef libgsd.gsd_type gsd_type cdef void *data_ptr + if precision == "single": + float_type = numpy.float32 + elif precision == "double": + float_type = numpy.float64 + else: + float_type = None # Special behavior for handling strings if type(data) is str: @@ -591,7 +599,15 @@ cdef class GSDFile: # Non-string behavior else: - data_array = numpy.ascontiguousarray(data) + if float_type is not None and isinstance(data, list) and isinstance(data[0], float): + data_array = numpy.ascontiguousarray(data, float_type) + elif ( + float_type is not None and + isinstance(data, numpy.ndarray) and data.dtype in (numpy.float32, numpy.float64) + ): + data_array = numpy.ascontiguousarray(data, float_type) + else: + data_array = numpy.ascontiguousarray(data) if data_array is not data: logger.warning('implicit data copy when writing chunk: ' + name) @@ -716,7 +732,7 @@ cdef class GSDFile: return index_entry != NULL - def read_chunk(self, frame, name): + def read_chunk(self, frame, name, precision="single"): """read_chunk(frame, name) Read a data chunk from the file and return it as a numpy array. @@ -777,6 +793,11 @@ cdef class GSDFile: cdef int64_t c_frame c_frame = frame + if precision == "single": + float_type = numpy.float32 + elif precision == "double": + float_type = numpy.float64 + with nogil: index_entry = libgsd.gsd_find_chunk(&self.__handle, c_frame, c_name) diff --git a/gsd/hoomd.py b/gsd/hoomd.py index 2e6b458c..f668d0c0 100644 --- a/gsd/hoomd.py +++ b/gsd/hoomd.py @@ -194,6 +194,14 @@ def __init__(self): self.image = None self.type_shapes = None + def validate_precision(self, data): + """Maintain floats in numpy arrays.""" + if isinstance(data, list): + return numpy.float32 + elif data.dtype == numpy.float64: + return numpy.float64 + return numpy.float32 + def validate(self): """Validate all attributes. @@ -207,31 +215,40 @@ def validate(self): replaced with contiguous numpy arrays of the appropriate type. """ if self.position is not None: - self.position = numpy.ascontiguousarray(self.position, dtype=numpy.float32) + # self.position = numpy.ascontiguousarray(self.position, dtype=numpy.float32) + self.position = numpy.ascontiguousarray( + self.position, dtype=self.validate_precision(self.position) + ) self.position = self.position.reshape([self.N, 3]) if self.orientation is not None: self.orientation = numpy.ascontiguousarray( - self.orientation, dtype=numpy.float32 + self.orientation, dtype=self.validate_precision(self.orientation) ) self.orientation = self.orientation.reshape([self.N, 4]) if self.typeid is not None: self.typeid = numpy.ascontiguousarray(self.typeid, dtype=numpy.uint32) self.typeid = self.typeid.reshape([self.N]) if self.mass is not None: - self.mass = numpy.ascontiguousarray(self.mass, dtype=numpy.float32) + self.mass = numpy.ascontiguousarray( + self.mass, dtype=self.validate_precision(self.mass) + ) self.mass = self.mass.reshape([self.N]) if self.charge is not None: - self.charge = numpy.ascontiguousarray(self.charge, dtype=numpy.float32) + self.charge = numpy.ascontiguousarray( + self.charge, dtype=self.validate_precision(self.charge) + ) self.charge = self.charge.reshape([self.N]) if self.diameter is not None: - self.diameter = numpy.ascontiguousarray(self.diameter, dtype=numpy.float32) + self.diameter = numpy.ascontiguousarray( + self.diameter, dtype=self.validate_precision(self.diameter) + ) self.diameter = self.diameter.reshape([self.N]) if self.body is not None: self.body = numpy.ascontiguousarray(self.body, dtype=numpy.int32) self.body = self.body.reshape([self.N]) if self.moment_inertia is not None: self.moment_inertia = numpy.ascontiguousarray( - self.moment_inertia, dtype=numpy.float32 + self.moment_inertia, dtype=self.validate_precision(self.moment_inertia) ) self.moment_inertia = self.moment_inertia.reshape([self.N, 3]) if self.velocity is not None: @@ -674,17 +691,19 @@ class HOOMDTrajectory: Args: file (`gsd.fl.GSDFile`): File to access. + precision (str): Precision to use for floats on write. Open hoomd GSD files with `open`. """ - def __init__(self, file): + def __init__(self, file, precision="single"): if file.mode == 'ab': msg = 'Append mode not yet supported' raise ValueError(msg) self._file = file self._initial_frame = None + self._precision = precision # Used to cache positive results when chunks exist in frame 0. self._chunk_exists_frame_0 = {} @@ -711,6 +730,11 @@ def __init__(self, file): def file(self): """:class:`gsd.fl.GSDFile`: The file handle.""" return self._file + + @property + def precision(self): + """:class:str: The object write precision.""" + return self._precision def __len__(self): """The number of frames in the trajectory.""" @@ -765,15 +789,15 @@ def append(self, frame): b = numpy.array(data, dtype=numpy.dtype((bytes, wid))) data = b.view(dtype=numpy.int8).reshape(len(b), wid) - self.file.write_chunk(path + '/' + name, data) + self.file.write_chunk(path + '/' + name, data, self.precision) # write state data for state, data in frame.state.items(): - self.file.write_chunk('state/' + state, data) + self.file.write_chunk('state/' + state, data, self.precision) # write log data for log, data in frame.log.items(): - self.file.write_chunk('log/' + log, data) + self.file.write_chunk('log/' + log, data, self.precision) self.file.end_frame() @@ -1062,7 +1086,7 @@ def flush(self): self._file.flush() -def open(name, mode='r'): # noqa: A001 - allow shadowing builtin open +def open(name, mode='r', precision="single"): # noqa: A001 - allow shadowing builtin open """Open a hoomd schema GSD file. The return value of `open` can be used as a context manager. @@ -1114,7 +1138,7 @@ def open(name, mode='r'): # noqa: A001 - allow shadowing builtin open schema_version=[1, 4], ) - return HOOMDTrajectory(gsdfileobj) + return HOOMDTrajectory(gsdfileobj, precision=precision) def read_log(name: str, scalar_only=False, glob_pattern='*'): diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index dd2c57e2..e8b05173 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -1075,3 +1075,35 @@ def test_initial_frame_copy(tmp_path, open_mode): for key in frame_1.log.keys(): assert frame_1.log[key] is initial.log[key] assert not frame_1.log[key].flags.writeable + +def test_demonstrate_buggy_behavior(tmp_path): + frame = gsd.hoomd.Frame() + frame.particles.N = 1 + frame.particles.position = [[0.1, 0.2, 0.3]] # floats + with gsd.hoomd.open( # converts from python float to dtype float64 + name=tmp_path / 'double.gsd', mode="w", precision="double" + ) as hf: + hf.append(frame) + with gsd.hoomd.open( # converts from float64 to float32 + name=tmp_path / 'single.gsd', mode="w", precision="single" + ) as hf: + hf.append(frame) + + with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode="r", precision="single") as hf: + s = hf[0] # read as a single precision + numpy.testing.assert_array_equal( + numpy.ascontiguousarray(frame.particles.position, dtype=numpy.float32), + s.particles.position + ) + assert s.particles.position.dtype == numpy.float32 + + with gsd.fl.open(name=tmp_path / 'double.gsd', mode='r') as f: + position = f.read_chunk(frame=0, name='particles/position') + assert position.dtype == numpy.float64 + with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode="r", precision="double") as hf: + s = hf[0] # read as double precision + numpy.testing.assert_array_equal( + frame.particles.position, s.particles.position + ) + assert s.particles.position.dtype == numpy.float64 + assert frame.particles.position.dtype == numpy.float32 \ No newline at end of file From a559bd6993e82bac9aeca998a831ce038b9a1b80 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Sat, 13 Dec 2025 10:49:47 -0600 Subject: [PATCH 2/9] Add further precision tests --- gsd/test/test_hoomd.py | 118 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 13 deletions(-) diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index e8b05173..2d1f730a 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -1076,10 +1076,102 @@ def test_initial_frame_copy(tmp_path, open_mode): assert frame_1.log[key] is initial.log[key] assert not frame_1.log[key].flags.writeable -def test_demonstrate_buggy_behavior(tmp_path): +def test_float64(tmp_path, open_mode): + """Test that charges can be optionally set to float32 or float64.""" frame = gsd.hoomd.Frame() - frame.particles.N = 1 - frame.particles.position = [[0.1, 0.2, 0.3]] # floats + frame.particles.N = 4 + charges32 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float32) + charges64 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float64) + frame.particles.charge = charges64 + numpy.testing.assert_array_equal( + frame.particles.charge, charges64 + ) + + with gsd.hoomd.open( + name=tmp_path / 'test_defaults.gsd', mode=open_mode.write, precision="double" + ) as hf: + hf.append(frame) + + with gsd.hoomd.open(name=tmp_path / 'test_defaults.gsd', mode=open_mode.read) as hf: + s = hf[0] + + numpy.testing.assert_array_equal( + s.particles.charge, charges64 + ) + numpy.testing.assert_raises(AssertionError, numpy.testing.assert_array_equal, + s.particles.charge, charges32 + ) + +def test_precision(tmp_path, open_mode): + """Test that single and double precision files can be stored.""" + frame = gsd.hoomd.Frame() + frame.particles.N = 4 + charges32 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float32) + charges64 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float64) + frame.particles.charge = charges64 + numpy.testing.assert_array_equal( + frame.particles.charge, charges64 + ) + + with gsd.hoomd.open( + name=tmp_path / 'double.gsd', mode=open_mode.write, precision="double" + ) as hf: + hf.append(frame) + + with gsd.hoomd.open( + name=tmp_path / 'single.gsd', mode=open_mode.write, precision="single" + ) as hf: + hf.append(frame) + + with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode=open_mode.read) as hf: + s = hf[0] + + numpy.testing.assert_array_equal( + s.particles.charge, charges64 + ) + + with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode=open_mode.read) as hf: + s = hf[0] + numpy.testing.assert_array_equal( + s.particles.charge, charges32 + ) + +def test_all_precision(tmp_path, open_mode): + frame0 = make_nondefault_frame() + with gsd.hoomd.open( # keeps at 64 bit + name=tmp_path / 'double.gsd', mode=open_mode.write, precision="double" + ) as hf: + hf.append(frame0) + + with gsd.hoomd.open( #fails if second, converts to 32 bit + name=tmp_path / 'single.gsd', mode=open_mode.write, precision="single" + ) as hf: + hf.append(frame0) + + with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode=open_mode.read, precision="double") as hf: + s = hf[0] + for name in [ + "position", "orientation", "velocity", "angmom", "charge" + ]: + numpy.testing.assert_array_equal( + getattr(frame0.particles, name), getattr(s.particles, name) + ) + assert getattr(s.particles, name).dtype == numpy.float64 + + with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode=open_mode.read) as hf: + s = hf[0] + for name in [ + "position", "orientation", "velocity", "angmom", "charge" + ]: + numpy.testing.assert_array_equal( + getattr(frame0.particles, name), getattr(s.particles, name) + ) + assert getattr(s.particles, name).dtype == numpy.float32 + +def test_write_multiple_precision(tmp_path): + frame = gsd.hoomd.Frame() + frame.particles.N = int(1) + frame.particles.position = [[0.1, 0.2, 0.3]] # is a list of list of floats with gsd.hoomd.open( # converts from python float to dtype float64 name=tmp_path / 'double.gsd', mode="w", precision="double" ) as hf: @@ -1089,21 +1181,21 @@ def test_demonstrate_buggy_behavior(tmp_path): ) as hf: hf.append(frame) + # frame is now fixed single precision, due to the conversion in hf.append(Frame) with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode="r", precision="single") as hf: s = hf[0] # read as a single precision numpy.testing.assert_array_equal( - numpy.ascontiguousarray(frame.particles.position, dtype=numpy.float32), - s.particles.position + frame.particles.position, s.particles.position ) - assert s.particles.position.dtype == numpy.float32 - - with gsd.fl.open(name=tmp_path / 'double.gsd', mode='r') as f: - position = f.read_chunk(frame=0, name='particles/position') - assert position.dtype == numpy.float64 with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode="r", precision="double") as hf: s = hf[0] # read as double precision numpy.testing.assert_array_equal( - frame.particles.position, s.particles.position + frame.particles.position, s.particles.position # will fail since frame is now single precision ) - assert s.particles.position.dtype == numpy.float64 - assert frame.particles.position.dtype == numpy.float32 \ No newline at end of file + + + + + + + From cc5d8c7d20b7fb12d56c27d64795055cff84b85c Mon Sep 17 00:00:00 2001 From: CalCraven Date: Mon, 15 Dec 2025 07:44:43 -0600 Subject: [PATCH 3/9] Fix prek formatting --- gsd/fl.pyx | 2 +- gsd/hoomd.py | 6 +-- gsd/test/test_hoomd.py | 105 +++++++++++++++++++---------------------- 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/gsd/fl.pyx b/gsd/fl.pyx index b25e9a3e..43b9449a 100644 --- a/gsd/fl.pyx +++ b/gsd/fl.pyx @@ -602,7 +602,7 @@ cdef class GSDFile: if float_type is not None and isinstance(data, list) and isinstance(data[0], float): data_array = numpy.ascontiguousarray(data, float_type) elif ( - float_type is not None and + float_type is not None and isinstance(data, numpy.ndarray) and data.dtype in (numpy.float32, numpy.float64) ): data_array = numpy.ascontiguousarray(data, float_type) diff --git a/gsd/hoomd.py b/gsd/hoomd.py index f668d0c0..16ac09ab 100644 --- a/gsd/hoomd.py +++ b/gsd/hoomd.py @@ -696,7 +696,7 @@ class HOOMDTrajectory: Open hoomd GSD files with `open`. """ - def __init__(self, file, precision="single"): + def __init__(self, file, precision='single'): if file.mode == 'ab': msg = 'Append mode not yet supported' raise ValueError(msg) @@ -730,7 +730,7 @@ def __init__(self, file, precision="single"): def file(self): """:class:`gsd.fl.GSDFile`: The file handle.""" return self._file - + @property def precision(self): """:class:str: The object write precision.""" @@ -1086,7 +1086,7 @@ def flush(self): self._file.flush() -def open(name, mode='r', precision="single"): # noqa: A001 - allow shadowing builtin open +def open(name, mode='r', precision='single'): # noqa: A001 - allow shadowing builtin open """Open a hoomd schema GSD file. The return value of `open` can be used as a context manager. diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index 2d1f730a..92550869 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -1076,83 +1076,79 @@ def test_initial_frame_copy(tmp_path, open_mode): assert frame_1.log[key] is initial.log[key] assert not frame_1.log[key].flags.writeable + def test_float64(tmp_path, open_mode): """Test that charges can be optionally set to float32 or float64.""" frame = gsd.hoomd.Frame() frame.particles.N = 4 - charges32 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float32) - charges64 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float64) + charges32 = numpy.array([-0.1, 0.1 / 3, 0.1 / 3, 0.1 / 3], dtype=numpy.float32) + charges64 = numpy.array([-0.1, 0.1 / 3, 0.1 / 3, 0.1 / 3], dtype=numpy.float64) frame.particles.charge = charges64 - numpy.testing.assert_array_equal( - frame.particles.charge, charges64 - ) + numpy.testing.assert_array_equal(frame.particles.charge, charges64) with gsd.hoomd.open( - name=tmp_path / 'test_defaults.gsd', mode=open_mode.write, precision="double" + name=tmp_path / 'test_defaults.gsd', mode=open_mode.write, precision='double' ) as hf: hf.append(frame) with gsd.hoomd.open(name=tmp_path / 'test_defaults.gsd', mode=open_mode.read) as hf: s = hf[0] - numpy.testing.assert_array_equal( - s.particles.charge, charges64 - ) - numpy.testing.assert_raises(AssertionError, numpy.testing.assert_array_equal, - s.particles.charge, charges32 + numpy.testing.assert_array_equal(s.particles.charge, charges64) + numpy.testing.assert_raises( + AssertionError, + numpy.testing.assert_array_equal, + s.particles.charge, + charges32, ) + def test_precision(tmp_path, open_mode): """Test that single and double precision files can be stored.""" frame = gsd.hoomd.Frame() frame.particles.N = 4 - charges32 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float32) - charges64 = numpy.array([-0.1, 0.1/3, 0.1/3, 0.1/3], dtype=numpy.float64) + charges32 = numpy.array([-0.1, 0.1 / 3, 0.1 / 3, 0.1 / 3], dtype=numpy.float32) + charges64 = numpy.array([-0.1, 0.1 / 3, 0.1 / 3, 0.1 / 3], dtype=numpy.float64) frame.particles.charge = charges64 - numpy.testing.assert_array_equal( - frame.particles.charge, charges64 - ) + numpy.testing.assert_array_equal(frame.particles.charge, charges64) with gsd.hoomd.open( - name=tmp_path / 'double.gsd', mode=open_mode.write, precision="double" + name=tmp_path / 'double.gsd', mode=open_mode.write, precision='double' ) as hf: hf.append(frame) with gsd.hoomd.open( - name=tmp_path / 'single.gsd', mode=open_mode.write, precision="single" + name=tmp_path / 'single.gsd', mode=open_mode.write, precision='single' ) as hf: hf.append(frame) with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode=open_mode.read) as hf: s = hf[0] - numpy.testing.assert_array_equal( - s.particles.charge, charges64 - ) + numpy.testing.assert_array_equal(s.particles.charge, charges64) with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode=open_mode.read) as hf: s = hf[0] - numpy.testing.assert_array_equal( - s.particles.charge, charges32 - ) + numpy.testing.assert_array_equal(s.particles.charge, charges32) + def test_all_precision(tmp_path, open_mode): frame0 = make_nondefault_frame() - with gsd.hoomd.open( # keeps at 64 bit - name=tmp_path / 'double.gsd', mode=open_mode.write, precision="double" + with gsd.hoomd.open( # keeps at 64 bit + name=tmp_path / 'double.gsd', mode=open_mode.write, precision='double' ) as hf: hf.append(frame0) - with gsd.hoomd.open( #fails if second, converts to 32 bit - name=tmp_path / 'single.gsd', mode=open_mode.write, precision="single" + with gsd.hoomd.open( # fails if second, converts to 32 bit + name=tmp_path / 'single.gsd', mode=open_mode.write, precision='single' ) as hf: hf.append(frame0) - with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode=open_mode.read, precision="double") as hf: + with gsd.hoomd.open( + name=tmp_path / 'double.gsd', mode=open_mode.read, precision='double' + ) as hf: s = hf[0] - for name in [ - "position", "orientation", "velocity", "angmom", "charge" - ]: + for name in ['position', 'orientation', 'velocity', 'angmom', 'charge']: numpy.testing.assert_array_equal( getattr(frame0.particles, name), getattr(s.particles, name) ) @@ -1160,42 +1156,37 @@ def test_all_precision(tmp_path, open_mode): with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode=open_mode.read) as hf: s = hf[0] - for name in [ - "position", "orientation", "velocity", "angmom", "charge" - ]: + for name in ['position', 'orientation', 'velocity', 'angmom', 'charge']: numpy.testing.assert_array_equal( getattr(frame0.particles, name), getattr(s.particles, name) ) - assert getattr(s.particles, name).dtype == numpy.float32 + assert getattr(s.particles, name).dtype == numpy.float32 + def test_write_multiple_precision(tmp_path): frame = gsd.hoomd.Frame() frame.particles.N = int(1) - frame.particles.position = [[0.1, 0.2, 0.3]] # is a list of list of floats - with gsd.hoomd.open( # converts from python float to dtype float64 - name=tmp_path / 'double.gsd', mode="w", precision="double" + frame.particles.position = [[0.1, 0.2, 0.3]] # is a list of list of floats + with gsd.hoomd.open( # converts from python float to dtype float64 + name=tmp_path / 'double.gsd', mode='w', precision='double' ) as hf: hf.append(frame) - with gsd.hoomd.open( # converts from float64 to float32 - name=tmp_path / 'single.gsd', mode="w", precision="single" + with gsd.hoomd.open( # converts from float64 to float32 + name=tmp_path / 'single.gsd', mode='w', precision='single' ) as hf: hf.append(frame) - + # frame is now fixed single precision, due to the conversion in hf.append(Frame) - with gsd.hoomd.open(name=tmp_path / 'single.gsd', mode="r", precision="single") as hf: - s = hf[0] # read as a single precision - numpy.testing.assert_array_equal( - frame.particles.position, s.particles.position - ) - with gsd.hoomd.open(name=tmp_path / 'double.gsd', mode="r", precision="double") as hf: - s = hf[0] # read as double precision + with gsd.hoomd.open( + name=tmp_path / 'single.gsd', mode='r', precision='single' + ) as hf: + s = hf[0] # read as a single precision + numpy.testing.assert_array_equal(frame.particles.position, s.particles.position) + with gsd.hoomd.open( + name=tmp_path / 'double.gsd', mode='r', precision='double' + ) as hf: + s = hf[0] # read as double precision numpy.testing.assert_array_equal( - frame.particles.position, s.particles.position # will fail since frame is now single precision + frame.particles.position, + s.particles.position, # will fail since frame is now single precision ) - - - - - - - From 07eb976bf79a87c41c89e8d48d568006cbe56f33 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Mon, 15 Dec 2025 07:46:13 -0600 Subject: [PATCH 4/9] add Nicholas Craven to contributors --- doc/credits.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/credits.rst b/doc/credits.rst index 9e7fb1d2..da194d1b 100644 --- a/doc/credits.rst +++ b/doc/credits.rst @@ -20,3 +20,4 @@ The following people contributed to GSD. * Charlotte Shiqi Zhao, University of Michigan * Tim Moore, University of Michigan * Joseph Burkhart, University of Michigan +* Nicholas Craven, Vanderbilt University From 3b7bfb3a0cde90e9ce5368ce9e6e4a4bc6a030e3 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Mon, 15 Dec 2025 07:52:43 -0600 Subject: [PATCH 5/9] Add 495 to changelog --- CHANGELOG.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9c67cea..c3029735 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,9 +7,16 @@ Change Log `GSD `_ releases follow `semantic versioning `_. -4.x +4.x (2025-12-12) --- +*Added:* + +* ``gsd.hoomd`` schema now accepts float64 in validate, ``gsd.hoomd.open`` will take precision arguments + (`#495 `__). +* ``gsd.fl.write_chunk`` will take precision arguments, rounding any floats to 32 or 64 precision with `"double"` or `"single"` + (`#495 `__). + 4.2.0 (2025-10-07) ^^^^^^^^^^^^^^^^^^ From 6df663b4610e6d3eb4a3c44067239bbb659db08f Mon Sep 17 00:00:00 2001 From: CalCraven <54594941+CalCraven@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:49:42 -0600 Subject: [PATCH 6/9] Update CHANGELOG.rst Co-authored-by: Joshua A. Anderson --- CHANGELOG.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3029735..888173c9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,9 +7,12 @@ Change Log `GSD `_ releases follow `semantic versioning `_. -4.x (2025-12-12) +5.x --- +5.0.0 (Not yet released) +------------------------ + *Added:* * ``gsd.hoomd`` schema now accepts float64 in validate, ``gsd.hoomd.open`` will take precision arguments @@ -17,6 +20,9 @@ Change Log * ``gsd.fl.write_chunk`` will take precision arguments, rounding any floats to 32 or 64 precision with `"double"` or `"single"` (`#495 `__). +4.x +--- + 4.2.0 (2025-10-07) ^^^^^^^^^^^^^^^^^^ From df776ed727206fac056da8d1e24f281cb810a17b Mon Sep 17 00:00:00 2001 From: CalCraven Date: Mon, 15 Dec 2025 22:46:59 -0600 Subject: [PATCH 7/9] Move precision argument from write_chunk to append --- gsd/fl.pyx | 27 +++------------------------ gsd/hoomd.py | 30 ++++++++++++++++++++++-------- gsd/test/test_hoomd.py | 30 ++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/gsd/fl.pyx b/gsd/fl.pyx index 43b9449a..851a5264 100644 --- a/gsd/fl.pyx +++ b/gsd/fl.pyx @@ -526,7 +526,7 @@ cdef class GSDFile: __raise_on_error(retval, self.name) - def write_chunk(self, name, data, precision=None): + def write_chunk(self, name, data): """write_chunk(name, data) Write a data chunk to the file. After writing all chunks in the @@ -537,8 +537,6 @@ cdef class GSDFile: data: Data to write into the chunk. Must be a numpy array, or array-like, with 2 or fewer dimensions. - precision (str): Approach for setting the specificity of floats. - If None, values are left as is. (Default ``None``) Warning: :py:meth:`write_chunk()` will implicitly converts array-like and @@ -579,12 +577,6 @@ cdef class GSDFile: cdef libgsd.gsd_type gsd_type cdef void *data_ptr - if precision == "single": - float_type = numpy.float32 - elif precision == "double": - float_type = numpy.float64 - else: - float_type = None # Special behavior for handling strings if type(data) is str: @@ -599,15 +591,7 @@ cdef class GSDFile: # Non-string behavior else: - if float_type is not None and isinstance(data, list) and isinstance(data[0], float): - data_array = numpy.ascontiguousarray(data, float_type) - elif ( - float_type is not None and - isinstance(data, numpy.ndarray) and data.dtype in (numpy.float32, numpy.float64) - ): - data_array = numpy.ascontiguousarray(data, float_type) - else: - data_array = numpy.ascontiguousarray(data) + data_array = numpy.ascontiguousarray(data) if data_array is not data: logger.warning('implicit data copy when writing chunk: ' + name) @@ -732,7 +716,7 @@ cdef class GSDFile: return index_entry != NULL - def read_chunk(self, frame, name, precision="single"): + def read_chunk(self, frame, name): """read_chunk(frame, name) Read a data chunk from the file and return it as a numpy array. @@ -793,11 +777,6 @@ cdef class GSDFile: cdef int64_t c_frame c_frame = frame - if precision == "single": - float_type = numpy.float32 - elif precision == "double": - float_type = numpy.float64 - with nogil: index_entry = libgsd.gsd_find_chunk(&self.__handle, c_frame, c_name) diff --git a/gsd/hoomd.py b/gsd/hoomd.py index 16ac09ab..c0e2eee8 100644 --- a/gsd/hoomd.py +++ b/gsd/hoomd.py @@ -196,11 +196,12 @@ def __init__(self): def validate_precision(self, data): """Maintain floats in numpy arrays.""" + outFloat = numpy.float32 if isinstance(data, list): - return numpy.float32 + outFloat = numpy.float64 elif data.dtype == numpy.float64: - return numpy.float64 - return numpy.float32 + outFloat = numpy.float64 + return outFloat def validate(self): """Validate all attributes. @@ -215,7 +216,6 @@ def validate(self): replaced with contiguous numpy arrays of the appropriate type. """ if self.position is not None: - # self.position = numpy.ascontiguousarray(self.position, dtype=numpy.float32) self.position = numpy.ascontiguousarray( self.position, dtype=self.validate_precision(self.position) ) @@ -761,6 +761,8 @@ def append(self, frame): if self._initial_frame is None and len(self) > 0: self._read_frame(0) + float_type = numpy.float64 if self.precision == 'double' else numpy.float32 + for path in [ 'configuration', 'particles', @@ -788,16 +790,27 @@ def append(self, frame): wid = max(len(w) for w in data) + 1 b = numpy.array(data, dtype=numpy.dtype((bytes, wid))) data = b.view(dtype=numpy.int8).reshape(len(b), wid) - - self.file.write_chunk(path + '/' + name, data, self.precision) + if name in [ + 'position', + 'orientation', + 'velocity', + 'angmom', + 'charge', + 'box', + 'value', + ]: + # convert using float specified precision + data = numpy.ascontiguousarray(data, dtype=float_type) + + self.file.write_chunk(path + '/' + name, data) # write state data for state, data in frame.state.items(): - self.file.write_chunk('state/' + state, data, self.precision) + self.file.write_chunk('state/' + state, data) # write log data for log, data in frame.log.items(): - self.file.write_chunk('log/' + log, data, self.precision) + self.file.write_chunk('log/' + log, data) self.file.end_frame() @@ -1094,6 +1107,7 @@ def open(name, mode='r', precision='single'): # noqa: A001 - allow shadowing bu Args: name (str): File name to open. mode (str): File open mode. + precision (str): Float precision to expect in File. Can be 'single' or 'double'. Returns: `HOOMDTrajectory` instance that accesses the file **name** with the diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index 92550869..abdc1964 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -1133,13 +1133,14 @@ def test_precision(tmp_path, open_mode): def test_all_precision(tmp_path, open_mode): + """Test both single and double precision on default Frame.""" frame0 = make_nondefault_frame() - with gsd.hoomd.open( # keeps at 64 bit + with gsd.hoomd.open( name=tmp_path / 'double.gsd', mode=open_mode.write, precision='double' ) as hf: hf.append(frame0) - with gsd.hoomd.open( # fails if second, converts to 32 bit + with gsd.hoomd.open( name=tmp_path / 'single.gsd', mode=open_mode.write, precision='single' ) as hf: hf.append(frame0) @@ -1158,35 +1159,44 @@ def test_all_precision(tmp_path, open_mode): s = hf[0] for name in ['position', 'orientation', 'velocity', 'angmom', 'charge']: numpy.testing.assert_array_equal( - getattr(frame0.particles, name), getattr(s.particles, name) + numpy.ascontiguousarray( + getattr(frame0.particles, name), dtype=numpy.float32 + ), + getattr(s.particles, name), ) assert getattr(s.particles, name).dtype == numpy.float32 def test_write_multiple_precision(tmp_path): + """Test single, then double precision writing on the particles position.""" frame = gsd.hoomd.Frame() - frame.particles.N = int(1) + frame.particles.N = 1 frame.particles.position = [[0.1, 0.2, 0.3]] # is a list of list of floats - with gsd.hoomd.open( # converts from python float to dtype float64 - name=tmp_path / 'double.gsd', mode='w', precision='double' - ) as hf: - hf.append(frame) with gsd.hoomd.open( # converts from float64 to float32 name=tmp_path / 'single.gsd', mode='w', precision='single' ) as hf: hf.append(frame) + with gsd.hoomd.open( # converts from python float to dtype float64 + name=tmp_path / 'double.gsd', mode='w', precision='double' + ) as hf: + hf.append(frame) # frame is now fixed single precision, due to the conversion in hf.append(Frame) with gsd.hoomd.open( name=tmp_path / 'single.gsd', mode='r', precision='single' ) as hf: s = hf[0] # read as a single precision - numpy.testing.assert_array_equal(frame.particles.position, s.particles.position) + numpy.testing.assert_array_equal( + numpy.ascontiguousarray(frame.particles.position, dtype=numpy.float32), + s.particles.position, + ) + assert s.particles.position.dtype == numpy.float32 with gsd.hoomd.open( name=tmp_path / 'double.gsd', mode='r', precision='double' ) as hf: s = hf[0] # read as double precision numpy.testing.assert_array_equal( frame.particles.position, - s.particles.position, # will fail since frame is now single precision + s.particles.position, ) + assert s.particles.position.dtype == numpy.float64 From 1c9e5fe47aaae6c354948e01dee674e0965d2254 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Wed, 17 Dec 2025 10:52:08 -0600 Subject: [PATCH 8/9] Add double precision to failing tests to account for lazy precision on write, while keeping python Frame object untouched --- gsd/test/test_hoomd.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gsd/test/test_hoomd.py b/gsd/test/test_hoomd.py index abdc1964..74f87976 100644 --- a/gsd/test/test_hoomd.py +++ b/gsd/test/test_hoomd.py @@ -355,7 +355,7 @@ def test_fallback(tmp_path, open_mode): frame2.pairs.N = 7 with gsd.hoomd.open( - name=tmp_path / 'test_fallback.gsd', mode=open_mode.write + name=tmp_path / 'test_fallback.gsd', mode=open_mode.write, precision='double' ) as hf: hf.extend([frame0, frame1, frame2]) @@ -516,7 +516,7 @@ def test_fallback_to_frame0(tmp_path, open_mode): frame1.pairs.N = None with gsd.hoomd.open( - name=tmp_path / 'test_fallback2.gsd', mode=open_mode.write + name=tmp_path / 'test_fallback2.gsd', mode=open_mode.write, precision='double' ) as hf: hf.extend([frame0, frame1]) @@ -987,7 +987,9 @@ def test_read_log_warning(tmp_path): def test_initial_frame_copy(tmp_path, open_mode): """Ensure that the user does not unintentionally modify _initial_frame.""" with gsd.hoomd.open( - name=tmp_path / 'test_initial_frame_copy.gsd', mode=open_mode.write + name=tmp_path / 'test_initial_frame_copy.gsd', + mode=open_mode.write, + precision='double', ) as hf: frame = make_nondefault_frame() From c031b7c776c101ee0092bcd9f1fc02bd715dcf17 Mon Sep 17 00:00:00 2001 From: CalCraven Date: Wed, 17 Dec 2025 11:06:36 -0600 Subject: [PATCH 9/9] Fix formatting in args for CHANGELOG.rst to remove error in docs building --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 888173c9..77428422 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,7 +17,7 @@ Change Log * ``gsd.hoomd`` schema now accepts float64 in validate, ``gsd.hoomd.open`` will take precision arguments (`#495 `__). -* ``gsd.fl.write_chunk`` will take precision arguments, rounding any floats to 32 or 64 precision with `"double"` or `"single"` +* ``gsd.fl.write_chunk`` will take precision arguments, rounding any floats to 32 or 64 precision with ``double`` or ``single`` (`#495 `__). 4.x