From e0359d98107386ed244c9e4b450da6fb977c53e7 Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Thu, 27 Aug 2026 19:16:32 -0700 Subject: [PATCH 1/2] test loading Bmad particle group --- tests/test_read.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/test_read.c b/tests/test_read.c index 8a3c2fc..4cf1d22 100644 --- a/tests/test_read.c +++ b/tests/test_read.c @@ -1158,6 +1158,8 @@ void test_padding_mismatch_file_based_root_group(void) { void test_read_bmad_dump(void) { pmd_series *series; pmd_iteration *iter; + pmd_particle_group pg; + pmd_particle_group_read_info read_info; pmd_status result; int64_t *iterations; int num_iterations; @@ -1182,7 +1184,75 @@ void test_read_bmad_dump(void) { TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); TEST_ASSERT_EQUAL_INT64(1000, particle_count); - /* Clean up */ + /* Read the electron particle group into caller supplied arrays. + * + * Only the records Bmad stores as datasets are read. position/z, weight and + * particleStatus are constant records whose 'value' attribute is a one element array + * rather than a scalar, which read_record_generic rejects, so their pointers are left + * NULL and those records are skipped. positionOffset and id are absent from the file. */ + memset(&pg, 0, sizeof(pmd_particle_group)); + pg.num_particles = particle_count; + pg.species_type = NULL; /* Not used by read function */ + + pg.x = (double *)malloc(particle_count * sizeof(double)); + pg.y = (double *)malloc(particle_count * sizeof(double)); + pg.t = (double *)malloc(particle_count * sizeof(double)); + pg.px = (double *)malloc(particle_count * sizeof(double)); + pg.py = (double *)malloc(particle_count * sizeof(double)); + pg.pz = (double *)malloc(particle_count * sizeof(double)); + pg.id = (int64_t *)malloc(particle_count * sizeof(int64_t)); + TEST_ASSERT_NOT_NULL(pg.x); + TEST_ASSERT_NOT_NULL(pg.y); + TEST_ASSERT_NOT_NULL(pg.t); + TEST_ASSERT_NOT_NULL(pg.px); + TEST_ASSERT_NOT_NULL(pg.py); + TEST_ASSERT_NOT_NULL(pg.pz); + TEST_ASSERT_NOT_NULL(pg.id); + + result = pmd_read_particle_group(iter, "electron", &pg, &read_info); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + + /* The skipped records leave their pointers untouched */ + TEST_ASSERT_NULL(pg.z); + TEST_ASSERT_NULL(pg.weight); + TEST_ASSERT_NULL(pg.status); + + /* Positions come straight from the datasets (unitSI is 1.0, metres) */ + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(7.394850528355765e-06, pg.x[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-0.00026090528338340325, pg.x[999]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-3.0302666148159694e-05, pg.y[0]); + + /* Momenta are scaled by the file's unitSI into eV/c. The values land ~7e-9 above the + * raw file numbers because parcel's CLIGHT is 299792456 rather than 299792458, which is + * why these need the relative tolerance rather than an exact comparison. */ + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(2029.7952043592982, pg.px[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(33557.29782907889, pg.py[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5002038554.641738, pg.pz[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5001545000.86001, pg.pz[999]); + + /* pg.t is read but deliberately not asserted: parcel returns the 'time' record on its + * own, while EXT_BeamPhysics.md defines absolute time as time + timeOffset and this + * file carries the reference time in timeOffset. */ + + /* The records backed by datasets were found */ + TEST_ASSERT_TRUE(read_info.t_present); + TEST_ASSERT_TRUE(read_info.px_present); + TEST_ASSERT_TRUE(read_info.py_present); + TEST_ASSERT_TRUE(read_info.pz_present); + + /* The file carries no id record, so the index fallback fills it */ + TEST_ASSERT_FALSE(read_info.id_present); + TEST_ASSERT_EQUAL_INT64(0, pg.id[0]); + TEST_ASSERT_EQUAL_INT64(999, pg.id[999]); + + /* Clean up - the arrays are ours, so pmd_free_particle_group must not be used */ + free(pg.x); + free(pg.y); + free(pg.t); + free(pg.px); + free(pg.py); + free(pg.pz); + free(pg.id); pmd_close_iteration(iter); free(iterations); pmd_close_series(series); From 1a2ce03f8b7777c4710fc944d6a1472e530f493e Mon Sep 17 00:00:00 2001 From: "Christopher M. Pierce" Date: Fri, 28 Aug 2026 01:01:46 -0700 Subject: [PATCH 2/2] allow reading of single element arrays in constant records --- parcel.h | 12 ++++--- tests/test_read.c | 86 +++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 56 deletions(-) diff --git a/parcel.h b/parcel.h index 2f37f57..bd2fce0 100644 --- a/parcel.h +++ b/parcel.h @@ -5133,7 +5133,7 @@ static pmd_status read_record_generic(hid_t group_id, const char *name, } } - /* Validate that value is scalar, not an array */ + /* Validate that value holds exactly one element */ hid_t attr_space = H5Aget_space(attr_id); if (attr_space < 0) { H5Aclose(attr_id); @@ -5141,12 +5141,14 @@ static pmd_status read_record_generic(hid_t group_id, const char *name, return PMD_ERROR_HDF5; } - H5S_class_t space_type = H5Sget_simple_extent_type(attr_space); + hssize_t value_points = H5Sget_simple_extent_npoints(attr_space); H5Sclose(attr_space); - if (space_type != H5S_SCALAR) { - /* value must be a scalar, not an array */ - pmd_log(PMD_LOG_ERROR, "read_record_generic: Constant record '%s' has array 'value', expected scalar", name); + /* Parcel supports reading either a scalar dataspace or a single element array (output + * by some codes). Reject anything with none or more than one element here. */ + if (value_points != 1) { + pmd_log(PMD_LOG_ERROR, "read_record_generic: Constant record '%s' has 'value' with %lld elements, expected 1", + name, (long long)value_points); H5Aclose(attr_id); H5Gclose(group_id_local); return PMD_ERROR_FILE_FORMAT; diff --git a/tests/test_read.c b/tests/test_read.c index 4cf1d22..2c1a320 100644 --- a/tests/test_read.c +++ b/tests/test_read.c @@ -1158,7 +1158,7 @@ void test_padding_mismatch_file_based_root_group(void) { void test_read_bmad_dump(void) { pmd_series *series; pmd_iteration *iter; - pmd_particle_group pg; + pmd_particle_group *pg; pmd_particle_group_read_info read_info; pmd_status result; int64_t *iterations; @@ -1184,75 +1184,59 @@ void test_read_bmad_dump(void) { TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); TEST_ASSERT_EQUAL_INT64(1000, particle_count); - /* Read the electron particle group into caller supplied arrays. - * - * Only the records Bmad stores as datasets are read. position/z, weight and - * particleStatus are constant records whose 'value' attribute is a one element array - * rather than a scalar, which read_record_generic rejects, so their pointers are left - * NULL and those records are skipped. positionOffset and id are absent from the file. */ - memset(&pg, 0, sizeof(pmd_particle_group)); - pg.num_particles = particle_count; - pg.species_type = NULL; /* Not used by read function */ - - pg.x = (double *)malloc(particle_count * sizeof(double)); - pg.y = (double *)malloc(particle_count * sizeof(double)); - pg.t = (double *)malloc(particle_count * sizeof(double)); - pg.px = (double *)malloc(particle_count * sizeof(double)); - pg.py = (double *)malloc(particle_count * sizeof(double)); - pg.pz = (double *)malloc(particle_count * sizeof(double)); - pg.id = (int64_t *)malloc(particle_count * sizeof(int64_t)); - TEST_ASSERT_NOT_NULL(pg.x); - TEST_ASSERT_NOT_NULL(pg.y); - TEST_ASSERT_NOT_NULL(pg.t); - TEST_ASSERT_NOT_NULL(pg.px); - TEST_ASSERT_NOT_NULL(pg.py); - TEST_ASSERT_NOT_NULL(pg.pz); - TEST_ASSERT_NOT_NULL(pg.id); - - result = pmd_read_particle_group(iter, "electron", &pg, &read_info); + /* Read the whole electron particle group */ + result = pmd_allocate_particle_group(iter, "electron", &pg); TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); + TEST_ASSERT_NOT_NULL(pg); + TEST_ASSERT_EQUAL_INT64(1000, pg->num_particles); - /* The skipped records leave their pointers untouched */ - TEST_ASSERT_NULL(pg.z); - TEST_ASSERT_NULL(pg.weight); - TEST_ASSERT_NULL(pg.status); + result = pmd_read_particle_group(iter, "electron", pg, &read_info); + TEST_ASSERT_EQUAL_INT(PMD_SUCCESS, result); /* Positions come straight from the datasets (unitSI is 1.0, metres) */ - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(7.394850528355765e-06, pg.x[0]); - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-0.00026090528338340325, pg.x[999]); - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-3.0302666148159694e-05, pg.y[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(7.394850528355765e-06, pg->x[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-0.00026090528338340325, pg->x[999]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(-3.0302666148159694e-05, pg->y[0]); + + /* position/z is a constant record sitting alongside the x and y datasets, so this also + * covers a record group that mixes the two forms. Bmad writes the constants with a one + * element array 'value' rather than a scalar. */ + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(0.0, pg->z[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(0.0, pg->z[999]); /* Momenta are scaled by the file's unitSI into eV/c. The values land ~7e-9 above the * raw file numbers because parcel's CLIGHT is 299792456 rather than 299792458, which is * why these need the relative tolerance rather than an exact comparison. */ - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(2029.7952043592982, pg.px[0]); - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(33557.29782907889, pg.py[0]); - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5002038554.641738, pg.pz[0]); - TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5001545000.86001, pg.pz[999]); - - /* pg.t is read but deliberately not asserted: parcel returns the 'time' record on its + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(2029.7952043592982, pg->px[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(33557.29782907889, pg->py[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5002038554.641738, pg->pz[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(5001545000.86001, pg->pz[999]); + + /* The remaining constant records, expanded across every particle */ + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(1.0000000000000002e-12, pg->weight[0]); + TEST_ASSERT_DOUBLE_CLOSE_DEFAULT(1.0000000000000002e-12, pg->weight[999]); + TEST_ASSERT_EQUAL_INT64(1, pg->status[0]); + TEST_ASSERT_EQUAL_INT64(1, pg->status[999]); + + /* pg->t is read but deliberately not asserted: parcel returns the 'time' record on its * own, while EXT_BeamPhysics.md defines absolute time as time + timeOffset and this * file carries the reference time in timeOffset. */ - /* The records backed by datasets were found */ + /* Every optional record the file carries was found */ TEST_ASSERT_TRUE(read_info.t_present); TEST_ASSERT_TRUE(read_info.px_present); TEST_ASSERT_TRUE(read_info.py_present); TEST_ASSERT_TRUE(read_info.pz_present); + TEST_ASSERT_TRUE(read_info.weight_present); + TEST_ASSERT_TRUE(read_info.status_present); /* The file carries no id record, so the index fallback fills it */ TEST_ASSERT_FALSE(read_info.id_present); - TEST_ASSERT_EQUAL_INT64(0, pg.id[0]); - TEST_ASSERT_EQUAL_INT64(999, pg.id[999]); + TEST_ASSERT_EQUAL_INT64(0, pg->id[0]); + TEST_ASSERT_EQUAL_INT64(999, pg->id[999]); - /* Clean up - the arrays are ours, so pmd_free_particle_group must not be used */ - free(pg.x); - free(pg.y); - free(pg.t); - free(pg.px); - free(pg.py); - free(pg.pz); - free(pg.id); + /* Clean up */ + pmd_free_particle_group(pg); pmd_close_iteration(iter); free(iterations); pmd_close_series(series);