From b8e0ac9f266c190540a3eec2c7f10bd7800fd268 Mon Sep 17 00:00:00 2001 From: Houjun Tang Date: Thu, 16 Apr 2026 21:08:52 -0700 Subject: [PATCH 1/2] Add explicit output format override --- README.md | 30 +++ convert.py | 311 +++++++++++++++++++---------- tests/test_convert_cli.py | 4 + tests/test_convert_fixture_data.py | 55 +++++ tests/test_convert_helpers.py | 26 +++ 5 files changed, 321 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 8c6434c..3470feb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,11 @@ mpirun -np 4 python convert.py ... - `--csv`: read node coordinates from a CSV file and write `csvNodeMotion.h5` - `--csv` with `--template`: use a CSV mapping file together with an SSI template file +When `--output-format` is omitted, the script keeps the current default for each input mode: +- DRM input defaults to OpenSees DRM output +- HDF5 and CSV inputs default to point-motion HDF5 output +- CSV + template input defaults to ESSI template output + ## Main Arguments ```text @@ -35,6 +40,7 @@ mpirun -np 4 python convert.py ... -h5, --hdf5 HDF5 file with node coordinates -e, --ssi SW4 SSI output file -t, --template SSI template file +-o, --output-format point, opensees, or essi -P, --savepath output directory -r, --reference reference coordinate offset -R, --rotateanlge rotation angle in degrees @@ -47,6 +53,8 @@ mpirun -np 4 python convert.py ... Notes: - `--timerange start end step` is in SSI time units. - If `start == end`, the script keeps all steps from `start` to the end of the SSI record. +- `--output-format point` is supported for DRM, HDF5, CSV, and template-driven inputs. +- `--output-format opensees` and `--output-format essi` require input that carries boundary-node metadata, so they are only supported for DRM or template-driven inputs. ## Examples @@ -70,6 +78,28 @@ mpirun -np 3 python convert.py \ -P tests/ ``` +Force point-motion output explicitly from an HDF5 node file: + +```bash +mpirun -np 3 python convert.py \ + -h5 template/h5NodeCrds.h5 \ + --ssi tests/data/small.ssi \ + -c template/motion_setting.csv \ + --output-format point \ + -P tests/ +``` + +Generate OpenSees DRM output from a template-driven run: + +```bash +mpirun -np 3 python convert.py \ + -c template/motion_setting.csv \ + -t template/DRMTemplate.h5drm \ + --ssi tests/data/small.ssi \ + --output-format opensees \ + -P tests/ +``` + Generate plots only: ```bash diff --git a/convert.py b/convert.py index 0119b5c..b478b07 100755 --- a/convert.py +++ b/convert.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # from genericpath import exists -import os -# import sys +import os +import shutil +# import sys import argparse import h5py import math @@ -51,11 +52,75 @@ def build_arg_parser(): type=float) parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true") parser.add_argument("-P", "--savepath", help="full path for saving the result files", default="") + parser.add_argument( + "-o", + "--output-format", + dest="output_format", + choices=("point", "opensees", "essi"), + help="output format override: point motions, OpenSees DRM, or ESSI template output", + default=None, + ) parser.add_argument("-z", "--zeroMotionDir", help="direction for zeroing out motion and enforce same motion across nodes in that direction: None(default), x, y, z", default="") return parser + +def resolve_output_mode(input_kind, requested_output_mode): + defaults = { + 'drm': 'opensees', + 'h5': 'point', + 'csv': 'point', + 'template': 'essi', + } + supported = { + 'drm': {'point', 'opensees', 'essi'}, + 'h5': {'point'}, + 'csv': {'point'}, + 'template': {'point', 'opensees', 'essi'}, + } + + if requested_output_mode is None: + return defaults[input_kind] + + output_mode = requested_output_mode.lower() + if output_mode not in supported[input_kind]: + supported_modes = ', '.join(sorted(supported[input_kind])) + raise ValueError( + f'Output format "{output_mode}" is not supported for {input_kind} input. ' + f'Supported formats: {supported_modes}.' + ) + return output_mode + + +def get_output_filename(input_kind, output_mode, save_path, source_fname, explicit_output_mode=False): + if output_mode == 'opensees': + return os.path.join(save_path, 'OpenSeesDRMinput.h5drm') + + if output_mode == 'point': + if input_kind == 'h5': + output_stem = os.path.splitext(os.path.basename(source_fname))[0] + return os.path.join(save_path, f'{output_stem}_motion.h5') + if input_kind == 'csv': + return os.path.join(save_path, 'csvNodeMotion.h5') + if input_kind == 'drm': + return os.path.join(save_path, 'drmNodeMotion.h5') + if input_kind == 'template': + output_stem = os.path.splitext(os.path.basename(source_fname))[0] + return os.path.join(save_path, f'{output_stem}_motion.h5') + + if output_mode == 'essi': + if input_kind == 'template' and not explicit_output_mode: + return source_fname + return os.path.join(save_path, os.path.basename(source_fname)) + + raise ValueError(f'Unsupported output format: {output_mode}') + + +def prepare_essi_output_file(source_fname, output_fname): + if os.path.abspath(source_fname) != os.path.abspath(output_fname): + shutil.copyfile(source_fname, output_fname) + # from scipy.signal import butter,filtfilt # def butter_lowpass_filter(data, cutoff, nyq, order): # normal_cutoff = cutoff / nyq @@ -722,9 +787,13 @@ def create_hdf5_csv(h5_fname, ncoord, tsteprange, essi_dt, gen_vel, gen_acc, gen def create_hdf5_essi(h5_fname, ncoord, nstep, dt, gen_vel, gen_acc, gen_dis, extra_dname): h5file = h5py.File(h5_fname, 'r+') - - if gen_vel: - dset = h5file.create_dataset('Velocity', (ncoord*3, nstep), dtype='f4') + + for dname in ('Velocity', 'Accelerations', 'Displacements', 'Time'): + if dname in h5file: + del h5file[dname] + + if gen_vel: + dset = h5file.create_dataset('Velocity', (ncoord*3, nstep), dtype='f4') if gen_acc: dset = h5file.create_dataset('Accelerations', (ncoord*3, nstep), dtype='f4') if gen_dis: @@ -1377,7 +1446,7 @@ def generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, use # Write coordinates and boundary nodes (file created previously), in serial with baton passing comm.Barrier() - if output_format == "OpenSees": + if output_format == "opensees": if mpi_rank == 0: create_hdf5_opensees(output_fname, n_coord, tsteprange, essi_dt, gen_vel, gen_acc, gen_dis, extra_dname) @@ -1409,7 +1478,7 @@ def generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, use if mpi_rank != mpi_size-1: comm.send(my_ncoord, dest=mpi_rank+1, tag=11) - elif output_format == "csv" or output_format == "h5": + elif output_format == "point": if mpi_rank == 0: create_hdf5_csv(output_fname, n_coord, tsteprange, essi_dt, gen_vel, gen_acc, gen_dis, extra_dname) @@ -1441,7 +1510,7 @@ def generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, use if mpi_rank != mpi_size-1: comm.send(my_ncoord, dest=mpi_rank+1, tag=11) - elif output_format == "ESSI": + elif output_format == "essi": coords_start, coords_end = get_flat_coord_range(my_offset, my_ncoord[0]) if mpi_rank == 0: create_hdf5_essi(output_fname, n_coord, nsteps, dt, gen_vel, gen_acc, gen_dis, extra_dname) @@ -1481,67 +1550,79 @@ def generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, use print('Rank', mpi_rank, 'Finished writing data') return -def convert_drm(drm_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose): - if mpi_rank == 0: - print('Input DRM [%s]' %drm_fname) - print('Input ESSI [%s]' %ssi_fname) - - coord_sys = ['y', 'x', '-z'] - - # original unrotated node coordinates - user_x0, user_y0, user_z0, n_coord, isboundary = read_coord_drm(drm_fname, verbose) - if verbose and mpi_rank == 0: - print('Done read %d coordinates, first is (%d, %d, %d), last is (%d, %d, %d)' % (n_coord, user_x0[0], user_y0[0], user_z0[0], user_x0[-1], user_y0[-1], user_z0[-1])) - print('x, y, z (min/max): (%.0f, %.0f), (%.0f, %.0f), (%.0f, %.0f)' % (np.min(user_x0), np.max(user_x0), np.min(user_y0), np.max(user_y0), np.min(user_z0), np.max(user_z0)) ) - - gen_vel = False - gen_dis = True - gen_acc = True - extra_dname = 'internal' - - output_format = 'OpenSees' - output_fname = save_path + '/' + output_format + 'DRMinput.h5drm' - - generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, isboundary, extra_dname, output_format) - - return - -def convert_h5(h5_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose): - if mpi_rank == 0: - print('Input h5 [%s]' %h5_fname) - print('Input ESSI [%s]' %ssi_fname) - - coord_sys = ['y', 'x', '-z'] - gen_vel = True - gen_dis = True - gen_acc = True +def convert_drm(drm_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose, requested_output_mode=None): + if mpi_rank == 0: + print('Input DRM [%s]' %drm_fname) + print('Input ESSI [%s]' %ssi_fname) + + coord_sys = ['y', 'x', '-z'] + output_format = resolve_output_mode('drm', requested_output_mode) + + # original unrotated node coordinates + user_x0, user_y0, user_z0, n_coord, isboundary = read_coord_drm(drm_fname, verbose) + isboundary = np.asarray(isboundary).reshape(-1) + if verbose and mpi_rank == 0: + print('Done read %d coordinates, first is (%d, %d, %d), last is (%d, %d, %d)' % (n_coord, user_x0[0], user_y0[0], user_z0[0], user_x0[-1], user_y0[-1], user_z0[-1])) + print('x, y, z (min/max): (%.0f, %.0f), (%.0f, %.0f), (%.0f, %.0f)' % (np.min(user_x0), np.max(user_x0), np.min(user_y0), np.max(user_y0), np.min(user_z0), np.max(user_z0)) ) + + if output_format == 'opensees': + gen_vel = False + gen_dis = True + gen_acc = True + extra_dname = 'internal' + else: + gen_vel = True + gen_dis = True + gen_acc = True + extra_dname = 'Is Boundary Node' + + output_fname = get_output_filename( + 'drm', output_format, save_path, drm_fname, explicit_output_mode=requested_output_mode is not None + ) + if output_format == 'essi': + prepare_essi_output_file(drm_fname, output_fname) + + generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, isboundary, extra_dname, output_format) + + return + +def convert_h5(h5_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose, requested_output_mode=None): + if mpi_rank == 0: + print('Input h5 [%s]' %h5_fname) + print('Input ESSI [%s]' %ssi_fname) + + coord_sys = ['y', 'x', '-z'] + output_format = resolve_output_mode('h5', requested_output_mode) + gen_vel = True + gen_dis = True + gen_acc = True extra_dname = 'nodeTag' # original unrotated node coordinates user_x0, user_y0, user_z0, n_coord, node_tags = read_coord_h5(h5_fname, verbose) n_coord = len(node_tags) - if mpi_rank == 0: - print('Generating motions for %i nodes...' % (n_coord)) - - output_format = 'h5' - # output_fname = save_path + '/' + output_format + 'NodeMotion.h5' - output_stem = os.path.splitext(os.path.basename(h5_fname))[0] - output_fname = save_path + '/' + output_stem + '_motion.h5' - - generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, node_tags, extra_dname, output_format) - - return - -def convert_csv(csv_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose): - if mpi_rank == 0: - print('Input CSV [%s]' %csv_fname) - print('Input ESSI [%s]' %ssi_fname) - - coord_sys = ['y', 'x', '-z'] - gen_vel = True - gen_dis = True - gen_acc = True + if mpi_rank == 0: + print('Generating motions for %i nodes...' % (n_coord)) + + output_fname = get_output_filename( + 'h5', output_format, save_path, h5_fname, explicit_output_mode=requested_output_mode is not None + ) + + generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, node_tags, extra_dname, output_format) + + return + +def convert_csv(csv_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose, requested_output_mode=None): + if mpi_rank == 0: + print('Input CSV [%s]' %csv_fname) + print('Input ESSI [%s]' %ssi_fname) + + coord_sys = ['y', 'x', '-z'] + output_format = resolve_output_mode('csv', requested_output_mode) + gen_vel = True + gen_dis = True + gen_acc = True extra_dname = 'nodeTag' # original unrotated node coordinates @@ -1556,21 +1637,22 @@ def convert_csv(csv_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tste user_y0[i] = df.loc[i, 'y'] user_z0[i] = df.loc[i, 'z'] - if mpi_rank == 0: - print('Generating motions for %i nodes...' % (n_coord)) - - output_format = 'csv' - output_fname = save_path + '/' + output_format + 'NodeMotion.h5' - - generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, node_tags, extra_dname, output_format) - + if mpi_rank == 0: + print('Generating motions for %i nodes...' % (n_coord)) + + output_fname = get_output_filename( + 'csv', output_format, save_path, csv_fname, explicit_output_mode=requested_output_mode is not None + ) + + generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, node_tags, extra_dname, output_format) + return def dframeToDict(dFrame): dFrame = list(dFrame.iterrows()) return {i[1].to_list()[0] : i[1].to_list()[1] for i in dFrame} -def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose, ref_coord=None): +def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plot_only, mpi_rank, mpi_size, verbose, ref_coord=None, requested_output_mode=None): if mpi_rank == 0: print('Input CSV [%s]' %csv_fname) print('Input ESSI [%s]' %ssi_fname) @@ -1578,6 +1660,8 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep if ref_coord is None: ref_coord = np.zeros(3) + output_format = resolve_output_mode('template', requested_output_mode) + sw4ToESSI_params = dframeToDict(pd.read_csv(csv_fname)) sw4_i_start = sw4ToESSI_params["sw4_i_start"] sw4_i_end = sw4ToESSI_params["sw4_i_end"] @@ -1597,18 +1681,23 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep ref_coord[1] = essi_y_start ref_coord[2] = essi_z_start - coord_sys = ['y', 'x', '-z'] - gen_vel = True - gen_dis = True - gen_acc = True - extra_dname = 'Is Boundary Node' - - # original unrotated node coordinates - output_fname = template_fname - template_file = h5py.File(template_fname) - coordinates = template_file['Coordinates'][:] - - is_boundary = template_file['Is Boundary Node'][:] + coord_sys = ['y', 'x', '-z'] + if output_format == 'opensees': + gen_vel = False + gen_dis = True + gen_acc = True + extra_dname = 'internal' + else: + gen_vel = True + gen_dis = True + gen_acc = True + extra_dname = 'Is Boundary Node' + + # original unrotated node coordinates + template_file = h5py.File(template_fname) + coordinates = template_file['Coordinates'][:] + + is_boundary = np.asarray(template_file['Is Boundary Node'][:]).reshape(-1) n_coord = len(is_boundary) user_x = np.zeros(n_coord) user_y = np.zeros(n_coord) @@ -1625,7 +1714,11 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep print('x, y, z (min/max): (%.0f, %.0f), (%.0f, %.0f), (%.0f, %.0f)' % (np.min(user_x), np.max(user_x), np.min(user_y), np.max(user_y), np.min(user_z), np.max(user_z)) ) print('Start/end time', start_t, end_t) - output_format = 'ESSI' + output_fname = get_output_filename( + 'template', output_format, save_path, template_fname, explicit_output_mode=requested_output_mode is not None + ) + if output_format == 'essi': + prepare_essi_output_file(template_fname, output_fname) generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x, user_y, user_z, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, is_boundary, extra_dname, output_format) return @@ -1650,8 +1743,9 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep start_t=0 end_t=0 tstep = 1 - rotate_angle = 0 - zeroMotionDir = 'None' + rotate_angle = 0 + zeroMotionDir = 'None' + requested_output_mode = None parser = build_arg_parser() args = parser.parse_args() @@ -1689,10 +1783,12 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep tstep = args.timerange[2] # time step interval if args.rotateanlge: rotate_angle = args.rotateanlge - if args.savepath: - save_path = args.savepath - if args.zeroMotionDir: - zeroMotionDir = args.zeroMotionDir + if args.savepath: + save_path = args.savepath + if args.zeroMotionDir: + zeroMotionDir = args.zeroMotionDir + if args.output_format: + requested_output_mode = args.output_format comm = MPI.COMM_WORLD mpi_size = comm.Get_size() @@ -1706,24 +1802,29 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep startTimeStr = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(startTime)) print('Start time:', startTimeStr) - if drm_fname == '' and csv_fname == '' and template_fname == '': - print('Error, no node coordinate input file is provided, exit...') - exit(0) + if drm_fname == '' and h5_fname == '' and csv_fname == '' and template_fname == '': + print('Error, no node coordinate input file is provided, exit...') + exit(0) if ssi_fname == '': print('Error, no SW4 SSI output file is provided, exit...') exit(0) - if verbose and mpi_rank == 0: - print('Using ref_coord={}, start_t={}, end_t={}, tstep={}, rotate_angle={} to extract motions'.format(ref_coord, start_t, end_t, tstep, rotate_angle)) - - if use_drm: - convert_drm(drm_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose) - elif use_h5: - convert_h5(h5_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose) - elif use_csv and not use_template: - convert_csv(csv_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose) - elif use_csv and use_template: - convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose, ref_coord) + if verbose and mpi_rank == 0: + print('Using ref_coord={}, start_t={}, end_t={}, tstep={}, rotate_angle={}, output_mode={} to extract motions'.format(ref_coord, start_t, end_t, tstep, rotate_angle, requested_output_mode or 'default')) + + try: + if use_drm: + convert_drm(drm_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose, requested_output_mode) + elif use_h5: + convert_h5(h5_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose, requested_output_mode) + elif use_csv and not use_template: + convert_csv(csv_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose, requested_output_mode) + elif use_csv and use_template: + convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep, rotate_angle, zeroMotionDir, plotonly, mpi_rank, mpi_size, verbose, ref_coord, requested_output_mode) + except ValueError as exc: + if mpi_rank == 0: + print(f'Error: {exc}') + raise SystemExit(1) if mpi_rank == 0: endTime = time.time() diff --git a/tests/test_convert_cli.py b/tests/test_convert_cli.py index 1356c6c..9225518 100644 --- a/tests/test_convert_cli.py +++ b/tests/test_convert_cli.py @@ -31,6 +31,10 @@ def test_legacy_essi_flag_maps_to_ssi_destination(self): args = self.parser.parse_args(["--essi", str(self.sample_ssi)]) self.assertEqual(args.ssi, str(self.sample_ssi)) + def test_output_format_flag_accepts_point_override(self): + args = self.parser.parse_args(["--ssi", str(self.sample_ssi), "--output-format", "point"]) + self.assertEqual(args.output_format, "point") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_convert_fixture_data.py b/tests/test_convert_fixture_data.py index 8e9d6bd..547688c 100644 --- a/tests/test_convert_fixture_data.py +++ b/tests/test_convert_fixture_data.py @@ -85,6 +85,61 @@ def test_convert_h5_matches_checked_in_fixture_data(self): expected_tend = float(output_h5["dt"][()]) * (output_h5["acceleration"].shape[1] - 1) self.assertAlmostEqual(float(output_h5["tend"][()]), expected_tend) + def test_convert_h5_accepts_explicit_point_output_mode(self): + with tempfile.TemporaryDirectory() as tmpdir: + ref_coord, start_t, end_t, tstep, rotate_angle, zero_motion_dir = convert.get_csv_meta( + str(self.sample_csv) + ) + + convert.convert_h5( + str(self.sample_h5), + str(self.sample_ssi), + tmpdir, + ref_coord, + start_t, + end_t, + tstep, + rotate_angle, + zero_motion_dir, + False, + 0, + 1, + False, + requested_output_mode="point", + ) + + output_path = Path(tmpdir) / "h5NodeCrds_motion.h5" + self.assertTrue(output_path.exists(), f"Missing output file: {output_path}") + + with h5py.File(output_path, "r") as output_h5, h5py.File(self.reference_output, "r") as reference_h5: + np.testing.assert_allclose(output_h5["acceleration"][:], reference_h5["acceleration"][:]) + np.testing.assert_allclose(output_h5["xyz"][:], reference_h5["xyz"][:]) + np.testing.assert_array_equal(output_h5["nodeTag"][:], reference_h5["nodeTag"][:]) + + def test_convert_h5_rejects_unsupported_essi_output_mode(self): + with tempfile.TemporaryDirectory() as tmpdir: + ref_coord, start_t, end_t, tstep, rotate_angle, zero_motion_dir = convert.get_csv_meta( + str(self.sample_csv) + ) + + with self.assertRaisesRegex(ValueError, 'not supported for h5 input'): + convert.convert_h5( + str(self.sample_h5), + str(self.sample_ssi), + tmpdir, + ref_coord, + start_t, + end_t, + tstep, + rotate_angle, + zero_motion_dir, + False, + 0, + 1, + False, + requested_output_mode="essi", + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_convert_helpers.py b/tests/test_convert_helpers.py index 98699a2..ba5c56c 100644 --- a/tests/test_convert_helpers.py +++ b/tests/test_convert_helpers.py @@ -20,10 +20,36 @@ class ConvertHelperTests(unittest.TestCase): + def test_resolve_output_mode_keeps_existing_defaults(self): + self.assertEqual(convert.resolve_output_mode("drm", None), "opensees") + self.assertEqual(convert.resolve_output_mode("h5", None), "point") + self.assertEqual(convert.resolve_output_mode("csv", None), "point") + self.assertEqual(convert.resolve_output_mode("template", None), "essi") + + def test_resolve_output_mode_rejects_unsupported_input_output_pairs(self): + with self.assertRaisesRegex(ValueError, 'not supported for h5 input'): + convert.resolve_output_mode("h5", "essi") + with self.assertRaisesRegex(ValueError, 'not supported for csv input'): + convert.resolve_output_mode("csv", "opensees") + def test_get_flat_coord_range_scales_node_offsets_by_xyz_width(self): self.assertEqual(convert.get_flat_coord_range(0, 2), (0, 6)) self.assertEqual(convert.get_flat_coord_range(2, 3), (6, 15)) + def test_get_output_filename_uses_requested_mode_and_input_kind(self): + self.assertEqual( + convert.get_output_filename("drm", "point", "/tmp/out", "/tmp/in/template.h5drm"), + "/tmp/out/drmNodeMotion.h5", + ) + self.assertEqual( + convert.get_output_filename("template", "essi", "/tmp/out", "/tmp/in/template.h5drm", explicit_output_mode=False), + "/tmp/in/template.h5drm", + ) + self.assertEqual( + convert.get_output_filename("template", "essi", "/tmp/out", "/tmp/in/template.h5drm", explicit_output_mode=True), + "/tmp/out/template.h5drm", + ) + def test_create_hdf5_essi_writes_float_time_axis_matching_motion_length(self): with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "template.h5" From 54548af39222987a8e48d4cbfa3f1d9eaa77fe5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:41:53 +0000 Subject: [PATCH 2/2] Fix MPI race when preparing ESSI output files Agent-Logs-Url: https://github.com/houjun/sw4essi_converter/sessions/6072eacf-a74d-47f3-9bb3-4f070c2e3e1c Co-authored-by: houjun <3237466+houjun@users.noreply.github.com> --- convert.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/convert.py b/convert.py index b478b07..16428e7 100755 --- a/convert.py +++ b/convert.py @@ -1580,7 +1580,9 @@ def convert_drm(drm_fname, ssi_fname, save_path, ref_coord, start_t, end_t, tste 'drm', output_format, save_path, drm_fname, explicit_output_mode=requested_output_mode is not None ) if output_format == 'essi': - prepare_essi_output_file(drm_fname, output_fname) + if mpi_rank == 0: + prepare_essi_output_file(drm_fname, output_fname) + MPI.COMM_WORLD.Barrier() generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x0, user_y0, user_z0, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir,gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, isboundary, extra_dname, output_format) @@ -1718,7 +1720,9 @@ def convert_template(csv_fname, template_fname, ssi_fname, start_t, end_t, tstep 'template', output_format, save_path, template_fname, explicit_output_mode=requested_output_mode is not None ) if output_format == 'essi': - prepare_essi_output_file(template_fname, output_fname) + if mpi_rank == 0: + prepare_essi_output_file(template_fname, output_fname) + MPI.COMM_WORLD.Barrier() generate_acc_dis_time(ssi_fname, coord_sys, ref_coord, user_x, user_y, user_z, n_coord, start_t, end_t, tstep, rotate_angle, zeroMotionDir, gen_vel, gen_acc, gen_dis, verbose, plot_only, output_fname, mpi_rank, mpi_size, is_boundary, extra_dname, output_format) return