Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
pull_request:

jobs:
cli-parser-test:
unit-tests:
runs-on: ubuntu-latest

steps:
Expand All @@ -19,10 +19,46 @@ jobs:
with:
python-version: "3.11"

- name: Install MPI Runtime
run: |
sudo apt-get update
sudo apt-get install -y openmpi-bin libopenmpi-dev

- name: Install Test Dependencies
run: |
python -m pip install --upgrade pip
python -m pip install numpy scipy h5py pandas matplotlib
python -m pip install numpy scipy h5py pandas matplotlib mpi4py hdf5plugin

- name: Run Unit Tests
run: python -m unittest discover -s tests -p "test_*.py"

- name: Run MPI Fixture Conversion Test
env:
MPLCONFIGDIR: ${{ runner.temp }}/mplconfig
run: |
mkdir -p "$MPLCONFIGDIR" "$RUNNER_TEMP/convert-out"
mpirun -n 1 python convert.py \
-h5 template/h5NodeCrds.h5 \
--ssi tests/data/small.ssi \
-c template/motion_setting.csv \
-P "$RUNNER_TEMP/convert-out"
python - <<'PY'
import os
import h5py
import numpy as np

output_path = os.path.join(os.environ["RUNNER_TEMP"], "convert-out", "h5NodeCrds_motion.h5")
reference_path = "tests/data/h5NodeMotion.h5"

with h5py.File(output_path, "r") as output_h5, h5py.File(reference_path, "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"][:])
assert output_h5["velocity"].shape == (108, 600)
assert output_h5["displacement"].shape == (108, 600)

- name: Run CLI Parser Test
run: python -m unittest tests.test_convert_cli
dt = float(output_h5["dt"][()])
tend = float(output_h5["tend"][()])
expected_tend = dt * (output_h5["acceleration"].shape[1] - 1)
assert abs(tend - expected_tend) < 1e-12
PY
64 changes: 34 additions & 30 deletions convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,18 +651,21 @@ def write_to_hdf5_range_1d(h5_fname, gname, dname, data, mystart, myend):
dset[mystart:myend] = data[:]
h5file.close()

def write_to_hdf5_range_2d(h5_fname, gname, dname, data, mystart, myend):
h5file = h5py.File(h5_fname, 'r+')
if gname == '/':
dset = h5file[dname]
def write_to_hdf5_range_2d(h5_fname, gname, dname, data, mystart, myend):
h5file = h5py.File(h5_fname, 'r+')
if gname == '/':
dset = h5file[dname]
else:
grp = h5file[gname]
dset = grp[dname]

#print('mystart=%d, myend=%d' %(mystart, myend))
dset[mystart:myend,:] = data[:,:]
h5file.close()

#print('mystart=%d, myend=%d' %(mystart, myend))
dset[mystart:myend,:] = data[:,:]
h5file.close()

def get_flat_coord_range(coord_offset, ncoord, width=3):
return coord_offset * width, (coord_offset + ncoord) * width

def create_hdf5_opensees(h5_fname, ncoord, tsteprange, essi_dt, gen_vel, gen_acc, gen_dis, extra_dname):
tstep = tsteprange.step
nstep = len(tsteprange)
Expand Down Expand Up @@ -717,8 +720,8 @@ def create_hdf5_csv(h5_fname, ncoord, tsteprange, essi_dt, gen_vel, gen_acc, gen

h5file.close()

def create_hdf5_essi(h5_fname, ncoord, nstep, dt, gen_vel, gen_acc, gen_dis, extra_dname):
h5file = h5py.File(h5_fname, 'r+')
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')
Expand All @@ -727,9 +730,9 @@ def create_hdf5_essi(h5_fname, ncoord, nstep, dt, gen_vel, gen_acc, gen_dis, ext
if gen_dis:
dset = h5file.create_dataset('Displacements', (ncoord*3, nstep), dtype='f4')

timeseq = np.linspace(0, nstep*dt, nstep+1)
h5file.create_dataset('Time', data=timeseq, dtype='i4')
timeseq = np.arange(nstep, dtype='f8') * dt

h5file.create_dataset('Time', data=timeseq, dtype='f8')

h5file.close()

Expand Down Expand Up @@ -1438,29 +1441,30 @@ 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":
if mpi_rank == 0:
create_hdf5_essi(output_fname, n_coord, nsteps, dt, gen_vel, gen_acc, gen_dis, extra_dname)
# Write to the template file
if my_ncoord[0] > 0:
write_to_hdf5_range_1d(output_fname, '/', 'Coordinates', my_user_coordinates.reshape(my_ncoord[0]*3), my_offset, (my_offset+my_ncoord[0])*3)
write_to_hdf5_range_1d(output_fname, '/', extra_dname, is_boundary, my_offset, my_offset+my_ncoord[0])
if gen_acc:
write_to_hdf5_range(output_fname, '/', 'Accelerations', output_acc_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
if gen_dis:
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)
# Write to the template file
if my_ncoord[0] > 0:
write_to_hdf5_range_1d(output_fname, '/', 'Coordinates', my_user_coordinates.reshape(my_ncoord[0]*3), coords_start, coords_end)
write_to_hdf5_range_1d(output_fname, '/', extra_dname, is_boundary, my_offset, my_offset+my_ncoord[0])
if gen_acc:
write_to_hdf5_range(output_fname, '/', 'Accelerations', output_acc_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
if gen_dis:
write_to_hdf5_range(output_fname, '/', 'Displacements', output_dis_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
if gen_vel:
write_to_hdf5_range(output_fname, '/', 'Velocity', output_vel_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)

if mpi_size > 1:
comm.send(my_ncoord, dest=1, tag=111)
else:
data = comm.recv(source=mpi_rank-1, tag=111)
if my_ncoord[0] > 0:
write_to_hdf5_range_1d(output_fname, '/', 'Coordinates', my_user_coordinates.reshape(my_ncoord[0]*3), my_offset, (my_offset+my_ncoord[0])*3)
write_to_hdf5_range_1d(output_fname, '/', extra_dname, is_boundary, my_offset, my_offset+my_ncoord[0])
if gen_acc:
write_to_hdf5_range(output_fname, '/', 'Accelerations', output_acc_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
else:
data = comm.recv(source=mpi_rank-1, tag=111)
if my_ncoord[0] > 0:
write_to_hdf5_range_1d(output_fname, '/', 'Coordinates', my_user_coordinates.reshape(my_ncoord[0]*3), coords_start, coords_end)
write_to_hdf5_range_1d(output_fname, '/', extra_dname, is_boundary, my_offset, my_offset+my_ncoord[0])
if gen_acc:
write_to_hdf5_range(output_fname, '/', 'Accelerations', output_acc_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
if gen_dis:
write_to_hdf5_range(output_fname, '/', 'Displacements', output_dis_all[:,tsteprange], my_offset*3, (my_offset+my_ncoord[0])*3)
if gen_vel:
Expand Down
90 changes: 90 additions & 0 deletions tests/test_convert_fixture_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import os
import sys
import tempfile
import types
import unittest
from pathlib import Path

import h5py
import numpy as np


MPLCONFIGDIR = Path(tempfile.gettempdir()) / "mplconfig-convert-tests"
MPLCONFIGDIR.mkdir(exist_ok=True)
os.environ.setdefault("MPLCONFIGDIR", str(MPLCONFIGDIR))
os.environ.setdefault("XDG_CACHE_HOME", tempfile.gettempdir())

REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))


class _FakeComm:
def Barrier(self):
return None

def Allgather(self, sendbuf, recvbuf):
recvbuf[0][0] = sendbuf[0][0]

def send(self, *_args, **_kwargs):
raise AssertionError("send() should not be called in serial tests")

def recv(self, *_args, **_kwargs):
raise AssertionError("recv() should not be called in serial tests")


mpi4py_stub = types.ModuleType("mpi4py")
mpi4py_stub.MPI = types.SimpleNamespace(COMM_WORLD=_FakeComm(), INT=object())
sys.modules.setdefault("mpi4py", mpi4py_stub)
sys.modules.setdefault("hdf5plugin", types.ModuleType("hdf5plugin"))

import convert


class ConvertFixtureDataTests(unittest.TestCase):
def setUp(self):
convert.MPI = mpi4py_stub.MPI
self.sample_ssi = REPO_ROOT / "tests" / "data" / "small.ssi"
self.sample_h5 = REPO_ROOT / "template" / "h5NodeCrds.h5"
self.sample_csv = REPO_ROOT / "template" / "motion_setting.csv"
self.reference_output = REPO_ROOT / "tests" / "data" / "h5NodeMotion.h5"

def test_convert_h5_matches_checked_in_fixture_data(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,
)

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:
self.assertEqual(output_h5["velocity"].shape, (108, 600))
self.assertEqual(output_h5["displacement"].shape, (108, 600))
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"][:])
self.assertEqual(float(output_h5["dt"][()]), float(reference_h5["dt"][()]))
self.assertEqual(float(output_h5["tstart"][()]), float(reference_h5["tstart"][()]))

expected_tend = float(output_h5["dt"][()]) * (output_h5["acceleration"].shape[1] - 1)
self.assertAlmostEqual(float(output_h5["tend"][()]), expected_tend)


if __name__ == "__main__":
unittest.main()
54 changes: 54 additions & 0 deletions tests/test_convert_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import sys
import tempfile
import types
import unittest
from pathlib import Path

import h5py
import numpy as np


REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT))

mpi4py_stub = types.ModuleType("mpi4py")
mpi4py_stub.MPI = types.SimpleNamespace()
sys.modules.setdefault("mpi4py", mpi4py_stub)
sys.modules.setdefault("hdf5plugin", types.ModuleType("hdf5plugin"))

import convert


class ConvertHelperTests(unittest.TestCase):
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_create_hdf5_essi_writes_float_time_axis_matching_motion_length(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "template.h5"
with h5py.File(path, "w"):
pass

convert.create_hdf5_essi(
str(path),
ncoord=2,
nstep=3,
dt=0.25,
gen_vel=True,
gen_acc=True,
gen_dis=True,
extra_dname="unused",
)

with h5py.File(path, "r") as h5file:
self.assertEqual(h5file["Velocity"].shape, (6, 3))
self.assertEqual(h5file["Accelerations"].shape, (6, 3))
self.assertEqual(h5file["Displacements"].shape, (6, 3))
self.assertEqual(h5file["Time"].shape, (3,))
self.assertEqual(h5file["Time"].dtype, np.dtype("float64"))
np.testing.assert_allclose(h5file["Time"][:], [0.0, 0.25, 0.5])


if __name__ == "__main__":
unittest.main()
Loading