From 4838f5c6c14d8aff8dee75124566ead3365605f0 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Wed, 22 Jul 2026 09:20:40 +0000 Subject: [PATCH 01/10] FDTD | Feature | Add XDMF movie and frequency outputs --- src_output/frequencySliceProbeOutput.F90 | 340 ++++++++++++++ src_output/movieProbeOutput.F90 | 478 +++++++++++++++++++ src_output/xdmfAPI.F90 | 561 +++++++++++++++++++++++ 3 files changed, 1379 insertions(+) create mode 100644 src_output/frequencySliceProbeOutput.F90 create mode 100644 src_output/movieProbeOutput.F90 create mode 100644 src_output/xdmfAPI.F90 diff --git a/src_output/frequencySliceProbeOutput.F90 b/src_output/frequencySliceProbeOutput.F90 new file mode 100644 index 00000000..128c7598 --- /dev/null +++ b/src_output/frequencySliceProbeOutput.F90 @@ -0,0 +1,340 @@ +module frequencySliceProbeOutput_m + use FDETYPES_m + use utils_m + use report_m + use outputTypes_m + use outputUtils_m + use volumicProbeUtils_m + use directoryUtils_m + use xdmfAPI_m + implicit none + private + + !=========================== + ! Public interface summary + !=========================== + public :: init_frequency_slice_probe_output + public :: update_frequency_slice_probe_output + public :: flush_frequency_slice_probe_output + public :: close_frequency_slice_probe_output + !=========================== + + !=========================== + ! Private interface summary + !=========================== + private :: save_field + private :: save_field_module + private :: save_field_component + private :: save_current + private :: save_current_module + private :: save_current_component + !=========================== + + !=========================== + +contains + + subroutine init_frequency_slice_probe_output(this, lowerBound, upperBound, timeInterval, field, domain, outputTypeExtension, control, problemInfo) + type(frequency_slice_probe_output_t), intent(out) :: this + type(cell_coordinate_t), intent(in) :: lowerBound, upperBound + real(kind=RKIND_tiempo), intent(in) :: timeInterval + integer(kind=SINGLE), intent(in) :: field + type(domain_t), intent(in) :: domain + character(len=BUFSIZE), intent(in) :: outputTypeExtension + type(sim_control_t), intent(in) :: control + type(problem_info_t), intent(in) :: problemInfo + + integer :: i + integer :: error + character(len=BUFSIZE) :: filename + + this%mainCoords = lowerBound + this%auxCoords = upperBound + this%component = field !This can refer to electric, magnetic or currentDensity + this%domain = domain + this%nFreq = domain%fnum + + call alloc_and_init(this%frequencySlice, this%nFreq, 0.0_RKIND) + do i = 1, this%nFreq + call init_frequency_slice(this%frequencySlice, this%domain) + end do + + call find_and_store_important_coords(this%mainCoords, this%auxCoords, this%component, problemInfo, this%nPoints, this%coords) + + call alloc_and_init(this%xValueForFreq, this%nFreq, this%nPoints, (0.0_CKIND, 0.0_CKIND)) + call alloc_and_init(this%yValueForFreq, this%nFreq, this%nPoints, (0.0_CKIND, 0.0_CKIND)) + call alloc_and_init(this%zValueForFreq, this%nFreq, this%nPoints, (0.0_CKIND, 0.0_CKIND)) + + call alloc_and_init(this%auxExp_E, this%nFreq, (0.0_CKIND, 0.0_CKIND)) + call alloc_and_init(this%auxExp_H, this%nFreq, (0.0_CKIND, 0.0_CKIND)) + + do i = 1, this%nFreq + this%auxExp_E(i) = timeInterval*(1.0E0_RKIND, 0.0E0_RKIND)*Exp(mcpi2*this%frequencySlice(i)) ! the dt should be some kind of average + this%auxExp_H(i) = this%auxExp_E(i)*Exp(mcpi2*this%frequencySlice(i)*timeInterval*0.5_RKIND) + end do + + this%path = get_output_path_freq(this, outputTypeExtension, field, control) + + filename = get_last_component(this%path) + this%filesPath = join_path(this%path, filename) + + call create_folder(this%path, error) + call create_bin_file(this%filesPath, error) + end subroutine init_frequency_slice_probe_output + + subroutine create_bin_file(filePath, error) + character(len=*), intent(in) :: filePath + integer, intent(out) :: error + call create_file_with_path(add_extension(filePath, binaryExtension), error) + end subroutine + + subroutine write_bin_file(this) + ! Check type definition for binary format + type(frequency_slice_probe_output_t), intent(inout) :: this + integer :: i, f, unit + !We rewrite the binary as simulation continues + open (unit=unit, file=add_extension(this%filesPath, binaryExtension), & + status='old', form='unformatted', action='write', access='stream') + do f = 1, this%nFreq + do i = 1, this%nPoints + write(unit) this%frequencySlice(f), this%coords(1,i), this%coords(2,i), this%coords(3,i), this%xValueForFreq(f,i), this%yValueForFreq(f,i), this%xValueForFreq(f,i) + end do + end do + flush (unit) + close (unit) + end subroutine + + subroutine write_to_xdmf_h5(this) + !If we call to this subrutine it will always replace old values + type(frequency_slice_probe_output_t), intent(inout) :: this + + integer(HID_T) :: file_id + integer :: f, error, xdmfunit + real(dp), allocatable, dimension(:, :) :: coordsReal + character(len=256) :: h5_filename, h5_filepath + character(len=256) :: xdmf_filename + character(len=10) :: dimension_string + character(len=10) :: nCoordsString + character(len=14) :: stepName + h5_filepath = add_extension(this%filesPath, ".h5") + h5_filename = get_last_component(h5_filepath) + + call H5open_f(error) + call H5Fopen_f(trim(h5_filepath), H5F_ACC_RDWR_F, file_id, error) + write(dimension_string, '(I0,1X,I0)') this%nPoints, this%nFreq + write(nCoordsString, '(I0, I0)') this%nPoints, 1 + do f = 1, this%nFreq + write(stepName, '((I5.5))') f + call h5_append_rows_to_dataset(file_id, "xVal", reshape(real(abs(this%xValueForFreq(f, :)), dp), [1, this%nPoints])) + call h5_append_rows_to_dataset(file_id, "yVal", reshape(real(abs(this%yValueForFreq(f, :)), dp), [1, this%nPoints])) + call h5_append_rows_to_dataset(file_id, "zVal", reshape(real(abs(this%zValueForFreq(f, :)), dp), [1, this%nPoints])) + end do + call H5Fclose_f(file_id, error) + call H5close_f(error) + end subroutine + + function get_output_path_freq(this, outputTypeExtension, field, control) result(outputPath) + type(frequency_slice_probe_output_t), intent(in) :: this + character(len=*), intent(in) :: outputTypeExtension + integer(kind=SINGLE), intent(in) :: field + type(sim_control_t), intent(in) :: control + character(len=BUFSIZE) :: probeBoundsExtension, prefixFieldExtension + character(len=BUFSIZE) :: outputPath + probeBoundsExtension = get_coordinates_extension(this%mainCoords, this%auxCoords, control%mpidir) + prefixFieldExtension = get_prefix_extension(field, control%mpidir) + outputPath = & + trim(adjustl(outputTypeExtension))//'_'//trim(adjustl(prefixFieldExtension))//'_'//trim(adjustl(probeBoundsExtension)) + end function get_output_path_freq + + subroutine update_frequency_slice_probe_output(this, step, fieldsReference, control, problemInfo) + type(frequency_slice_probe_output_t), intent(inout) :: this + real(kind=RKIND_tiempo), intent(in) :: step + type(sim_control_t), intent(in) :: control + type(problem_info_t), intent(in) :: problemInfo + type(fields_reference_t), intent(in) :: fieldsReference + + integer(kind=4) :: request + request = this%component + + if (any(VOLUMIC_M_MEASURE == request)) then + select case (request) + case (iCur); call save_current_module(this, fieldsReference, step, problemInfo) + case (iMEC); call save_field_module(this, fieldsReference%E, step, request, problemInfo) + case (iMHC); call save_field_module(this, fieldsReference%H, step, request, problemInfo) + case default; call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + + else if (any(VOLUMIC_X_MEASURE == request)) then + select case (request) + case (iCurX); call save_current_component(this, this%xValueForFreq, fieldsReference, problemInfo, iEx, this%auxExp_E, this%nFreq, step) + case (iExC); call save_field_component(this, this%xValueForFreq, fieldsReference%E%x, step, problemInfo, iEx) + case (iHxC); call save_field_component(this, this%xValueForFreq, fieldsReference%H%x, step, problemInfo, iHx) + case default; call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + + else if (any(VOLUMIC_Y_MEASURE == request)) then + select case (request) + case (iCurY); call save_current_component(this, this%yValueForFreq, fieldsReference, problemInfo, iEy, this%auxExp_E, this%nFreq, step) + case (iEyC); call save_field_component(this, this%yValueForFreq, fieldsReference%E%y, step, problemInfo, iEy) + case (iHyC); call save_field_component(this, this%yValueForFreq, fieldsReference%H%y, step, problemInfo, iHy) + case default; call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + + else if (any(VOLUMIC_Z_MEASURE == request)) then + select case (request) + case (iCurZ); call save_current_component(this, this%zValueForFreq, fieldsReference, problemInfo, iEz, this%auxExp_E, this%nFreq, step) + case (iEzC); call save_field_component(this, this%zValueForFreq, fieldsReference%E%z, step, problemInfo, iEz) + case (iHzC); call save_field_component(this, this%zValueForFreq, fieldsReference%H%z, step, problemInfo, iHz) + case default; call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + end if + end subroutine update_frequency_slice_probe_output + + subroutine save_current_module(this, fieldsReference, step, problemInfo) + type(frequency_slice_probe_output_t), intent(inout) :: this + type(fields_reference_t), intent(in) :: fieldsReference + type(problem_info_t), intent(in) :: problemInfo + real(kind=RKIND_tiempo), intent(in) :: step + + integer :: i, j, k, coordIdx + + coordIdx = 0 + do i = this%mainCoords%x, this%auxCoords%x + do j = this%mainCoords%y, this%auxCoords%y + do k = this%mainCoords%z, this%auxCoords%z + if (isValidPointForCurrent(iCur, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_current(this%xValueForFreq, iEx, coordIdx, i, j, k, fieldsReference, this%auxExp_E, this%nFreq, step) + call save_current(this%yValueForFreq, iEy, coordIdx, i, j, k, fieldsReference, this%auxExp_E, this%nFreq, step) + call save_current(this%zValueForFreq, iEz, coordIdx, i, j, k, fieldsReference, this%auxExp_E, this%nFreq, step) + end if + end do + end do + end do + end subroutine + + subroutine save_current_component(this, currentData, fieldsReference, problemInfo, fieldDir, auxExp, nFreq, step) + type(frequency_slice_probe_output_t), intent(inout) :: this + complex(kind=CKIND), intent(inout) :: currentData(:, :) + type(fields_reference_t), intent(in) :: fieldsReference + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: fieldDir, nFreq + complex(kind=ckind), intent(in), dimension(:) :: auxExp + real(kind=RKIND_tiempo), intent(in) :: step + + integer :: i, j, k, coordIdx + + coordIdx = 0 + do i = this%mainCoords%x, this%auxCoords%x + do j = this%mainCoords%y, this%auxCoords%y + do k = this%mainCoords%z, this%auxCoords%z + if (isValidPointForCurrent(fieldDir, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_current(currentData, fieldDir, coordIdx, i, j, k, fieldsReference, auxExp, nFreq, step) + end if + end do + end do + end do + end subroutine + + subroutine save_current(valorComplex, direction, coordIdx, i, j, k, fieldsReference, auxExponential, nFreq, step) + integer, intent(in) :: direction + complex(kind=CKIND), intent(inout) :: valorComplex(:, :) + complex(kind=CKIND), intent(in) :: auxExponential(:) + integer, intent(in) :: i, j, k, coordIdx, nFreq + type(fields_reference_t), intent(in) :: fieldsReference + real(kind=RKIND_tiempo), intent(in) :: step + + integer :: iter + complex(kind=CKIND) :: z_cplx = (0.0_RKIND, 0.0_RKIND) + real(kind=rkind) :: jdir + + jdir = computej(direction, i, j, k, fieldsReference) + + do iter = 1, nFreq + valorComplex(iter, coordIdx) = valorComplex(iter, coordIdx) + (auxExponential(iter)**step)*jdir + end do + end subroutine + + subroutine save_field_module(this, fieldInfo, simTime, request, problemInfo) + type(frequency_slice_probe_output_t), intent(inout) :: this + type(field_data_t), intent(in) :: fieldInfo + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: request + + complex(kind=CKIND), dimension(this%nFreq) :: auxExponential + integer :: i, j, k, coordIdx + + if (iMHC == request) auxExponential = this%auxExp_H**simTime + if (iMEC == request) auxExponential = this%auxExp_E**simTime + + coordIdx = 0 + do i = this%mainCoords%x, this%auxCoords%x + do j = this%mainCoords%y, this%auxCoords%y + do k = this%mainCoords%z, this%auxCoords%z + if (isValidPointForField(request, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_field(this%xValueForFreq, auxExponential, fieldInfo%x(i, j, k), this%nFreq, coordIdx) + call save_field(this%yValueForFreq, auxExponential, fieldInfo%y(i, j, k), this%nFreq, coordIdx) + call save_field(this%zValueForFreq, auxExponential, fieldInfo%z(i, j, k), this%nFreq, coordIdx) + end if + end do + end do + end do + + end subroutine + + subroutine save_field_component(this, fieldData, fieldComponent, simTime, problemInfo, fieldDir) + type(frequency_slice_probe_output_t), intent(inout) :: this + complex(kind=CKIND), intent(inout) :: fieldData(:, :) + real(kind=RKIND), intent(in) :: fieldComponent(:, :, :) + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: fieldDir + + complex(kind=CKIND), dimension(this%nFreq) :: auxExponential + integer :: i, j, k, coordIdx + + if (any(MAGNETIC_FIELD_DIRECTION == fieldDir)) auxExponential = this%auxExp_H**simTime + if (any(ELECTRIC_FIELD_DIRECTION == fieldDir)) auxExponential = this%auxExp_E**simTime + + coordIdx = 0 + do i = this%mainCoords%x, this%auxCoords%x + do j = this%mainCoords%y, this%auxCoords%y + do k = this%mainCoords%z, this%auxCoords%z + if (isValidPointForField(fieldDir, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_field(fieldData, auxExponential, fieldComponent(i, j, k), this%nFreq, coordIdx) + end if + end do + end do + end do + end subroutine + + subroutine save_field(valorComplex, auxExp, fieldValue, nFreq, coordIdx) + complex(kind=CKIND), intent(inout) :: valorComplex(:, :) + complex(kind=CKIND), intent(in) :: auxExp(:) + real(KIND=RKIND), intent(in) :: fieldValue + integer(KIND=SINGLE), intent(in) :: nFreq, coordIdx + + integer :: freq + + do freq = 1, nFreq + valorComplex = valorComplex(freq, coordIdx) + auxExp(freq)*fieldValue + end do + end subroutine + + subroutine flush_frequency_slice_probe_output(this) + type(frequency_slice_probe_output_t), intent(inout) :: this + call write_bin_file(this) + end subroutine flush_frequency_slice_probe_output + + subroutine close_frequency_slice_probe_output(this) + type(frequency_slice_probe_output_t), intent(inout) :: this + call write_to_xdmf_h5(this) + end subroutine + + + +end module frequencySliceProbeOutput_m diff --git a/src_output/movieProbeOutput.F90 b/src_output/movieProbeOutput.F90 new file mode 100644 index 00000000..b937eac2 --- /dev/null +++ b/src_output/movieProbeOutput.F90 @@ -0,0 +1,478 @@ +module movieProbeOutput_m + use FDETYPES_m + use utils_m + use report_m + use outputTypes_m + use outputUtils_m + use volumicProbeUtils_m + use xdmfAPI_m + implicit none + private + + !=========================== + ! Public interface + !=========================== + public :: init_movie_probe_output + public :: update_movie_probe_output + public :: flush_movie_probe_output + !=========================== + + !=========================== + ! Private helpers + !=========================== + ! Output & File Management + private :: clear_memory_data + +contains + + !=========================== + ! Public routines + !=========================== + + subroutine init_movie_probe_output(this, lowerBound, upperBound, field, domain, control, problemInfo, outputTypeExtension) + type(movie_probe_output_t), intent(out) :: this + type(cell_coordinate_t), intent(in) :: lowerBound, upperBound + integer(kind=SINGLE), intent(in) :: field + type(domain_t), intent(in) :: domain + type(sim_control_t), intent(in) :: control + type(problem_info_t), intent(in) :: problemInfo + character(len=BUFSIZE), intent(in) :: outputTypeExtension + + integer :: error + character(len=BUFSIZE) :: filename + real(RKIND), pointer :: xsteps(:), ysteps(:), zsteps(:) + + this%mainCoords = lowerBound + this%auxCoords = upperBound + this%component = field + this%domain = domain + + xsteps => problemInfo%xSteps(lowerBound%x:upperBound%x) + ysteps => problemInfo%ySteps(lowerBound%y:upperBound%y) + zsteps => problemInfo%zSteps(lowerBound%z:upperBound%z) + + call find_and_store_important_coords(this%mainCoords, this%auxCoords, this%component, problemInfo, this%nPoints, this%coords) + call alloc_and_init(this%timeStep, BUFSIZE, 0.0_RKIND_tiempo) + + ! Allocate value arrays based on component type + call alloc_and_init(this%xValueForTime, BUFSIZE, this%nPoints, 0.0_RKIND) + call alloc_and_init(this%yValueForTime, BUFSIZE, this%nPoints, 0.0_RKIND) + call alloc_and_init(this%zValueForTime, BUFSIZE, this%nPoints, 0.0_RKIND) + + this%path = get_output_path(this, outputTypeExtension, field, control%mpidir) + filename = get_last_component(this%path) + this%filesPath = join_path(this%path, filename) + + call create_folder(this%path, error) + call create_bin_file(this%filesPath, error) + call create_movie_files(this, error, xsteps, ysteps, zsteps) + if (error/=0) print *, 'error en creacion' + end subroutine init_movie_probe_output + + subroutine create_bin_file(filePath, error) + character(len=*), intent(in) :: filePath + integer, intent(out) :: error + call create_file_with_path(add_extension(filePath, binaryExtension), error) + end subroutine + + subroutine create_movie_files(this, error, xsteps, ysteps, zsteps) + type(movie_probe_output_t), intent(in) :: this + real(RKIND), pointer, intent(in) :: xsteps(:), ysteps(:), zsteps(:) + integer, intent(out) :: error + + real(dp), allocatable, dimension(:, :) :: coordsReal + integer(HID_T) :: file_id + character(len=BUFSIZE) :: h5_filename + character(len=BUFSIZE) :: attributeBaseName + integer(SINGLE), dimension(3) :: topology_size + + h5_filename = add_extension(this%filesPath, ".h5") + topology_size(1) = this%auxCoords%x - this%mainCoords%x + 1 + topology_size(2) = this%auxCoords%y - this%mainCoords%y + 1 + topology_size(3) = this%auxCoords%z - this%mainCoords%z + 1 + + call H5open_f(error) + call create_h5_file(trim(h5_filename), file_id) + + call h5_create_rectilinear_coords_dataset(file_id, real(xsteps,dp), real(ysteps,dp), real(zsteps,dp)) + call h5_create_times_dataset(file_id, BUFSIZE) + call create_h5_data_dataset(file_id, this%component, topology_size) + + call H5Fclose_f(file_id, error) + call H5close_f(error) + end subroutine + + subroutine create_h5_data_dataset(file_id, requestedComponent, topology_size) + integer(HID_T), intent(in) :: file_id + integer(SINGLE), intent(in) :: requestedComponent + integer(SINGLE), dimension(3), intent(in) :: topology_size + + character(len=BUFSIZE) :: attributeBaseName + + select case(requestedComponent) + case(iCur, iCurX, iCurY, iCurZ); attributeBaseName = 'CurrenDensity' + case(iMEC, iExC, iEyC, iEzC); attributeBaseName = 'ElectricField' + case(iMHC, iHxC, iHyC, iHzC); attributeBaseName = 'MagneticField' + end select + + select case(requestedComponent) + case(iCur, iMEC, iMHC) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'X', topology_size, BUFSIZE) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'Y', topology_size, BUFSIZE) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'Z', topology_size, BUFSIZE) + case(iCurX, iEXC, iHXC) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'X', topology_size, BUFSIZE) + case(iCurY, iEyC, iHyC) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'Y', topology_size, BUFSIZE) + case(iCurZ, iEZC, iHzC) + call h5_init_extendable_dataset(file_id, trim(attributeBaseName)//'Z', topology_size, BUFSIZE) + end select + end subroutine + + subroutine update_movie_probe_output(this, step, fieldsReference, control, problemInfo) + type(movie_probe_output_t), intent(inout) :: this + real(kind=RKIND_tiempo), intent(in) :: step + type(fields_reference_t), intent(in) :: fieldsReference + type(sim_control_t), intent(in) :: control + type(problem_info_t), intent(in) :: problemInfo + + integer(kind=4) :: request + request = this%component + this%nTime = this%nTime + 1 + + ! Determine which save routine to call + if (any(VOLUMIC_M_MEASURE == request)) then + select case (request) + case (iCur) + call save_current_module(this, fieldsReference, step, problemInfo) + case (iMEC) + call save_field_module(this, fieldsReference%E, request, step, problemInfo) + case (iMHC) + call save_field_module(this, fieldsReference%H, request, step, problemInfo) + case default + call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + else if (any(VOLUMIC_X_MEASURE == request)) then + select case (request) + case (iCurX) + call save_current_component(this, this%xValueForTime, fieldsReference, step, problemInfo, iEx) + case (iExC) + call save_field_component(this, this%xValueForTime, fieldsReference%E%x, step, problemInfo, iEx) + case (iHxC) + call save_field_component(this, this%xValueForTime, fieldsReference%H%x, step, problemInfo, iHx) + case default + call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + else if (any(VOLUMIC_Y_MEASURE == request)) then + select case (request) + case (iCurY) + call save_current_component(this, this%yValueForTime, fieldsReference, step, problemInfo, iEy) + case (iEyC) + call save_field_component(this, this%yValueForTime, fieldsReference%E%y, step, problemInfo, iEy) + case (iHyC) + call save_field_component(this, this%yValueForTime, fieldsReference%H%y, step, problemInfo, iHy) + case default + call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + else if (any(VOLUMIC_Z_MEASURE == request)) then + select case (request) + case (iCurZ) + call save_current_component(this, this%zValueForTime, fieldsReference, step, problemInfo, iEz) + case (iEzC) + call save_field_component(this, this%zValueForTime, fieldsReference%E%z, step, problemInfo, iEz) + case (iHzC) + call save_field_component(this, this%zValueForTime, fieldsReference%H%z, step, problemInfo, iHz) + case default + call StopOnError(control%layoutnumber, control%num_procs, "Volumic measure not supported") + end select + end if + end subroutine update_movie_probe_output + + subroutine flush_movie_probe_output(this) + type(movie_probe_output_t), intent(inout) :: this + integer :: i + if (this%nTime /= 0) then + call write_bin_file(this) + call write_to_h5_file(this) + call write_to_xdmf_file(this) + end if + call clear_memory_data(this) + end subroutine flush_movie_probe_output + + !=========================== + ! Private routines + !=========================== + + subroutine write_bin_file(this) + ! Check type definition for binary format + type(movie_probe_output_t), intent(inout) :: this + integer :: i, t, unit + + open (unit=unit, file=add_extension(this%filesPath, binaryExtension), & + status='old', form='unformatted', position='append', access='stream') + do t = 1, this%nTime + do i = 1, this%nPoints + write(unit) this%timeStep(t), this%coords(1,i), this%coords(2,i), this%coords(3,i), this%xValueForTime(t,i), this%yValueForTime(t,i), this%zValueForTime(t,i) + end do + end do + flush (unit) + close (unit) + end subroutine + + subroutine write_to_xdmf_file(this) + type(movie_probe_output_t), intent(inout) :: this + + character(len=256) :: xdmf_filename + character(len=256) :: h5_filename + character(len=256) :: attributeBaseName + integer :: xdmfunit, error + integer, dimension(3) :: topologyDimensions + integer, dimension(4) :: h5_dimensions + xdmf_filename = add_extension(this%filesPath, ".xdmf") + h5_filename = add_extension(get_last_component(this%filesPath), ".h5") + + topologyDimensions(1) = this%auxCoords%x - this%mainCoords%x + 1 + topologyDimensions(2) = this%auxCoords%y - this%mainCoords%y + 1 + topologyDimensions(3) = this%auxCoords%z - this%mainCoords%z + 1 + + h5_dimensions(1) = this%nTime + this%nTimesFlushed + h5_dimensions(2) = topologyDimensions(3) + h5_dimensions(3) = topologyDimensions(2) + h5_dimensions(4) = topologyDimensions(1) + + select case(this%component) + case(iCur, iCurX, iCurY, iCurZ); attributeBaseName = 'CurrenDensity' + case(iMEC, iExC, iEyC, iEzC); attributeBaseName = 'ElectricField' + case(iMHC, iHxC, iHyC, iHzC); attributeBaseName = 'MagneticField' + end select + + open(newunit=xdmfunit, file=trim(xdmf_filename), status='replace', position='append', iostat=error) + call xdmf_write_header_file(xdmfunit, 'movieProbe') + + call xdmf_write_topology(xdmfunit, topologyDimensions) + call xdmf_write_geometry(xdmfunit, topologyDimensions, h5_filename) + call xdmf_write_time_array(xdmfunit, this%nTime + this%nTimesFlushed, h5_filename) + + select case(this%component) + case(iCur, iMEC, iMHC) + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'X') + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'Y') + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'Z') + case(iCurX, iEXC, iHXC) + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'X') + case(iCurY, iEyC, iHyC) + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'Y') + case(iCurZ, iEZC, iHzC) + call xdmf_write_scalar_attribute(xdmfunit, h5_dimensions, h5_filename, trim(attributeBaseName)//'Z') + end select + + call xdmf_write_footer_file(xdmfunit) + + close(xdmfunit) + end subroutine + + subroutine write_to_h5_file(this) + type(movie_probe_output_t), intent(inout) :: this + + integer(HID_T) :: file_id + integer :: i, error, probeDimensions(3) + real(dp), allocatable, dimension(:,:,:,:) :: h5Table + character(len=256) :: h5_filename, h5_filepath + character(len=256) :: attributeBaseName + h5_filepath = add_extension(this%filesPath, ".h5") + h5_filename = get_last_component(h5_filepath) + + !Only stores the volume associated to that probe + + probeDimensions(1) = this%auxCoords%x - this%mainCoords%x + 1 + probeDimensions(2) = this%auxCoords%y - this%mainCoords%y + 1 + probeDimensions(3) = this%auxCoords%z - this%mainCoords%z + 1 + + select case(this%component) + case(iCur, iCurX, iCurY, iCurZ); attributeBaseName = 'CurrenDensity' + case(iMEC, iExC, iEyC, iEzC); attributeBaseName = 'ElectricField' + case(iMHC, iHxC, iHyC, iHzC); attributeBaseName = 'MagneticField' + end select + + call H5open_f(error) + call H5Fopen_f(trim(h5_filepath), H5F_ACC_RDWR_F, file_id, error) + + call h5_append_rows_to_dataset(file_id, 'times', this%timeStep(:this%nTime)) + + allocate(h5Table(probeDimensions(1), probeDimensions(2), probeDimensions(3), this%nTime)) !(x,y,z,t) + if (any([iCur, iMEC, iMHC, iCurX, iExC, iHxC]==this%component)) then + h5Table = 0_dp + do i=1, this%nPoints !Readjust idx into hyperslab (x,y,z,t) + h5Table(this%coords(1,i) - this%mainCoords%x + 1, & + this%coords(2,i) - this%mainCoords%y + 1, & + this%coords(3,i) - this%mainCoords%z + 1, & + : ) = this%xValueForTime(:this%nTime, i) + end do + call h5_append_rows_to_dataset(file_id, trim(attributeBaseName)//'X', h5Table) + end if + if (any([iCur, iMEC, iMHC, iCurY, iEyC, iHyC]==this%component)) then + h5Table = 0_dp + do i=1, this%nPoints !Readjust idx into hyperslab (x,y,z,t) + h5Table(this%coords(1,i) - this%mainCoords%x + 1, & + this%coords(2,i) - this%mainCoords%y + 1, & + this%coords(3,i) - this%mainCoords%z + 1, & + : ) = this%yValueForTime(:this%nTime, i) + end do + call h5_append_rows_to_dataset(file_id, trim(attributeBaseName)//'Y', h5Table) + end if + if (any([iCur, iMEC, iMHC, iCurZ, iEzC, iHzC]==this%component)) then + h5Table = 0_dp + do i=1, this%nPoints !Readjust idx into hyperslab (x,y,z,t) + h5Table(this%coords(1,i) - this%mainCoords%x + 1, & + this%coords(2,i) - this%mainCoords%y + 1, & + this%coords(3,i) - this%mainCoords%z + 1, & + : ) = this%zValueForTime(:this%nTime, i) + end do + call h5_append_rows_to_dataset(file_id, trim(attributeBaseName)//'Z', h5Table) + end if + deallocate(h5Table) + + call H5Fclose_f(file_id, error) + call H5close_f(error) + if (error/=0) stop + end subroutine write_to_h5_file + + function get_output_path(this, outputTypeExtension, field, mpidir) result(path) + type(movie_probe_output_t), intent(in) :: this + character(len=*), intent(in) :: outputTypeExtension + integer(kind=SINGLE), intent(in) :: field, mpidir + character(len=BUFSIZE) :: path, probeBoundsExtension, prefixFieldExtension + + probeBoundsExtension = get_coordinates_extension(this%mainCoords, this%auxCoords, mpidir) + prefixFieldExtension = get_prefix_extension(field, mpidir) + path = trim(adjustl(outputTypeExtension))//'_'//trim(adjustl(prefixFieldExtension))//'_'//trim(adjustl(probeBoundsExtension)) + end function get_output_path + + subroutine save_current_module(this, fieldsReference, simTime, problemInfo) + type(movie_probe_output_t), intent(inout) :: this + type(fields_reference_t), intent(in) :: fieldsReference + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + + integer :: i, j, k, coordIdx + this%timeStep(this%nTime) = simTime + coordIdx = 0 + do k = this%mainCoords%z, this%auxCoords%z + do j = this%mainCoords%y, this%auxCoords%y + do i = this%mainCoords%x, this%auxCoords%x + if (isValidPointForCurrent(iCur, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_current(this%xValueForTime, this%nTime, coordIdx, iEx, i, j, k, fieldsReference) + call save_current(this%yValueForTime, this%nTime, coordIdx, iEy, i, j, k, fieldsReference) + call save_current(this%zValueForTime, this%nTime, coordIdx, iEz, i, j, k, fieldsReference) + end if + end do + end do + end do + end subroutine save_current_module + + subroutine save_current_component(this, currentData, fieldsReference, simTime, problemInfo, fieldDir) + type(movie_probe_output_t), intent(inout) :: this + real(kind=RKIND), intent(inout) :: currentData(:, :) + type(fields_reference_t), intent(in) :: fieldsReference + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: fieldDir + + integer :: i, j, k, coordIdx + this%timeStep(this%nTime) = simTime + coordIdx = 0 + do k = this%mainCoords%z, this%auxCoords%z + do j = this%mainCoords%y, this%auxCoords%y + do i = this%mainCoords%x, this%auxCoords%x + if (isValidPointForCurrent(fieldDir, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_current(currentData, this%nTime, coordIdx, fieldDir, i, j, k, fieldsReference) + end if + end do + end do + end do + end subroutine save_current_component + + subroutine save_current(currentData, timeIdx, coordIdx, field, i, j, k, fieldsReference) + real(kind=RKIND), intent(inout) :: currentData(:, :) + integer(kind=SINGLE), intent(in) :: timeIdx, coordIdx, field, i, j, k + type(fields_reference_t), intent(in) :: fieldsReference + + currentData(timeIdx, coordIdx) = computeJ(field, i, j, k, fieldsReference) + end subroutine save_current + + subroutine save_field_module(this, field, request, simTime, problemInfo) + type(movie_probe_output_t), intent(inout) :: this + type(field_data_t), intent(in) :: field + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: request + + integer :: i, j, k, coordIdx + this%timeStep(this%nTime) = simTime + coordIdx = 0 + do k = this%mainCoords%z, this%auxCoords%z + do j = this%mainCoords%y, this%auxCoords%y + do i = this%mainCoords%x, this%auxCoords%x + if (isValidPointForField(request, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_field(this%xValueForTime, this%nTime, coordIdx, field%x(i, j, k)) + call save_field(this%yValueForTime, this%nTime, coordIdx, field%y(i, j, k)) + call save_field(this%zValueForTime, this%nTime, coordIdx, field%z(i, j, k)) + end if + end do + end do + end do + end subroutine save_field_module + + subroutine save_field_component(this, fieldData, fieldComponent, simTime, problemInfo, fieldDir) + type(movie_probe_output_t), intent(inout) :: this + real(kind=RKIND), intent(inout) :: fieldData(:, :) + real(kind=RKIND), intent(in) :: fieldComponent(:, :, :) + real(kind=RKIND_tiempo), intent(in) :: simTime + type(problem_info_t), intent(in) :: problemInfo + integer, intent(in) :: fieldDir + + integer :: i, j, k, coordIdx + this%timeStep(this%nTime) = simTime + coordIdx = 0 + do k = this%mainCoords%z, this%auxCoords%z + do j = this%mainCoords%y, this%auxCoords%y + do i = this%mainCoords%x, this%auxCoords%x + if (isValidPointForField(fieldDir, i, j, k, problemInfo)) then + coordIdx = coordIdx + 1 + call save_field(fieldData, this%nTime, coordIdx, fieldComponent(i, j, k)) + end if + end do + end do + end do + end subroutine save_field_component + + subroutine save_field(fieldData, timeIdx, coordIdx, fieldValue) + real(kind=RKIND), intent(inout) :: fieldData(:, :) + integer(kind=SINGLE), intent(in) :: timeIdx, coordIdx + real(kind=RKIND), intent(in) :: fieldValue + + fieldData(timeIdx, coordIdx) = fieldValue + end subroutine save_field + + subroutine clear_memory_data(this) + type(movie_probe_output_t), intent(inout) :: this + this%nTimesFlushed = this%nTimesFlushed + this%nTime + this%nTime = 0 + this%timeStep = 0.0_RKIND + if (any(VOLUMIC_M_MEASURE == this%component)) then + this%xValueForTime = 0.0_RKIND + this%yValueForTime = 0.0_RKIND + this%zValueForTime = 0.0_RKIND + else if (any(VOLUMIC_X_MEASURE == this%component)) then + this%xValueForTime = 0.0_RKIND + else if (any(VOLUMIC_Y_MEASURE == this%component)) then + this%yValueForTime = 0.0_RKIND + else if (any(VOLUMIC_Z_MEASURE == this%component)) then + this%zValueForTime = 0.0_RKIND + end if + end subroutine clear_memory_data + +end module movieProbeOutput_m diff --git a/src_output/xdmfAPI.F90 b/src_output/xdmfAPI.F90 new file mode 100644 index 00000000..17bfb875 --- /dev/null +++ b/src_output/xdmfAPI.F90 @@ -0,0 +1,561 @@ +module xdmfAPI_m + use HDF5 + implicit none + + ! HDF5 constants + + integer, parameter :: dp = kind(1.0d0) + + + interface h5_write_dataset + module procedure write_1d_dataset + module procedure write_2d_dataset + module procedure write_3d_dataset + end interface + + interface h5_init_extendable_dataset + module procedure init_extendable_1D_dataset + module procedure init_extendable_2D_dataset + module procedure init_extendable_4D_dataset + end interface + + interface h5_append_rows_to_dataset + module procedure append_rows_to_1d_dataset + module procedure append_rows_to_2d_dataset + module procedure append_rows_to_4d_dataset + end interface + +contains + subroutine xdmf_write_header_file(unit, gridName) + integer, intent(in) :: unit + character (len=*), intent(in) :: gridName + write (unit, '(A)' ) '' + write (unit, '(A)' ) '' + write (unit, '(A)' ) '' + write (unit, '(A,A,A)') '' + end subroutine + + subroutine xdmf_write_footer_file(unit) + integer :: unit + write (unit, '(A)') '' + write (unit, '(A)') '' + write (unit, '(A)') '' + end subroutine + + subroutine xdmf_write_topology(unit, dimensions) + integer, intent(in) :: unit + integer, intent(in), dimension(3) :: dimensions + write (unit, '(A,I0,1X,I0,1X,I0,A)') '' !(z,y,x) + end subroutine + + subroutine xdmf_write_geometry(unit, dimensions, h5_filename) + integer, intent(in) :: unit + integer, intent(in), dimension(3) :: dimensions + character (len=*), intent(in) :: h5_filename + + write(unit, '(A)') '' + ! X coordinates + write(unit, '(A,I0,A)') '' + write(unit, '(A,A)' ) trim(h5_filename),':/coordsX' + write(unit, '(A)' ) '' + ! Y coordinates + write(unit, '(A,I0,A)') '' + write(unit, '(A,A)' ) trim(h5_filename),':/coordsY' + write(unit, '(A)' ) '' + ! Z coordinates + write(unit, '(A,I0,A)') '' + write(unit, '(A,A)' ) trim(h5_filename),':/coordsZ' + write(unit, '(A)' ) '' + ! Close JOIN and Geometry + write(unit, '(A)' ) '' + end subroutine + + subroutine xdmf_write_time_array(unit, nTime, h5_filename) + integer, intent(in) :: unit + integer, intent(in) :: nTime + character (len=*), intent(in) :: h5_filename + write(unit, '(A)' ) '' + end subroutine + + subroutine xdmf_write_scalar_attribute(unit, dimensions, h5_filename, attributeName) + integer, intent(in) :: unit + integer, intent(in), dimension(4) :: dimensions + character (len=*), intent(in) :: h5_filename + character (len=*), intent(in) :: attributeName + + write(unit, '(A,A,A)' ) '' + write(unit, '(A,I0,1X,I0,1X,I0,1X,I0,A)') '' + write(unit, '(A,A,A)' ) trim(h5_filename), ':/' ,trim(attributeName) + write(unit, '(A)' ) '' + write(unit, '(A)' ) '' + end subroutine + + subroutine xdmf_create_grid_step_info(unit, stepName, stepValue, h5_filename, ncoords) + !Requires file already open + !h5_file must contain a coords field + integer, intent(in) :: unit + character(len=*), intent(in) :: stepName + real, intent(in) :: stepValue + character(len=*), intent(in) :: h5_filename + integer, intent(in) :: ncoords + + write (unit, '(A,A,A)' ) '' + write (unit, '(A,G0,A)' ) '' + end subroutine xdmf_close_grid + + subroutine xdmf_write_attribute(unit, attributeName) + integer, intent(in) :: unit + character(len=*), intent(in) :: attributeName + write (unit, '(A,A,A)' ) '' + end subroutine xdmf_write_attribute + + subroutine xdmf_close_attribute(unit) + integer, intent(in) :: unit + write (unit, '(A)' ) '' + end subroutine xdmf_close_attribute + + subroutine xdmf_write_hyperslab_data_item(unit, offset, stride, selection, h5_dimension, h5_file_path, h5_data_path) + integer, intent(in) :: unit, offset(4), stride(4), selection(4), h5_dimension(4) + character(len=*), intent(in) :: h5_file_path, h5_data_path + write (unit, '(A,I0,1X,I0,1X,I0,1X,I0,A)') '' + call xdmf_write_h5_access_data_item(unit, offset, stride, selection) + call xdmf_write_h5_data_path(unit, h5_dimension, h5_file_path, h5_data_path) + call xdmf_close_data_item(unit) + end subroutine xdmf_write_hyperslab_data_item + + subroutine xdmf_write_h5_access_data_item(unit, offset, stride, selection) + !Used on cases where we acces parts of an h5 array + integer, intent(in) :: unit, offset(4), stride(4), selection(4) + write (unit, '(A)' ) '' + write (unit, '(I0,1X,I0,1X,I0,1X,I0)') offset(1), offset(2), offset(3), offset(4) + write (unit, '(I0,1X,I0,1X,I0,1X,I0)') stride(1), stride(2), stride(3), stride(4) + write (unit, '(I0,1X,I0,1X,I0,1X,I0)') selection(1), selection(2), selection(3), selection(4) + call xdmf_close_data_item(unit) + end subroutine xdmf_write_h5_access_data_item + + subroutine xdmf_write_h5_data_path(unit, h5_dimension, h5_file_path, h5_data_path) + integer, intent(in) :: unit, h5_dimension(4) + character(len=*), intent(in) :: h5_file_path, h5_data_path + write (unit, '(A,A,A)') ' Date: Wed, 22 Jul 2026 09:27:29 +0000 Subject: [PATCH 02/10] FDTD | Feature | Integrate observation output pipeline --- .github/workflows/ubuntu.yml | 5 +- CMakeLists.txt | 41 +- src_main_pub/fdetypes.F90 | 12 + src_main_pub/timestepping.F90 | 242 ++-- src_output/CMakeLists.txt | 46 + src_output/output.F90 | 547 ++++++++ test/CMakeLists.txt | 13 +- test/fdtd_tests.cpp | 7 +- test/output/CMakeLists.txt | 51 + test/output/output_tests.cpp | 1 + test/output/output_tests.h | 36 + test/output/test_output.F90 | 1100 +++++++++++++++++ test/output/test_output_utils.F90 | 236 ++++ test/output/test_volumic_utils.F90 | 138 +++ test/output/test_vtkAPI.F90 | 396 ++++++ test/output/test_xdmfAPI.F90 | 286 +++++ test/output/vtkAPI_tests.cpp | 1 + test/output/vtkAPI_tests.h | 27 + test/output/xdmfAPI_tests.cpp | 1 + test/output/xdmfAPI_tests.h | 18 + test/utils/CMakeLists.txt | 4 + test/utils/array_assertion_tools.F90 | 337 +++++ test/utils/assertion_tools.F90 | 180 +++ test/utils/fdetypes_tools.F90 | 869 ++++++++++++- test/utils/sgg_setters.F90 | 416 +++++++ .../movie_and_frequency_slices.fdtd.json | 197 +++ .../probes/time_movie_over_cube.fdtd.json | 2 +- 27 files changed, 5040 insertions(+), 169 deletions(-) create mode 100644 src_output/CMakeLists.txt create mode 100644 src_output/output.F90 create mode 100644 test/output/CMakeLists.txt create mode 100644 test/output/output_tests.cpp create mode 100644 test/output/output_tests.h create mode 100644 test/output/test_output.F90 create mode 100644 test/output/test_output_utils.F90 create mode 100644 test/output/test_volumic_utils.F90 create mode 100644 test/output/test_vtkAPI.F90 create mode 100644 test/output/test_xdmfAPI.F90 create mode 100644 test/output/vtkAPI_tests.cpp create mode 100644 test/output/vtkAPI_tests.h create mode 100644 test/output/xdmfAPI_tests.cpp create mode 100644 test/output/xdmfAPI_tests.h create mode 100644 test/utils/array_assertion_tools.F90 create mode 100644 test/utils/assertion_tools.F90 create mode 100644 test/utils/sgg_setters.F90 create mode 100644 testData/input_examples/probes/movie_and_frequency_slices.fdtd.json diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index c86b9f4d..e36a8674 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -26,6 +26,7 @@ jobs: mtln: ["ON", "OFF"] hdf: ["ON"] double-precision: ["OFF"] + new-output-module: ["OFF","ON"] include: # Disable by lack of space on github action @@ -86,13 +87,15 @@ jobs: -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} \ - -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} + -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} \ + -DSEMBA_FDTD_ENABLE_OUTPUT_MODULE=${{matrix.new-output-module}} cmake --build build -j - name: Run unit tests run: build/bin/fdtd_tests - name: Run python tests + if: matrix.new-output-module=='OFF' env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 06c3cadb..2c65d14a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ option(SEMBA_FDTD_ENABLE_MPI "Use MPI" OFF) option(SEMBA_FDTD_ENABLE_HDF "Use HDF" ON) option(SEMBA_FDTD_ENABLE_MTLN "Use MTLN" ON) option(SEMBA_FDTD_ENABLE_SMBJSON "Use smbjson" ON) -option(SEMBA_FDTD_ENABLE_DOUBLE_PRECISION "Use double precision (CompileWithReal8)" OFF) +option(SEMBA_FDTD_ENABLE_DOUBLE_PRECISION "Use double precision (CompileWithReal8)" OFF) option(SEMBA_FDTD_ENABLE_TEST "Compile tests" ON) @@ -27,12 +27,18 @@ option(SEMBA_FDTD_EXECUTABLE "Compiles executable" ON) option(SEMBA_FDTD_MAIN_LIB "Compiles main library" ON) option(SEMBA_FDTD_COMPONENTS_LIB "Compiles components library" ON) option(SEMBA_FDTD_OUTPUTS_LIB "Compiles outputs library" ON) + +option(SEMBA_FDTD_ENABLE_OUTPUT_MODULE "Use the new output module" ON) + # Compilation defines. if(CMAKE_BUILD_TYPE MATCHES "Release" OR CMAKE_BUILD_TYPE MATCHES "release" ) add_definitions(-DCompileWithRelease) else() add_definitions(-DCompileWithDebug) endif() +if(SEMBA_FDTD_ENABLE_OUTPUT_MODULE) + add_definitions(-DCompileWithNewOutputModule) +endif() if(SEMBA_FDTD_ENABLE_SMBJSON) add_definitions(-DCompileWithSMBJSON) endif() @@ -44,12 +50,16 @@ if (SEMBA_FDTD_ENABLE_DOUBLE_PRECISION) else() add_definitions(-DCompileWithReal4) endif() -add_definitions( - -DCompileWithInt2 - -DCompileWithOpenMP -) - -include("${CMAKE_CURRENT_SOURCE_DIR}/set_precompiled_libraries.cmake") +add_definitions( + -DCompileWithInt2 + -DCompileWithOpenMP +) + +if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU") + add_definitions(-DGNUCompiler) +endif() + +include("${CMAKE_CURRENT_SOURCE_DIR}/set_precompiled_libraries.cmake") if (CMAKE_SYSTEM_NAME MATCHES "Linux") message(STATUS "Using Linux flags") @@ -180,6 +190,15 @@ if (SEMBA_FDTD_ENABLE_MTLN) endif() endif() +add_subdirectory(src_utils) + +if (SEMBA_FDTD_ENABLE_OUTPUT_MODULE) + add_subdirectory(src_output) + set(OUTPUT_LIBRARIES fdtd-output) + set(VTK_API_LIBRARIES vtkAPI) + set(XDMF_API_LIBRARIES xdmfAPI) +endif() + add_subdirectory(src_conformal) set(CONFORMAL_LIBRARIES conformal) @@ -188,6 +207,8 @@ if (SEMBA_FDTD_ENABLE_TEST) add_subdirectory(test) endif() + + if(SEMBA_FDTD_COMPONENTS_LIB) add_library(semba-components "src_main_pub/anisotropic.F90" @@ -250,7 +271,11 @@ if(SEMBA_FDTD_MAIN_LIB) "src_main_pub/timestepping.F90" ) target_link_libraries(semba-main - semba-outputs + semba-outputs + fdtd-utils + ${OUTPUT_LIBRARIES} + ${VTK_API_LIBRARIES} + ${XDMF_API_LIBRARIES} ${SMBJSON_LIBRARIES} ${MTLN_LIBRARIES}) endif() diff --git a/src_main_pub/fdetypes.F90 b/src_main_pub/fdetypes.F90 index 6c07acda..a51b4ee6 100755 --- a/src_main_pub/fdetypes.F90 +++ b/src_main_pub/fdetypes.F90 @@ -186,6 +186,17 @@ module FDETYPES_m integer(kind=4), parameter :: iBloqueJx=100*iEx,iBloqueJy=100*iEy,iBloqueJz=100*iEz integer(kind=4), parameter :: iBloqueMx=100*iHx,iBloqueMy=100*iHy,iBloqueMz=100*iHz ! + integer(kind=4), parameter :: VOLUMIC_M_MEASURE(3) = [iCur, iMEC, iMHC] + integer(kind=4), parameter :: VOLUMIC_X_MEASURE(3) = [iCurx, iExC, iHxC] + integer(kind=4), parameter :: VOLUMIC_Y_MEASURE(3) = [iCury, iEyC, iHyC] + integer(kind=4), parameter :: VOLUMIC_Z_MEASURE(3) = [iCurz, iEzC, iHzC] + + integer(kind=4), parameter :: MAGNETIC_FIELD_DIRECTION(3) = [iEx, iEy, iEz] + integer(kind=4), parameter :: ELECTRIC_FIELD_DIRECTION(3) = [iHx, iHy, iHz] + integer(kind=4), parameter :: CURRENT_MEASURE(4) = [iCur, iCurx, iCury, iCurz] + integer(kind=4), parameter :: ELECTRIC_FIELD_MEASURE(4) = [iMEC, iExC, iEyC, iEzC] + integer(kind=4), parameter :: MAGNETIC_FIELD_MEASURE(4) = [iMHC, iHxC, iHyC, iHzC] + ! character(len=*), parameter :: SEPARADOR='______________' integer(kind=4), parameter :: comi=1,fine=2, icoord=1,jcoord=2,kcoord=3 @@ -594,6 +605,7 @@ module FDETYPES_m type :: MediaData_t + integer(kind=SINGLE) :: Id real(kind=RKIND) :: Priority,Epr,Sigma,Mur,SigmaM logical :: sigmareasignado !solo afecta a un chequeo de errores en lumped 120123 type(exists_t) :: Is diff --git a/src_main_pub/timestepping.F90 b/src_main_pub/timestepping.F90 index 546ca5ce..c0ec567e 100755 --- a/src_main_pub/timestepping.F90 +++ b/src_main_pub/timestepping.F90 @@ -15,11 +15,15 @@ !________________________________________________________________________________________ module Solver_m - + use logUtils_m use FDETYPES_m use Report_m use PostProcessing_m use ilumina_m +#ifdef CompileWithNewOutputModule + use output_m + use outputTypes_m +#endif use Observa_m use BORDERS_other_m use BORDERS_CPML_m @@ -1461,9 +1465,13 @@ subroutine initializeObservation() call MPI_Barrier(SUBCOMM_MPI,ierr) #endif write(dubuf,*) 'Init Observation...'; call print11(this%control%layoutnumber,dubuf) +#ifdef CompileWithNewOutputModule + call init_outputs(this%sgg, this%media, this%sinPML_fullsize, this%tag_numbers, this%bounds, this%control, this%thereAre%Observation, this%thereAre%wires) +#else call InitObservation (this%sgg,this%media,this%tag_numbers, & this%thereAre%Observation,this%thereAre%wires,this%thereAre%FarFields,this%initialtimestep,this%lastexecutedtime, & this%sinPML_fullsize,this%eps0,this%mu0,this%bounds, this%control) +#endif l_auxinput=this%thereAre%Observation.or.this%thereAre%FarFields l_auxoutput=l_auxinput @@ -1725,8 +1733,12 @@ end subroutine solver_init subroutine solver_run(this) class(solver_t) :: this - real(kind=rkind), pointer, dimension(:,:,:) :: Ex, Ey, Ez, Hx, Hy, Hz - real(kind=rkind), pointer, dimension(:) :: Idxe, Idye, Idze, Idxh, Idyh, Idzh, dxe, dye, dze, dxh, dyh, dzh + real(kind=rkind), pointer, dimension (:,:,:) :: Ex, Ey, Ez, Hx, Hy, Hz + real(kind=rkind), pointer, dimension (:) :: Idxe, Idye, Idze, Idxh, Idyh, Idzh, dxe, dye, dze, dxh, dyh, dzh + +#ifdef CompileWithNewOutputModule + type(fields_reference_t) :: fieldReference +#endif logical :: call_timing, l_aux, flushFF, somethingdone, newsomethingdone integer :: i @@ -1748,6 +1760,23 @@ subroutine solver_run(this) Idxe => this%Idxe; Idye => this%Idye; Idze => this%Idze; Idxh => this%Idxh; Idyh => this%Idyh; Idzh => this%Idzh; dxe => this%dxe; dye => this%dye; dze => this%dze; dxh => this%dxh; dyh => this%dyh; dzh => this%dzh +#ifdef CompileWithNewOutputModule + fieldReference%E%x => this%Ex + fieldReference%E%y => this%Ey + fieldReference%E%z => this%Ez + + fieldReference%E%deltax => this%dxe + fieldReference%E%deltay => this%dye + fieldReference%E%deltaz => this%dze + + fieldReference%H%x => this%Hx + fieldReference%H%y => this%Hy + fieldReference%H%z => this%Hz + + fieldReference%H%deltax => this%dxh + fieldReference%H%deltay => this%dyh + fieldReference%H%deltaz => this%dzh +#endif ciclo_temporal : do while (this%n <= this%control%finaltimestep) @@ -1771,48 +1800,13 @@ subroutine solver_run(this) Ex,Ey,Ez,this%everflushed,this%control%nentradaroot,this%control%maxSourceValue,this%control%opcionestotales,this%control%simu_devia,this%control%dontwritevtk,this%control%permitscaling) if (.not.this%parar) then !!! si es por parada se gestiona al final -!!!!! si esta hecho lo flushea todo pero poniendo de acuerdo a todos los mpi - do i=1,this%sgg%NumberRequest - if (this%sgg%Observation(i)%done.and.(.not.this%sgg%Observation(i)%flushed)) then - this%perform%flushXdmf=.true. - this%perform%flushVTK=.true. - end if - end do -#ifdef CompileWithMPI - l_aux=this%perform%flushVTK - call MPI_AllReduce( l_aux, this%perform%flushVTK, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) - ! - l_aux=this%perform%flushXdmf - call MPI_AllReduce( l_aux, this%perform%flushXdmf, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) - ! - l_aux=this%perform%flushDATA - call MPI_AllReduce( l_aux, this%perform%flushDATA, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) - ! - l_aux=this%perform%flushFIELDS - call MPI_AllReduce( l_aux, this%perform%flushFIELDS, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) - ! - l_aux=this%perform%postprocess - call MPI_AllReduce( l_aux, this%perform%postprocess, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) -#endif -!!!!!!!!!!!! - if (this%perform%flushFIELDS) then - write(dubuf,*) SEPARADOR,trim(adjustl(this%control%nentradaroot)),separador - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) 'INIT FLUSHING OF RESTARTING FIELDS n=',this%n - call print11(this%control%layoutnumber,dubuf) - call flush_and_save_resume(this%sgg, this%bounds, this%control%layoutnumber, this%control%num_procs, this%control%nentradaroot, this%control%nresumeable2, this%thereare, this%n,this%eps0,this%mu0, this%everflushed, & - Ex, Ey, Ez, Hx, Hy, Hz,this%control%wiresflavor,this%control%simu_devia,this%control%stochastic) -#ifdef CompileWithMPI - call MPI_Barrier(SUBCOMM_MPI,ierr) -#endif - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) 'DONE FLUSHING OF RESTARTING FIELDS n=',this%n - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) - end if - if (this%perform%isFlush()) then + call request_flush_if_any_observation_is_done() + + if (this%perform%flushFIELDS) then + call performFlushField() + end if + + if (this%perform%isFlush()) then ! flushFF=this%perform%postprocess if (this%thereAre%FarFields.and.flushFF) then @@ -1820,12 +1814,11 @@ subroutine solver_run(this) else write(dubuf,'(a,i9)') ' INIT OBSERVATION DATA FLUSHING n= ',this%n end if - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - !! + call printMessageWithSeparator(this%control%layoutnumber,dubuf) +#ifdef CompileWithNewOutputModule + if (this%thereAre%Observation) call flush_outputs(this%sgg%tiempo, this%n, this%control, fieldReference, this%bounds, flushFF) +#else if (this%thereAre%Observation) call FlushObservationFiles(this%sgg,this%ini_save, this%n,this%control%layoutnumber, this%control%num_procs, dxe, dye, dze, dxh, dyh, dzh,this%bounds,this%control%singlefilewrite,this%control%facesNF2FF,flushFF) - !! #ifdef CompileWithMPI call MPI_Barrier(SUBCOMM_MPI,ierr) #endif @@ -1834,15 +1827,11 @@ subroutine solver_run(this) else write(dubuf,'(a,i9)') ' Done OBSERVATION DATA FLUSHED n= ',this%n end if - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber,dubuf) ! if (this%perform%postprocess) then write(dubuf,'(a,i9)') 'Postprocessing frequency domain probes, if any, at n= ',this%n - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) somethingdone=.false. at=this%n*this%sgg%dt if (this%thereAre%Observation) call PostProcessOnthefly(this%control%layoutnumber,this%control%num_procs,this%sgg,this%control%nentradaroot,at,somethingdone,this%control%niapapostprocess,this%control%forceresampled) @@ -1853,22 +1842,16 @@ subroutine solver_run(this) #endif if (somethingdone) then write(dubuf,*) 'End Postprocessing frequency domain probes.' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) else write(dubuf,*) 'No frequency domain probes snapshots found to be postrocessed' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) end if end if !! if (this%perform%flushvtk) then write(dubuf,'(a,i9)') ' Post-processing .vtk files n= ',this%n - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber,dubuf) somethingdone=.false. if (this%thereAre%Observation) call createvtkOnTheFly(this%control%layoutnumber,this%control%num_procs,this%sgg,this%control%vtkindex,somethingdone,this%control%mpidir,this%media%sggMtag,this%control%dontwritevtk) #ifdef CompileWithMPI @@ -1878,21 +1861,15 @@ subroutine solver_run(this) #endif if (somethingdone) then write(dubuf,*) 'End flushing .vtk snapshots' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) else write(dubuf,*) 'No .vtk snapshots found to be flushed' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) end if end if if (this%perform%flushXdmf) then write(dubuf,'(a,i9)') ' Post-processing .xdmf files n= ',this%n - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber,dubuf) somethingdone=.false. if (this%thereAre%Observation) call createxdmfOnTheFly(this%sgg,this%control%layoutnumber,this%control%num_procs,this%control%vtkindex,this%control%createh5bin,somethingdone,this%control%mpidir) @@ -1905,30 +1882,22 @@ subroutine solver_run(this) #endif if (somethingdone) then write(dubuf,*) 'End flushing .xdmf snapshots' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) else write(dubuf,*) 'No .xdmf snapshots found to be flushed' - call print11(this%control%layoutnumber,dubuf) - write(dubuf,*) SEPARADOR//separador//separador - call print11(this%control%layoutnumber,dubuf) + call printMessageWithEndingSeparator(this%control%layoutnumber,dubuf) end if end if #ifdef CompileWithMPI call MPI_Barrier(SUBCOMM_MPI,ierr) +#endif #endif end if !del if (this%performflushDATA.or.... - ! - - if (this%control%singlefilewrite.and.this%perform%Unpack) call singleUnpack() if ((this%control%singlefilewrite.and.this%perform%Unpack).or.this%perform%isFlush()) then write(dubuf,'(a,i9)') ' Continuing simulation at n= ',this%n - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber,dubuf) end if end if !!!del if (.not.this%parar) @@ -1970,15 +1939,72 @@ subroutine solver_run(this) this%n=this%n+1 !sube de iteracion end do ciclo_temporal ! End of the time-stepping loop + contains + + subroutine request_flush_if_any_observation_is_done() + do i=1,this%sgg%NumberRequest + if (this%sgg%Observation(i)%done.and.(.not.this%sgg%Observation(i)%flushed)) then + this%perform%flushXdmf=.true. + this%perform%flushVTK=.true. + end if + end do +#ifdef CompileWithMPI + call syncroniceFlushFlags(this%perform, ierr) +#endif + end subroutine + +#ifdef CompileWithMPI + subroutine syncroniceFlushFlags(performFlags, integerError) + type(perform_t), intent(inout) :: performFlags + integer, intent(inout) :: integerError + logical :: logicalAux + logicalAux=performFlags%flushVTK + call MPI_AllReduce( logicalAux, performFlags%flushVTK, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, integerError) + logicalAux=performFlags%flushXdmf + call MPI_AllReduce( logicalAux, performFlags%flushXdmf, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, integerError) + logicalAux=performFlags%flushDATA + call MPI_AllReduce( logicalAux, performFlags%flushDATA, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, integerError) + logicalAux=performFlags%flushFIELDS + call MPI_AllReduce( logicalAux, performFlags%flushFIELDS, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, integerError) + logicalAux=performFlags%postprocess + call MPI_AllReduce( logicalAux, performFlags%postprocess, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, integerError) + end subroutine syncroniceFlushFlags +#endif + + subroutine performFlushField() + write(dubuf,*) SEPARADOR,trim(adjustl(this%control%nentradaroot)),SEPARADOR + call printMessage(this%control%layoutnumber,dubuf) + write(dubuf,*) 'INIT FLUSHING OF RESTARTING FIELDS n=',this%n + call printMessage(this%control%layoutnumber,dubuf) + + call flush_and_save_resume(this%sgg, this%bounds, this%control%layoutnumber, this%control%num_procs, this%control%nentradaroot, this%control%nresumeable2, this%thereare, this%n,this%eps0,this%mu0, this%everflushed, & + Ex, Ey, Ez, Hx, Hy, Hz,this%control%wiresflavor,this%control%simu_devia,this%control%stochastic) +#ifdef CompileWithMPI + call MPI_Barrier(SUBCOMM_MPI,ierr) +#endif + write(dubuf,*) 'DONE FLUSHING OF RESTARTING FIELDS n=',this%n + call printMessageWithSeparator(this%control%layoutnumber, dubuf) + + end subroutine performFlushField subroutine updateAndFlush() integer(kind=4) :: mindum - if (this%thereAre%Observation) then + IF (this%thereAre%Observation) then +#ifdef CompileWithNewOutputModule + if (this%n /= 0) then + call update_outputs(this%control, this%sgg%tiempo, this%n, fieldReference) + if (this%n>=this%ini_save+BuffObse) then + mindum=min(this%control%finaltimestep,this%ini_save+BuffObse) + call flush_outputs(this%sgg%tiempo, this%n, this%control, fieldReference, this%bounds, .FALSE.) + end if + end if +#else call UpdateObservation(this%sgg,this%media,this%tag_numbers, this%n,this%ini_save, Ex, Ey, Ez, Hx, Hy, Hz, dxe, dye, dze, dxh, dyh, dzh,this%control%wiresflavor,this%sinPML_fullsize,this%control%wirecrank, this%control%noconformalmapvtk,this%bounds) if (this%n>=this%ini_save+BuffObse) then mindum=min(this%control%finaltimestep,this%ini_save+BuffObse) call FlushObservationFiles(this%sgg,this%ini_save,mindum,this%control%layoutnumber,this%control%num_procs, dxe, dye, dze, dxh, dyh, dzh,this%bounds,this%control%singlefilewrite,this%control%facesNF2FF,.FALSE.) !no se flushean los farfields ahora end if +#endif end if end subroutine @@ -1989,10 +2015,9 @@ subroutine singleUnpack() #ifdef CompileWithMPI integer(kind=4) :: ierr #endif - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) write(dubuf,'(a,i9)') ' Unpacking .bin files and prostprocessing them at n= ',this%n - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber, dubuf) + if (this%thereAre%Observation) call unpacksinglefiles(this%sgg,this%control%layoutnumber,this%control%num_procs,this%control%singlefilewrite,this%initialtimestep,this%control%resume) !dump the remaining to disk somethingdone=.false. if (this%control%singlefilewrite.and.this%perform%Unpack) then @@ -2005,9 +2030,7 @@ subroutine singleUnpack() somethingdone=newsomethingdone #endif write(dubuf,'(a,i9)') ' Done Unpacking .bin files and prostprocessing them at n= ',this%n - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) - call print11(this%control%layoutnumber,dubuf) - call print11(this%control%layoutnumber,SEPARADOR//separador//separador) + call printMessageWithSeparator(this%control%layoutnumber, dubuf) end subroutine singleUnpack @@ -2663,12 +2686,35 @@ subroutine solver_end(this) logical :: dummylog, somethingdone, newsomethingdone character(len=bufsize) :: dubuf +#ifdef CompileWithNewOutputModule + type(fields_reference_t) :: fieldReference +#endif + + #ifdef CompileWithMPI integer(kind=4) :: ierr #endif Ex => this%Ex; Ey => this%Ey; Ez => this%Ez; Hx => this%Hx; Hy => this%Hy; Hz => this%Hz; dxe => this%dxe; dye => this%dye; dze => this%dze; dxh => this%dxh; dyh => this%dyh; dzh => this%dzh +#ifdef CompileWithNewOutputModule + fieldReference%E%x => this%Ex + fieldReference%E%y => this%Ey + fieldReference%E%z => this%Ez + + fieldReference%E%deltax => this%dxe + fieldReference%E%deltay => this%dye + fieldReference%E%deltaz => this%dze + + fieldReference%H%x => this%Hx + fieldReference%H%y => this%Hy + fieldReference%H%z => this%Hz + + fieldReference%H%deltax => this%dxh + fieldReference%H%deltay => this%dyh + fieldReference%H%deltaz => this%dzh +#endif + #ifdef CompileWithProfiling call nvtxEndRange #endif @@ -2710,10 +2756,14 @@ subroutine solver_end(this) call print11(this%control%layoutnumber,dubuf) call print11(this%control%layoutnumber,SEPARADOR//separador//separador) if (this%thereAre%Observation) then +#ifdef CompileWithNewOutputModule + call flush_outputs(this%sgg%tiempo, this%n, this%control, fieldReference, this%bounds, .TRUE.) + call close_outputs() +#else call FlushObservationFiles(this%sgg,this%ini_save, this%n,this%control%layoutnumber, this%control%num_procs, dxe, dye, dze, dxh, dyh, dzh,this%bounds,this%control%singlefilewrite,this%control%facesNF2FF,.TRUE.) call CloseObservationFiles(this%sgg,this%control%layoutnumber,this%control%num_procs,this%control%singlefilewrite,this%initialtimestep,this%lastexecutedtime,this%control%resume) !dump the remaining to disk +#endif end if - if (this%thereAre%FarFields) then write(dubuf,'(a,i9)') ' DONE FINAL OBSERVATION DATA FLUSHED and Near-to-Far field n= ',this%n else @@ -2727,6 +2777,9 @@ subroutine solver_end(this) call MPI_Barrier(SUBCOMM_MPI,ierr) #endif +#ifdef CompileWithNewOutputModule +#else + write(dubuf,'(a,i9)') 'INIT FINAL Postprocessing frequency domain probes, if any, at n= ',this%n call print11(this%control%layoutnumber,dubuf) write(dubuf,*) SEPARADOR//separador//separador @@ -2734,6 +2787,7 @@ subroutine solver_end(this) somethingdone=.false. at=this%n*this%sgg%dt if (this%thereAre%Observation) call PostProcess(this%control%layoutnumber,this%control%num_procs,this%sgg,this%control%nentradaroot,at,somethingdone,this%control%niapapostprocess,this%control%forceresampled) +#endif #ifdef CompileWithMPI call MPI_Barrier(SUBCOMM_MPI,ierr) call MPI_AllReduce(somethingdone, newsomethingdone, 1_4, MPI_LOGICAL, MPI_LOR, SUBCOMM_MPI, ierr) diff --git a/src_output/CMakeLists.txt b/src_output/CMakeLists.txt new file mode 100644 index 00000000..9e8efc87 --- /dev/null +++ b/src_output/CMakeLists.txt @@ -0,0 +1,46 @@ +message(STATUS "Creating build system for output") + +set(CMAKE_Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/mod) + +add_library(fdtd-output + "output.F90" + "outputTypes.F90" + "domain.F90" + "outputUtils.F90" + "volumicProbeUtils.F90" + "pointProbeOutput.F90" + "wireProbeOutput.F90" + "bulkProbeOutput.F90" + "movieProbeOutput.F90" + "frequencySliceProbeOutput.F90" + "farFieldProbeOutput.F90" + "mapVTKOutput.F90" +) + +target_link_libraries(fdtd-output PRIVATE + semba-components + semba-types + fdtd-utils + ${HDF5_LIBRARIES} + ${HDF5_HL_LIBRARIES} +) + + +add_library(vtkAPI + "vtkAPI.F90" +) + +add_library(xdmfAPI + "xdmfAPI.F90" +) + +target_link_libraries(xdmfAPI PRIVATE + ${HDF5_LIBRARIES} + ${HDF5_HL_LIBRARIES} +) + +target_link_libraries(vtkAPI + fdtd-utils +) + +target_include_directories(xdmfAPI PRIVATE ${HDF5_INCLUDE_DIRS}) diff --git a/src_output/output.F90 b/src_output/output.F90 new file mode 100644 index 00000000..4c21bb35 --- /dev/null +++ b/src_output/output.F90 @@ -0,0 +1,547 @@ +module output_m + use FDETYPES_m + use report_m + use domain_m + use outputUtils_m + use pointProbeOutput_m + use wireProbeOutput_m + use bulkProbeOutput_m + use movieProbeOutput_m + use frequencySliceProbeOutput_m + use farFieldOutput_m + use mapVTKOutput_m +#ifdef CompileWithMTLN + use Wire_bundles_mtln_m, only: GetSolverPtr + use mtln_solver_m, only: mtln_solver_t => mtln_t +#endif + + + implicit none + private + + !=========================== + ! Public interface summary + !=========================== + public :: solver_output_t + public :: GetOutputs + public :: init_outputs + public :: update_outputs + public :: flush_outputs + public :: close_outputs + + public :: POINT_PROBE_ID, WIRE_CURRENT_PROBE_ID, WIRE_CHARGE_PROBE_ID, BULK_PROBE_ID, VOLUMIC_CURRENT_PROBE_ID, & + MOVIE_PROBE_ID, FREQUENCY_SLICE_PROBE_ID, FAR_FIELD_PROBE_ID + !=========================== + + !=========================== + ! Private interface summary + !=========================== + private :: get_required_output_count + !=========================== + + integer(kind=SINGLE), parameter :: UNDEFINED_PROBE = -1, & + POINT_PROBE_ID = 0, & + WIRE_CURRENT_PROBE_ID = 1, & + WIRE_CHARGE_PROBE_ID = 2, & + BULK_PROBE_ID = 3, & + VOLUMIC_CURRENT_PROBE_ID = 4, & + MOVIE_PROBE_ID = 5, & + FREQUENCY_SLICE_PROBE_ID = 6, & + FAR_FIELD_PROBE_ID = 7, & + MAPVTK_ID = 8 + + REAL(KIND=RKIND), save :: eps0, mu0 + REAL(KIND=RKIND), pointer, dimension(:), save :: InvEps, InvMu + type(solver_output_t), pointer, dimension(:), save :: outputs + type(problem_info_t), save, target :: problemInfo + + interface init_solver_output + module procedure & + init_point_probe_output, & + init_wire_current_probe_output, & + init_wire_charge_probe_output, & + init_bulk_probe_output, & + init_movie_probe_output, & + init_frequency_slice_probe_output, & + init_farField_probe_output, & + init_mapvtk_output + end interface + + interface update_solver_output + module procedure & + update_point_probe_output, & + update_wire_current_probe_output, & + update_wire_charge_probe_output, & + update_bulk_probe_output, & + update_movie_probe_output, & + update_frequency_slice_probe_output, & + update_farField_probe_output + end interface + + interface flush_solver_output + module procedure & + flush_point_probe_output, & + flush_wire_current_probe_output, & + flush_wire_charge_probe_output, & + flush_bulk_probe_output, & + flush_movie_probe_output, & + flush_frequency_slice_probe_output, & + flush_farField_probe_output + + end interface + + interface close_solver_output + module procedure close_frequency_slice_probe_output + end interface +contains + + function GetOutputs() result(r) + type(solver_output_t), pointer, dimension(:) :: r + r => outputs + return + end function + + function GetProblemInfo() result(r) + type(problem_info_t), pointer :: r + r => problemInfo + return + end function + + subroutine init_outputs(sgg, media, sinpml_fullsize, materialTags, bounds, control, observationsExists, wiresExists) + + type(SGGFDTDINFO_t), intent(in) :: sgg + type(media_matrices_t), target, intent(in) :: media + type(limit_t), dimension(:), target, intent(in) :: SINPML_fullsize + type(bounds_t),intent(in), target :: bounds + type(taglist_t),intent(in), target :: materialTags + type(sim_control_t), intent(in) :: control + logical, intent(inout) :: wiresExists + logical, intent(out) :: observationsExists + + type(domain_t) :: domain + type(spheric_domain_t) :: sphericRange + type(cell_coordinate_t) :: lowerBound, upperBound + integer(kind=SINGLE) :: i, ii, outputRequestType + integer(kind=SINGLE) :: NODE + integer(kind=SINGLE) :: outputCount + integer(kind=SINGLE) :: requestedOutputs + character(len=BUFSIZE) :: outputTypeExtension + +#ifdef CompileWithMTLN + logical :: thereAreMtlnObservations = .false. +#endif + observationsExists = .false. + requestedOutputs = get_required_output_count(sgg) + + problemInfo%geometryToMaterialData => media + problemInfo%materialList => sgg%Med + problemInfo%simulationBounds => bounds + problemInfo%problemDimension => SINPML_fullsize + problemInfo%materialTag => materialTags + problemInfo%xSteps => sgg%LineX + problemInfo%ySteps => sgg%LineY + problemInfo%zSteps => sgg%LineZ + + outputs => NULL() + allocate (outputs(requestedOutputs)) + + allocate (InvEps(0:sgg%NumMedia - 1), InvMu(0:sgg%NumMedia - 1)) + outputCount = 0 + + InvEps(0:sgg%NumMedia - 1) = 1.0_RKIND/(Eps0*sgg%Med(0:sgg%NumMedia - 1)%Epr) + InvMu(0:sgg%NumMedia - 1) = 1.0_RKIND/(Mu0*sgg%Med(0:sgg%NumMedia - 1)%Mur) + + !do ii = 1, sgg%NumberRequest + !do i = 1, sgg%Observation(ii)%nP + ! call eliminate_unnecesary_observation_points(sgg%Observation(ii)%P(i), output(ii)%item(i), & + ! sgg%Sweep, sgg%SINPMLSweep, sgg%Observation(ii)%P(1)%ZI, sgg%Observation(ii)%P(1)%ZE, control%layoutnumber, control%num_procs) + !end do + !end do + +#ifdef CompileWithMTLN + block + type(mtln_solver_t), pointer :: mtln_solver + integer :: i, j + mtln_solver => GetSolverPtr() + if (mtln_solver%number_of_bundles > 0) then + do i = 1, ubound(mtln_solver%bundles, 1) + if (ubound(mtln_solver%bundles(i)%probes, 1) /= 0) then + do j = 1, ubound(mtln_solver%bundles(i)%probes, 1) + if (mtln_solver%bundles(i)%probes(j)%in_layer) thereAreMtlnObservations = .true. + end do + end if + end do + end if + end block +#endif + + do ii = 1, sgg%NumberRequest + domain = preprocess_domain(sgg%Observation(ii), sgg%tiempo, sgg%dt, control%finaltimestep) + if (domain%domainType == UNDEFINED_DOMAIN) cycle + do i = 1, sgg%Observation(ii)%nP + lowerBound%x = sgg%observation(ii)%P(i)%XI + lowerBound%y = sgg%observation(ii)%P(i)%YI + lowerBound%z = sgg%observation(ii)%P(i)%ZI + + upperBound%x = sgg%observation(ii)%P(i)%XE + upperBound%y = sgg%observation(ii)%P(i)%YE + upperBound%z = sgg%observation(ii)%P(i)%ZE + NODE = sgg%observation(ii)%P(i)%NODE + + outputTypeExtension = trim(adjustl(control%nEntradaRoot))//'_'//trim(adjustl(sgg%observation(ii)%outputrequest)) + + outputRequestType = sgg%observation(ii)%P(i)%what + select case (outputRequestType) + case (mapvtk) + outputCount = outputCount + 1 + outputs(outputCount)%outputID = MAPVTK_ID + + allocate (outputs(outputCount)%mapvtkOutput) + call init_solver_output(outputs(outputCount)%mapvtkOutput, lowerBound, upperBound, outputRequestType, outputTypeExtension, control%mpidir, problemInfo) + call create_geometry_simulation_vtu(outputs(outputCount)%mapvtkOutput, control, sgg%LineX, sgg%LineY, sgg%LineZ) + !! call adjust_computation_range --- Required due to issues in mpi region edges + + case (iEx, iEy, iEz, iHx, iHy, iHz) + outputCount = outputCount + 1 + outputs(outputCount)%outputID = POINT_PROBE_ID + + allocate (outputs(outputCount)%pointProbe) + call init_solver_output(outputs(outputCount)%pointProbe, lowerBound, outputRequestType, domain, outputTypeExtension, control%mpidir, sgg%dt) + case (iJx, iJy, iJz) + if (wiresExists) then + outputCount = outputCount + 1 + outputs(outputCount)%outputID = WIRE_CURRENT_PROBE_ID + + allocate (outputs(outputCount)%wireCurrentProbe) + call init_solver_output(outputs(outputCount)%wireCurrentProbe, lowerBound, NODE, outputRequestType, domain, problemInfo%materialList, outputTypeExtension, control%mpidir, control%wiresflavor) + end if + + case (iQx, iQy, iQz) + outputCount = outputCount + 1 + outputs(outputCount)%outputID = WIRE_CHARGE_PROBE_ID + + allocate (outputs(outputCount)%wireChargeProbe) + call init_solver_output(outputs(outputCount)%wireChargeProbe, lowerBound, NODE, outputRequestType, domain, outputTypeExtension, control%mpidir, control%wiresflavor) + + case (iBloqueJx, iBloqueJy, iBloqueJz, iBloqueMx, iBloqueMy, iBloqueMz) + outputCount = outputCount + 1 + outputs(outputCount)%outputID = BULK_PROBE_ID + + allocate (outputs(outputCount)%bulkCurrentProbe) + call init_solver_output(outputs(outputCount)%bulkCurrentProbe, lowerBound, upperBound, outputRequestType, domain, outputTypeExtension, control%mpidir) + !! call adjust_computation_range --- Required due to issues in mpi region edges + + case (iCur, iMEC, iMHC, iCurX, iCurY, iCurZ, iExC, iEyC, iEzC, iHxC, iHyC, iHzC) + call adjust_bound_range() + + if (domain%domainType == TIME_DOMAIN) then + + outputCount = outputCount + 1 + outputs(outputCount)%outputID = MOVIE_PROBE_ID + allocate (outputs(outputCount)%movieProbe) + call init_solver_output(outputs(outputCount)%movieProbe, lowerBound, upperBound, outputRequestType, domain, control, problemInfo, outputTypeExtension) + else if (domain%domainType == FREQUENCY_DOMAIN) then + + outputCount = outputCount + 1 + outputs(outputCount)%outputID = FREQUENCY_SLICE_PROBE_ID + allocate (outputs(outputCount)%frequencySliceProbe) + call init_solver_output(outputs(outputCount)%frequencySliceProbe, lowerBound, upperBound, sgg%dt, outputRequestType, domain, outputTypeExtension, control, problemInfo) + end if + case (farfield) + sphericRange = preprocess_polar_range(sgg%Observation(ii)) + + outputCount = outputCount + 1 + outputs(outputCount)%outputID = FAR_FIELD_PROBE_ID + allocate (outputs(outputCount)%farFieldOutput) + call init_solver_output(outputs(outputCount)%farFieldOutput, sgg, lowerBound, upperBound, outputRequestType, domain, sphericRange, outputTypeExtension, sgg%Observation(ii)%FileNormalize, control, problemInfo, eps0, mu0) + case default + call stoponerror(0, 0, 'OutputRequestType type not implemented yet on new observations') + end select + end do + end do + if (outputCount /= requestedOutputs) then + call remove_unused_outputs(outputs) + outputCount = size(outputs) + end if + if (outputCount /= 0) observationsExists = .true. +#ifdef CompileWithMTLN + observationsExists = observationsExists .or. thereAreMtlnObservations +#endif + if (observationsExists) call registerOutputFiles(control, outputCount) + return + contains + subroutine adjust_bound_range() + select case (outputRequestType) + case (iExC, iEyC, iHzC, iMhC) + lowerBound%z = max(sgg%Sweep(fieldo(outputRequestType, 'Z'))%ZI, sgg%observation(ii)%P(i)%ZI) + upperBound%z = min(sgg%Sweep(fieldo(outputRequestType, 'Z'))%ZE - 1, sgg%observation(ii)%P(i)%ZE) + case (iEzC, iHxC, iHyC, iMeC) + lowerBound%z = max(sgg%Sweep(fieldo(outputRequestType, 'Z'))%ZI, sgg%observation(ii)%P(i)%ZI) + upperbound%z = min(sgg%Sweep(fieldo(outputRequestType, 'Z'))%ZE, sgg%observation(ii)%P(i)%ZE) + case (iCur, iCurX, iCurY, iCurZ) + lowerBound%z = max(sgg%Sweep(fieldo(outputRequestType, 'X'))%ZI, sgg%observation(ii)%P(i)%ZI) !ojo estaba sweep(iEz) para ser conservador...puede dar problemas!! 03/07/15 + upperbound%z = min(sgg%Sweep(fieldo(outputRequestType, 'X'))%ZE, sgg%observation(ii)%P(i)%ZE) !ojo estaba sweep(iEz) para ser conservador...puede dar problemas!! 03/07/15 + end select + end subroutine + function preprocess_domain(observation, timeArray, simulationTimeStep, finalStepIndex) result(newDomain) + type(Obses_t), intent(in) :: observation + real(kind=RKIND_tiempo), pointer, dimension(:), intent(in) :: timeArray + real(kind=RKIND_tiempo), intent(in) :: simulationTimeStep + integer(kind=4), intent(in) :: finalStepIndex + type(domain_t) :: newDomain + + integer(kind=SINGLE) :: nFreq + + if (observation%TimeDomain) then + newdomain = domain_t(real(observation%InitialTime, kind=RKIND_tiempo), & + real(observation%FinalTime, kind=RKIND_tiempo), & + real(observation%TimeStep, kind=RKIND_tiempo)) + + newdomain%tstep = max(newdomain%tstep, simulationTimeStep) + + if (10.0_RKIND*(newdomain%tstop - newdomain%tstart)/min(simulationTimeStep, newdomain%tstep) >= huge(1_4)) then + newdomain%tstop = newdomain%tstart + min(simulationTimeStep, newdomain%tstep)*huge(1_4)/10.0_RKIND + end if + + if (newDomain%tstart < newDomain%tstep) then + newDomain%tstart = 0.0_RKIND_tiempo + end if + + if (newDomain%tstep > (newdomain%tstop - newdomain%tstart)) then + newDomain%tstop = newDomain%tstart + newDomain%tstep + end if + + elseif (observation%FreqDomain) then + !Just linear progression for now. Need to bring logartihmic info to here + nFreq = int((observation%FinalFreq - observation%InitialFreq)/observation%FreqStep, kind=SINGLE) + 1_SINGLE + newdomain = domain_t(observation%InitialFreq, observation%FinalFreq, nFreq, logarithmicspacing=.false.) + + newDomain%fstep = min(newDomain%fstep, 2.0_RKIND/simulationTimeStep) + if ((newDomain%fstep > newDomain%fstop - newDomain%fstart) .or. (newDomain%fstep == 0)) then + newDomain%fstep = newDomain%fstop - newDomain%fstart + newDomain%fstop = newDomain%fstart + newDomain%fstep + end if + + newDomain%fnum = int((newDomain%fstop - newDomain%fstart)/newDomain%fstep, kind=SINGLE) + + else + newDomain = domain_t() + end if + return + end function preprocess_domain + + function preprocess_polar_range(observation) result(sphericDomain) + type(spheric_domain_t) :: sphericDomain + type(Obses_t), intent(in) :: observation + + sphericDomain%phiStart = observation%phiStart + sphericDomain%phiStop = observation%phiStop + sphericDomain%phiStep = observation%phiStep + sphericDomain%thetaStart = observation%thetaStart + sphericDomain%thetaStop = observation%thetaStop + sphericDomain%thetaStep = observation%thetaStep + end function preprocess_polar_range + + end subroutine init_outputs + + subroutine update_outputs(control, discreteTimeArray, timeIndx, fieldsReference) + integer(kind=SINGLE), intent(in) :: timeIndx + real(kind=RKIND_tiempo), dimension(:), intent(in) :: discreteTimeArray + integer(kind=SINGLE) :: i, id + type(sim_control_t), intent(in) :: control + real(kind=RKIND), pointer, dimension(:, :, :) :: fieldComponent + type(field_data_t) :: fieldReference + type(fields_reference_t), intent(in) :: fieldsReference + real(kind=RKIND_tiempo) :: discreteTime + + discreteTime = discreteTimeArray(timeIndx) + + do i = 1, size(outputs) + select case (outputs(i)%outputID) + case (POINT_PROBE_ID) + fieldComponent => get_field_component(outputs(i)%pointProbe%component, fieldsReference) !Cada componente requiere de valores deiferentes pero estos valores no se como conseguirlos + call update_solver_output(outputs(i)%pointProbe, discreteTime, fieldComponent) + case (WIRE_CURRENT_PROBE_ID) + call update_solver_output(outputs(i)%wireCurrentProbe, discreteTime, control, InvEps, InvMu) + case (WIRE_CHARGE_PROBE_ID) + call update_solver_output(outputs(i)%wireChargeProbe, discreteTime) + case (BULK_PROBE_ID) + fieldReference = get_field_reference(outputs(i)%bulkCurrentProbe%component, fieldsReference) + call update_solver_output(outputs(i)%bulkCurrentProbe, discreteTime, fieldReference) + case (MOVIE_PROBE_ID) + call update_solver_output(outputs(i)%movieProbe, discreteTime, fieldsReference, control, problemInfo) + case (FREQUENCY_SLICE_PROBE_ID) + call update_solver_output(outputs(i)%frequencySliceProbe, discreteTime, fieldsReference, control, problemInfo) + case (FAR_FIELD_PROBE_ID) + call update_solver_output(outputs(i)%farFieldOutput, timeIndx, problemInfo%simulationBounds, fieldsReference) + case(MAPVTK_ID) + case default + call stoponerror(0, 0, 'Output update not implemented') + end select + end do + + end subroutine update_outputs + + subroutine flush_outputs(simulationTimeArray, simulationTimeIndex, control, fields, bounds, farFieldFlushRequested) + implicit none + type(fields_reference_t), target :: fields + type(fields_reference_t), pointer :: fieldsPtr + type(sim_control_t), intent(in) :: control + type(bounds_t), intent(in) :: bounds + logical, intent(in) :: farFieldFlushRequested + real(KIND=RKIND_tiempo), pointer, dimension(:), intent(in) :: simulationTimeArray + integer, intent(in) :: simulationTimeIndex + integer :: outIdx + + fieldsPtr => fields + + do outIdx = 1, size(outputs) + select case (outputs(outIdx)%outputID) + case (POINT_PROBE_ID) + call flush_solver_output(outputs(outIdx)%pointProbe) + case (WIRE_CURRENT_PROBE_ID) + call flush_solver_output(outputs(outIdx)%wireCurrentProbe) + case (WIRE_CHARGE_PROBE_ID) + call flush_solver_output(outputs(outIdx)%wireChargeProbe) + case (BULK_PROBE_ID) + call flush_solver_output(outputs(outIdx)%bulkCurrentProbe) + case (MOVIE_PROBE_ID) + call flush_solver_output(outputs(outIdx)%movieProbe) + case (FREQUENCY_SLICE_PROBE_ID) + call flush_solver_output(outputs(outIdx)%frequencySliceProbe) + case (FAR_FIELD_PROBE_ID) + if (farFieldFlushRequested) call flush_solver_output(outputs(outIdx)%farFieldOutput, simulationTimeArray, simulationTimeIndex, control, fieldsPtr, bounds) + case default + end select + end do + end subroutine flush_outputs + + subroutine remove_unused_outputs(output_list) + implicit none + type(solver_output_t), pointer, intent(inout) :: output_list(:) + + type(solver_output_t), allocatable :: tmp(:) + integer :: i, n, k + + n = count(output_list%outputID /= UNDEFINED_PROBE) + + allocate (tmp(n)) + + ! Copy valid elements + k = 0 + do i = 1, size(output_list) + if (output_list(i)%outputID /= UNDEFINED_PROBE) then + k = k + 1 + tmp(k) = output_list(i) ! deep copy of all allocatable components + end if + end do + + ! Replace the saved pointer target safely + if (associated(output_list)) deallocate (output_list) + allocate (output_list(n)) + output_list = tmp + + end subroutine remove_unused_outputs + + subroutine close_outputs() + integer :: i + do i = 1, size(outputs) + select case (outputs(i)%outputID) + case (POINT_PROBE_ID) + case (WIRE_CURRENT_PROBE_ID) + case (WIRE_CHARGE_PROBE_ID) + case (BULK_PROBE_ID) + case (VOLUMIC_CURRENT_PROBE_ID) + case (MOVIE_PROBE_ID) + case (FREQUENCY_SLICE_PROBE_ID) + call close_solver_output(outputs(i)%frequencySliceProbe) + end select + end do + end subroutine + + subroutine create_pvd(pvdPath) + implicit none + character(len=*), intent(in) :: pvdPath + integer :: ios + integer :: unit + + open (newunit=unit, file=trim(pvdPath), status="replace", action="write", iostat=ios) + if (ios /= 0) stop "Error al crear archivo PVD" + + ! Escribimos encabezados XML + write (unit, *) '' + write (unit, *) '' + write (unit, *) ' ' + close (unit) + end subroutine create_pvd + + subroutine close_pvd(pvdPath) + implicit none + character(len=*), intent(in) :: pvdPath + integer :: unit + integer :: ios + open (newunit=unit, file=trim(pvdPath), status="old", action="write", iostat=ios) + if (ios /= 0) stop "Error al abrir archivo PVD" + write (unit, *) ' ' + write (unit, *) '' + close (unit) + end subroutine close_pvd + + function get_required_output_count(sgg) result(count) + type(SGGFDTDINFO_t), intent(in) :: sgg + integer(kind=SINGLE) ::i, count + count = 0 + do i = 1, sgg%NumberRequest + count = count + sgg%Observation(i)%nP + end do + return + end function + + subroutine registerOutputFiles(control, outputCount) + type(sim_control_t), intent(in) :: control + integer, intent(in) :: outputCount + + character(LEN=BUFSIZE) :: whoami, whoamishort, outputRequestFile + integer :: iostat, i, unit + + write (whoamishort, '(i5)') control%layoutnumber + 1 + write (whoami, '(a,i5,a,i5,a)') '(', control%layoutnumber + 1, '/', control%num_procs, ') ' + write (outputRequestFile, *) trim(adjustl(control%nEntradaRoot))//'_Outputrequests_'//trim(adjustl(whoamishort))//'.txt' + + call create_file_with_path(outputRequestFile, iostat) + if (iostat /= 0) call StopOnError(control%layoutnumber, control%num_procs, 'Error while creating new outputrequestRegister file...') + + open (newunit=unit, file=trim(adjustl(outputRequestFile)), status='replace', action='write', position='append', iostat=iostat) + do i = 1, outputCount + select case (outputs(i)%outputID) + case (POINT_PROBE_ID) + if (any(outputs(i)%pointProbe%domain%domainType == (/TIME_DOMAIN, BOTH_DOMAIN/))) then + write (unit, *) trim(adjustl(outputs(i)%pointProbe%filePathTime)) + end if + if (any(outputs(i)%pointProbe%domain%domainType == (/FREQUENCY_DOMAIN, BOTH_DOMAIN/))) then + write (unit, *) trim(adjustl(outputs(i)%pointProbe%filePathFreq)) + end if + case (WIRE_CURRENT_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%wireCurrentProbe%filePathTime)) + case (WIRE_CHARGE_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%wireChargeProbe%filePathTime)) + case (BULK_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%bulkCurrentProbe%filePathTime)) + case (MOVIE_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%movieProbe%filePathTime)) + case (FREQUENCY_SLICE_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%frequencySliceProbe%filePathFreq)) + case (FAR_FIELD_PROBE_ID) + write (unit, *) trim(adjustl(outputs(i)%farFieldOutput%filePathFreq)) + case (MAPVTK_ID) + write (unit, *) trim(adjustl(outputs(i)%mapvtkOutput%path)) + case default + call stoponerror(0, 0, 'Output update not implemented') + end select + end do + + write (unit, *) 'END!' + close (unit) + end subroutine + +end module output_m diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1cf9378e..8e559176 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,7 +23,13 @@ if (SEMBA_FDTD_ENABLE_SMBJSON) set(ROTATE_TESTS_LIBRARY rotate_tests) add_subdirectory(vtk) set(VTK_TESTS_LIBRARY vtk_tests) - if (NOT SEMBA_FDTD_ENABLE_MPI) + if (SEMBA_FDTD_ENABLE_OUTPUT_MODULE) + add_subdirectory(output) + set(OUTPUT_TESTS_LIBRARY output_tests) + set(VTK_API_TESTS_LIBRARY vtkAPI_tests) + set(XDMF_API_TESTS_LIBRARY xdmfAPI_tests) + endif() + if (NOT SEMBA_FDTD_ENABLE_MPI AND NOT SEMBA_FDTD_ENABLE_OUTPUT_MODULE) add_subdirectory(observation) set(OBSERVATION_TESTS_LIBRARY observation_tests) endif() @@ -47,7 +53,10 @@ target_link_libraries(fdtd_tests ${ROTATE_TESTS_LIBRARY} ${HDF_TESTS_LIBRARY} ${VTK_TESTS_LIBRARY} + ${OUTPUT_TESTS_LIBRARY} + ${VTK_API_TESTS_LIBRARY} + ${XDMF_API_TESTS_LIBRARY} ${SYSTEM_TESTS_LIBRARY} ${OBSERVATION_TESTS_LIBRARY} GTest::gtest_main -) \ No newline at end of file +) diff --git a/test/fdtd_tests.cpp b/test/fdtd_tests.cpp index 8c9aee1c..2c0e74b3 100644 --- a/test/fdtd_tests.cpp +++ b/test/fdtd_tests.cpp @@ -8,8 +8,13 @@ #include "smbjson/smbjson_tests.h" #include "rotate/rotate_tests.h" #include "vtk/vtk_tests.h" + #ifdef CompileWithNewOutputModule + #include "output/output_tests.h" + #include "output/vtkAPI_tests.h" + #include "output/xdmfAPI_tests.h" + #endif #endif -#ifndef CompileWithMPI +#if !defined(CompileWithMPI) && !defined(CompileWithNewOutputModule) #include "observation/observation_tests.h" #endif #include "conformal/conformal_tests.h" diff --git a/test/output/CMakeLists.txt b/test/output/CMakeLists.txt new file mode 100644 index 00000000..86270a9d --- /dev/null +++ b/test/output/CMakeLists.txt @@ -0,0 +1,51 @@ +message(STATUS "Creating build system for test/output") + +add_library( + output_test_fortran + "test_output.F90" + "test_output_utils.F90" + "test_volumic_utils.F90" +) + +add_library(vtkAPI_test_fortran + "test_vtkAPI.F90" +) + +add_library(xdmfAPI_test_fortran + "test_xdmfAPI.F90" +) + +add_library(output_tests "output_tests.cpp") +add_library(vtkAPI_tests "vtkAPI_tests.cpp") +add_library(xdmfAPI_tests "xdmfAPI_tests.cpp") + +target_link_libraries(output_test_fortran + semba-outputs + fdtd-output + test_utils_fortran +) +target_link_libraries(output_tests + output_test_fortran + GTest::gtest +) +target_link_libraries(vtkAPI_test_fortran + vtkAPI + test_utils_fortran +) +target_link_libraries(vtkAPI_tests + vtkAPI_test_fortran + GTest::gtest +) + +target_link_libraries(xdmfAPI_test_fortran + xdmfAPI + test_utils_fortran + ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} +) + +target_link_libraries(xdmfAPI_tests + xdmfAPI_test_fortran + GTest::gtest +) + +target_include_directories(xdmfAPI_test_fortran PRIVATE ${HDF5_INCLUDE_DIRS}) diff --git a/test/output/output_tests.cpp b/test/output/output_tests.cpp new file mode 100644 index 00000000..0dcc8252 --- /dev/null +++ b/test/output/output_tests.cpp @@ -0,0 +1 @@ +#include "output_tests.h" \ No newline at end of file diff --git a/test/output/output_tests.h b/test/output/output_tests.h new file mode 100644 index 00000000..6e35893d --- /dev/null +++ b/test/output/output_tests.h @@ -0,0 +1,36 @@ +#ifdef CompileWithNewOutputModule + +#include + +extern "C" int test_init_point_probe(); +extern "C" int test_update_point_probe(); +extern "C" int test_flush_point_probe(); +extern "C" int test_multiple_flush_point_probe(); +extern "C" int test_init_movie_probe(); +extern "C" int test_update_movie_probe(); +extern "C" int test_flush_movie_probe(); +extern "C" int test_init_frequency_slice_probe(); +extern "C" int test_update_frequency_slice_probe(); + +extern "C" int test_count_required_coords(); +extern "C" int test_store_required_coords(); +extern "C" int test_is_valid_point_current(); +extern "C" int test_is_valid_point_field(); + + +TEST(output, test_initialize_point_probe) {EXPECT_EQ(0, test_init_point_probe()); } +TEST(output, test_update_point_probe_info) {EXPECT_EQ(0, test_update_point_probe()); } +TEST(output, test_flush_point_probe_info) {EXPECT_EQ(0, test_flush_point_probe()); } +TEST(output, test_flush_multiple_point_probe_info) {EXPECT_EQ(0, test_multiple_flush_point_probe()); } +TEST(output, test_init_movie_probe_for_pec_surface) {EXPECT_EQ(0, test_init_movie_probe()); } +TEST(output, test_update_movie_probe_for_pec_surface) {EXPECT_EQ(0, test_update_movie_probe()); } +TEST(output, test_flush_movie_probe_data) {EXPECT_EQ(0, test_flush_movie_probe()); } +TEST(output, test_init_frequency_slice) {EXPECT_EQ(0, test_init_frequency_slice_probe()); } +TEST(output, test_update_frequency_slice) {EXPECT_EQ(0, test_update_frequency_slice_probe()); } + +TEST(output, test_volumic_utils_count) { EXPECT_EQ(0, test_count_required_coords()); } +TEST(output, test_volumic_utils_store) { EXPECT_EQ(0, test_store_required_coords()); } +TEST(output, test_volumic_utils_valid_current) { EXPECT_EQ(0, test_is_valid_point_current()); } +TEST(output, test_volumic_utils_valid_field) { EXPECT_EQ(0, test_is_valid_point_field()); } + +#endif \ No newline at end of file diff --git a/test/output/test_output.F90 b/test/output/test_output.F90 new file mode 100644 index 00000000..00ff2f87 --- /dev/null +++ b/test/output/test_output.F90 @@ -0,0 +1,1100 @@ +integer function test_init_point_probe() bind(c) result(err) +! This test initializes a single point probe at coordinates (4,4,4). +! It verifies that the probe is correctly registered in the simulation outputs, +! that the output ID matches POINT_PROBE_ID, and that the output paths for +! both the probe and its time data file are correctly set and exist. + use FDETYPES_m + use FDETYPES_TOOLS + use output_m + use outputTypes_m + use testOutputUtils_m + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + ! Parameters + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=18), parameter :: test_name = 'initPointProbeTest' + + ! Local variables + character(len=1) :: sep + character(len=BUFSIZE) :: nEntrada + character(len=BUFSIZE) :: expectedProbePath + character(len=BUFSIZE) :: expectedDataPath + + type(SGGFDTDINFO_t) :: sgg + type(sim_control_t) :: control + type(bounds_t) :: bounds + type(media_matrices_t) :: media + type(limit_t), allocatable :: sinpml(:) + type(Obses_t) :: probe + type(solver_output_t), pointer :: outputs(:) + type(MediaData_t), allocatable, target :: materials(:) + type(MediaData_t), pointer :: materialsPtr(:) + type(taglist_t) :: tagNumbers + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nSteps = 100_SINGLE + + logical :: outputRequested + logical :: hasWires = .false. + integer(kind=SINGLE) :: test_err = 0 + integer :: ios + + ! Setup + sep = get_path_separator() + nEntrada = test_folder//sep//test_name + + call sgg_init(sgg) + call init_time_array(timeArray, nSteps, dt) + call sgg_set_tiempo(sgg, timeArray) + call sgg_set_dt(sgg, dt) + + call init_simulation_material_list(materials) + materialsPtr => materials + call sgg_set_Med(sgg, materialsPtr) + + probe = create_point_probe_observation(4, 4, 4) + call sgg_add_observation(sgg, probe) + + control = create_control_flags(mpidir=3, nEntradaRoot=trim(nEntrada), wiresflavor='holland') + + ! Action + call init_outputs(sgg, media, sinpml, tagNumbers, bounds, control, outputRequested, hasWires) + outputs => GetOutputs() + + ! Assertions + test_err = test_err + assert_true(outputRequested, 'Valid probes not found') + test_err = test_err + assert_integer_equal(outputs(1)%outputID, POINT_PROBE_ID, 'Unexpected probe id') + + expectedProbePath = trim(nEntrada)//wordSeparation//'pointProbe_Ex_4_4_4' + expectedDataPath = trim(expectedProbePath)//wordSeparation//timeExtension//datFileExtension + + test_err = test_err + assert_string_equal(outputs(1)%pointProbe%path, expectedProbePath, 'Unexpected path') + test_err = test_err + assert_string_equal(outputs(1)%pointProbe%filePathTime, expectedDataPath, 'Unexpected path') + test_err = test_err + assert_true(file_exists(expectedDataPath), 'Time data file do not exist') + + ! Cleanup + call remove_folder(test_folder, ios) + deallocate (sgg%Observation, outputs) + + err = test_err +end function + +integer function test_update_point_probe() bind(c) result(err) +! This test updates the values recorded by a single point probe at (4,4,4) +! over two timesteps. It verifies that the probe correctly stores the time +! steps and associated field values, ensuring proper temporal tracking of +! measured quantities. + use FDETYPES_m + use FDETYPES_TOOLS + use output_m + use outputTypes_m + use testOutputUtils_m + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + ! Parameters + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=20), parameter :: test_name = 'updatePointProbeTest' + + ! Local variables + character(len=1) :: sep + character(len=BUFSIZE) :: nEntrada + + type(SGGFDTDINFO_t) :: sgg + type(sim_control_t) :: control + type(bounds_t) :: bounds + type(media_matrices_t) :: media + type(limit_t), allocatable :: sinpml(:) + type(Obses_t) :: probe + type(solver_output_t), pointer :: outputs(:) + type(MediaData_t), allocatable, target :: materials(:) + type(MediaData_t), pointer :: materialsPtr(:) + type(taglist_t) :: tagNumbers + + type(dummyFields_t), target :: dummyFields + type(fields_reference_t) :: fields + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nSteps = 100_SINGLE + + logical :: outputRequested + logical :: hasWires = .false. + integer(kind=SINGLE) :: test_err = 0 + integer :: ios + + ! Setup + sep = get_path_separator() + nEntrada = test_folder//sep//test_name + + call sgg_init(sgg) + call init_time_array(timeArray, nSteps, dt) + call sgg_set_tiempo(sgg, timeArray) + call sgg_set_dt(sgg, dt) + + probe = create_point_probe_observation(4, 4, 4) + call sgg_add_observation(sgg, probe) + + call init_simulation_material_list(materials) + materialsPtr => materials + call sgg_set_Med(sgg, materialsPtr) + + control = create_control_flags(mpidir=3, nEntradaRoot=nEntrada, wiresflavor='holland') + call init_outputs(sgg, media, sinpml, tagNumbers, bounds, control, outputRequested, hasWires) + + call create_dummy_fields(dummyFields, 1, 10, 0.01_RKIND) + + fields%E%x => dummyFields%Ex + fields%E%y => dummyFields%Ey + fields%E%z => dummyFields%Ez + fields%E%deltax => dummyFields%dxe + fields%E%deltaY => dummyFields%dye + fields%E%deltaZ => dummyFields%dze + + fields%H%x => dummyFields%Hx + fields%H%y => dummyFields%Hy + fields%H%z => dummyFields%Hz + fields%H%deltax => dummyFields%dxh + fields%H%deltaY => dummyFields%dyh + fields%H%deltaZ => dummyFields%dzh + + ! Action + dummyFields%Ex(4, 4, 4) = 5.0_RKIND + call update_outputs(control, sgg%tiempo, 1_SINGLE, fields) + outputs => GetOutputs() + + ! Assertions + test_err = test_err + assert_real_equal(outputs(1)%pointProbe%timeStep(1), 0.0_RKIND_tiempo, 1e-5_RKIND_tiempo, 'Unexpected timestep 1') + test_err = test_err + assert_real_equal(outputs(1)%pointProbe%valueForTime(1), 5.0_RKIND, 1e-5_RKIND, 'Unexpected field 1') + + dummyFields%Ex(4, 4, 4) = -4.0_RKIND + call update_outputs(control, sgg%tiempo, 2_SINGLE, fields) + + test_err = test_err + assert_real_equal(outputs(1)%pointProbe%timeStep(2), 0.1_RKIND_tiempo, 1e-5_RKIND_tiempo, 'Unexpected timestep 2') + test_err = test_err + assert_real_equal(outputs(1)%pointProbe%valueForTime(2), -4.0_RKIND, 1e-5_RKIND, 'Unexpected field 2') + + !Cleanup + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_flush_point_probe() bind(c) result(err) +! This test validates the flush operation for a point probe. It ensures +! that time and frequency data are properly written to files, and that +! internal arrays are cleared/reset after flushing, preserving data integrity. + use output_m + use outputTypes_m + use pointProbeOutput_m + use domain_m + use testOutputUtils_m + use assertionTools_m + use directoryUtils_m + implicit none + + ! Parameters + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=19), parameter :: test_name = 'flushPointProbeTest' + + ! Local variables + character(len=1) :: sep + character(len=BUFSIZE) :: nEntrada + + type(point_probe_output_t) :: probe + type(domain_t) :: domain + type(cell_coordinate_t) :: coordinates + + integer :: n, i + integer :: test_err = 0 + integer :: ios + + ! Setup + sep = get_path_separator() + nEntrada = test_folder//sep//test_name + + domain = domain_t( & + 0.0_RKIND_tiempo, 10.0_RKIND_tiempo, 0.1_RKIND_tiempo, & + 10.0_RKIND, 100.0_RKIND, 10, .false.) + + coordinates%x = 2 + coordinates%y = 2 + coordinates%z = 2 + + call init_point_probe_output(probe, coordinates, iEx, domain, nEntrada, 3, 0.1_RKIND_tiempo) + + ! Action + n = 10 + do i = 1, n + probe%timeStep(i) = real(i) + probe%valueForTime(i) = 10.0*i + probe%frequencySlice(i) = 0.1*i + probe%valueForFreq(i) = 0.2*i + end do + + probe%nTime = n + probe%nFreq = n + + call flush_point_probe_output(probe) + + ! Assertions + test_err = test_err + assert_written_output_file(probe%filePathTime) + test_err = test_err + assert_written_output_file(probe%filePathFreq) + + test_err = test_err + assert_integer_equal(probe%nTime, 0, 'ERROR: clear_time_data did not reset serializedTimeSize!') + + if (.not. all(probe%timeStep == 0.0) .or. .not. all(probe%valueForTime == 0.0)) then + print *, 'ERROR: time arrays not cleared!' + test_err = test_err + 1 + end if + + if (probe%nFreq == 0) then + print *, 'ERROR: Destroyed frequency reference!' + test_err = test_err + 1 + end if + + !Cleanup + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_multiple_flush_point_probe() bind(c) result(err) +! This test verifies that multiple consecutive flushes of a point probe +! correctly append or overwrite data files without losing previous data. +! It ensures consistency of both time and frequency outputs across multiple flushes. + use output_m + use outputTypes_m + use pointProbeOutput_m + use domain_m + use testOutputUtils_m + use assertionTools_m + use directoryUtils_m + implicit none + + ! Parameters + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=27), parameter :: test_name = 'flushMultiplePointProbeTest' + + ! Local variables + character(len=1) :: sep + character(len=BUFSIZE) :: nEntrada + + type(point_probe_output_t) :: probe + type(domain_t) :: domain + type(cell_coordinate_t) :: coordinates + + real(kind=RKIND), allocatable :: expectedTime(:, :) + real(kind=RKIND), allocatable :: expectedFreq(:, :) + + integer :: n, i, unit + integer :: test_err = 0 + integer :: ios + + ! Setup + sep = get_path_separator() + nEntrada = test_folder//sep//test_name + + domain = domain_t( & + 0.0_RKIND_tiempo, 10.0_RKIND_tiempo, 0.1_RKIND_tiempo, & + 10.0_RKIND, 100.0_RKIND, 10, .false.) + + coordinates%x = 2 + coordinates%y = 2 + coordinates%z = 2 + + call init_point_probe_output(probe, coordinates, iEx, domain, nEntrada, 3, 0.1_RKIND_tiempo) + + n = 10 + allocate (expectedTime(2*n, 2)) + allocate (expectedFreq(n, 2)) + + ! Action - first flush + do i = 1, n + probe%timeStep(i) = real(i) + probe%valueForTime(i) = 10.0*i + probe%frequencySlice(i) = 0.1*i + probe%valueForFreq(i) = 0.2*i + + expectedTime(i, 1) = real(i) + expectedTime(i, 2) = 10.0*i + + expectedFreq(i, 1) = 0.1*i + expectedFreq(i, 2) = 0.2*i + end do + + probe%nTime = n + probe%nFreq = n + call flush_point_probe_output(probe) + + ! Action - second flush + do i = 1, n + probe%timeStep(i) = real(i + 10) + probe%valueForTime(i) = 10.0*(i + 10) + probe%valueForFreq(i) = -0.5*i + + expectedTime(i + n, 1) = real(i + 10) + expectedTime(i + n, 2) = 10.0*(i + 10) + + expectedFreq(i, 1) = 0.1*i + expectedFreq(i, 2) = -0.5*i + end do + + probe%nTime = n + call flush_point_probe_output(probe) + + ! Assertions + unit = 1 + open (unit=unit, file=probe%filePathTime, status='old', action='read') + test_err = test_err + assert_file_content(unit, expectedTime, 2*n, 2, 1e-06_RKIND) + close (unit) + unit = 2 + open (unit=unit, file=probe%filePathFreq, status='old', action='read') + test_err = test_err + assert_file_content(unit, expectedFreq, n, 2, 1e-06_RKIND) + close (unit) + + !Cleanup + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_init_movie_probe() bind(c) result(err) +! This test initializes a movie probe over a 3D region from (2,2,2) to (5,5,5). +! It checks that the probe is correctly allocated, that the number of measurement +! points and buffer sizes are correct, and that the output folder and PVD file +! for the movie are properly created. + use output_m + use outputTypes_m + use testOutputUtils_m + use FDETYPES_TOOLS + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + type(SGGFDTDINFO_t) :: dummysgg + type(sim_control_t) :: dummyControl + type(bounds_t) :: dummyBound + type(XYZlimit_t) :: dummySweep(6) + type(XYZlimit_t) :: dummySinpmlSweep(6) + type(XYZlimit_t) :: allocationRange(6) + type(solver_output_t), pointer :: outputs(:) + + type(media_matrices_t), target :: media + type(media_matrices_t), pointer :: mediaPtr + + type(MediaData_t), allocatable, target :: simulationMaterials(:) + type(MediaData_t), pointer :: simulationMaterialsPtr(:) + + type(taglist_t) :: tagNumbers + + type(limit_t) :: sinpml(6) + + type(Obses_t) :: movieObservable + type(cell_coordinate_t) :: lowerBoundMovieProbe + type(cell_coordinate_t) :: upperBoundMovieProbe + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nTimeSteps = 100_SINGLE + + real(kind=RKIND), dimension(:), pointer :: x_steps, y_steps, z_steps + + integer(kind=SINGLE) :: expectedNumMeasurments + integer(kind=SINGLE) :: mpidir = 3 + logical :: ThereAreWires = .false. + logical :: outputRequested + integer(kind=SINGLE) :: iter + integer(kind=SINGLE) :: test_err = 0 + + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=9), parameter :: test_name = 'initMovie' + + character(len=BUFSIZE) :: nEntrada + character(len=1) :: sep + character(len=BUFSIZE) :: expectedProbePath + character(len=BUFSIZE) :: pdvFileName + integer :: ios + + sep = get_path_separator() + nEntrada = test_folder//sep//test_name + + err = 1 + + lowerBoundMovieProbe = cell_coordinate_t(2, 2, 2) + upperBoundMovieProbe = cell_coordinate_t(5, 5, 5) + + call sgg_init(dummysgg) + call init_time_array(timeArray, nTimeSteps, dt) + call sgg_set_tiempo(dummysgg, timeArray) + call sgg_set_dt(dummysgg, dt) + + call init_simulation_material_list(simulationMaterials) + simulationMaterialsPtr => simulationMaterials + call sgg_set_NumMedia(dummysgg, size(simulationMaterials)) + call sgg_set_Med(dummysgg, simulationMaterialsPtr) + dummySweep = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Sweep(dummysgg, dummySweep) + dummySinpmlSweep = create_xyz_limit_array(1, 1, 1, 5, 5, 5) + call sgg_set_SINPMLSweep(dummysgg, dummySinpmlSweep) + call sgg_set_NumPlaneWaves(dummysgg, 1) + allocationRange = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Alloc(dummysgg, allocationRange) + + allocate(x_steps(6),source=1.0_RKIND) + allocate(y_steps(6),source=1.0_RKIND) + allocate(z_steps(6),source=1.0_RKIND) + call sgg_set_LineX(dummysgg, x_steps) + call sgg_set_LineY(dummysgg, y_steps) + call sgg_set_LineZ(dummysgg, z_steps) + + movieObservable = create_movie_observation(2, 2, 2, 5, 5, 5, iCur) + call sgg_add_observation(dummysgg, movieObservable) + + call create_geometry_media(media, 0, 8, 0, 8, 0, 8) + + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 4, 3, simulationMaterials(0)%Id) + + expectedNumMeasurments = 4_SINGLE + mediaPtr => media + + do iter = 1, 6 + sinpml(iter) = create_limit_t(0, 8, 0, 8, 0, 8, 10, 10, 10) + end do + + dummyControl = create_control_flags(nEntradaRoot=nEntrada, mpidir=mpidir) + + call init_outputs(dummysgg, media, sinpml, tagNumbers, dummyBound, dummyControl, & + outputRequested, ThereAreWires) + + outputs => GetOutputs() + + test_err = test_err + assert_integer_equal(outputs(1)%outputID, MOVIE_PROBE_ID, 'Unexpected probe id') + test_err = test_err + assert_integer_equal(outputs(1)%movieProbe%nPoints, expectedNumMeasurments, 'Unexpected number of measurements') + test_err = test_err + assert_integer_equal(size(outputs(1)%movieProbe%xValueForTime), expectedNumMeasurments*BuffObse, 'Unexpected allocation size') + test_err = test_err + assert_integer_equal(size(outputs(1)%movieProbe%timeStep), BuffObse, 'Unexpected timestep buffer size') + + expectedProbePath = trim(nEntrada)//wordSeparation//'movieProbe_BC_2_2_2__5_5_5' + pdvFileName = trim(get_last_component(expectedProbePath))//pvdExtension + + test_err = test_err + assert_string_equal(outputs(1)%movieProbe%path, expectedProbePath, 'Unexpected path') + test_err = test_err + assert_true(folder_exists(expectedProbePath), 'Movie folder do not exist') + + !Cleanup + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_update_movie_probe() bind(c) result(err) +! This test updates a movie probe with field values at a single timestep. +! It verifies that the probe correctly stores the measured values in the x, y, +! and z components for all points, and that the timestep buffer is properly populated. + use output_m + use outputTypes_m + use testOutputUtils_m + use FDETYPES_TOOLS + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + type(SGGFDTDINFO_t) :: dummysgg + type(sim_control_t) :: dummyControl + type(bounds_t) :: dummyBound + type(solver_output_t), pointer :: outputs(:) + + type(media_matrices_t), target :: media + type(media_matrices_t), pointer :: mediaPtr + + type(MediaData_t), allocatable, target :: simulationMaterials(:) + type(MediaData_t), pointer :: simulationMaterialsPtr(:) + + type(limit_t), target :: sinpml_fullsize(6) + type(limit_t), pointer :: sinpml_fullsizePtr(:) + + type(taglist_t) :: tagNumbers + + type(XYZlimit_t) :: dummySweep(6) + type(XYZlimit_t) :: dummySinpmlSweep(6) + type(XYZlimit_t) :: allocationRange(6) + + type(Obses_t) :: movieObservable + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nTimeSteps = 100_SINGLE + + real(kind=RKIND), dimension(:), pointer :: x_steps, y_steps, z_steps + + type(dummyFields_t), target :: dummyFields + type(fields_reference_t) :: fields + + integer(kind=SINGLE) :: expectedNumMeasurments + integer(kind=SINGLE) :: mpidir = 3 + integer(kind=SINGLE) :: iter + integer(kind=SINGLE) :: test_err = 0 + logical :: ThereAreWires = .false. + logical :: outputRequested + + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=11), parameter :: test_name = 'updateMovie' + + character(len=BUFSIZE) :: nEntrada + integer :: ios + + nEntrada = join_path(test_folder, test_name) + + err = 1 + + call sgg_init(dummysgg) + call init_time_array(timeArray, nTimeSteps, dt) + call sgg_set_tiempo(dummysgg, timeArray) + call sgg_set_dt(dummysgg, dt) + + call init_simulation_material_list(simulationMaterials) + simulationMaterialsPtr => simulationMaterials + call sgg_set_NumMedia(dummysgg, size(simulationMaterials)) + call sgg_set_Med(dummysgg, simulationMaterialsPtr) + + dummySweep = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Sweep(dummysgg, dummySweep) + dummySinpmlSweep = create_xyz_limit_array(1, 1, 1, 5, 5, 5) + call sgg_set_SINPMLSweep(dummysgg, dummySinpmlSweep) + call sgg_set_NumPlaneWaves(dummysgg, 1) + allocationRange = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Alloc(dummysgg, allocationRange) + + allocate(x_steps(6),source=1.0_RKIND) + allocate(y_steps(6),source=1.0_RKIND) + allocate(z_steps(6),source=1.0_RKIND) + call sgg_set_LineX(dummysgg, x_steps) + call sgg_set_LineY(dummysgg, y_steps) + call sgg_set_LineZ(dummysgg, z_steps) + + movieObservable = create_movie_observation(2, 2, 2, 5, 5, 5, iCur) + call sgg_add_observation(dummysgg, movieObservable) + + call create_geometry_media(media, 0, 8, 0, 8, 0, 8) + + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 4, 3, simulationMaterials(0)%Id) + + expectedNumMeasurments = 4_SINGLE + mediaPtr => media + + do iter = 1, 6 + sinpml_fullsize(iter) = create_limit_t(0, 8, 0, 8, 0, 8, 10, 10, 10) + end do + sinpml_fullsizePtr => sinpml_fullsize + + dummyControl = create_control_flags(nEntradaRoot=nEntrada, mpidir=mpidir) + + call init_outputs(dummysgg, media, sinpml_fullsize, tagNumbers, dummyBound, dummyControl, & + outputRequested, ThereAreWires) + + outputs => GetOutputs() + + call create_dummy_fields(dummyFields, 1, 5, 0.1_RKIND) + + fields%E%x => dummyFields%Ex + fields%E%y => dummyFields%Ey + fields%E%z => dummyFields%Ez + fields%E%deltax => dummyFields%dxe + fields%E%deltaY => dummyFields%dye + fields%E%deltaZ => dummyFields%dze + fields%H%x => dummyFields%Hx + fields%H%y => dummyFields%Hy + fields%H%z => dummyFields%Hz + fields%H%deltax => dummyFields%dxh + fields%H%deltaY => dummyFields%dyh + fields%H%deltaZ => dummyFields%dzh + + dummyFields%Hx(3, 3, 3) = 2.0_RKIND + dummyFields%Hy(3, 3, 3) = 5.0_RKIND + dummyFields%Hz(3, 3, 3) = 4.0_RKIND + + call update_outputs(dummyControl, dummysgg%tiempo, 1_SINGLE, fields) + + test_err = test_err + assert_real_equal(outputs(1)%movieProbe%yValueForTime(1, 1), & + -0.2_RKIND, 1e-5_RKIND, 'Value error') + + test_err = test_err + assert_real_equal(outputs(1)%movieProbe%yValueForTime(1, 2), & + 0.4_RKIND, 1e-5_RKIND, 'Value error') + + test_err = test_err + assert_real_equal(outputs(1)%movieProbe%yValueForTime(1, 3), & + 0.0_RKIND, 1e-5_RKIND, 'Value error') + + test_err = test_err + assert_real_equal(outputs(1)%movieProbe%yValueForTime(1, 4), & + 0.0_RKIND, 1e-5_RKIND, 'Value error') + + test_err = test_err + assert_integer_equal( & + size(outputs(1)%movieProbe%timeStep), BuffObse, 'Unexpected timestep buffer size') + + !Cleanup + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_flush_movie_probe() bind(c) result(err) +! This test validates flushing movie probes to disk. It ensures that +! VTU files for each timestep and the PVD file are created, confirming that +! the temporal sequence of the movie probe is correctly serialized and saved. + use output_m + use outputTypes_m + use testOutputUtils_m + use FDETYPES_TOOLS + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + type(SGGFDTDINFO_t) :: dummysgg + type(sim_control_t) :: dummyControl + type(bounds_t) :: dummyBound + type(solver_output_t), pointer :: outputs(:) + + type(media_matrices_t), target :: media + type(media_matrices_t), pointer :: mediaPtr + + type(MediaData_t), allocatable, target :: simulationMaterials(:) + type(MediaData_t), pointer :: simulationMaterialsPtr(:) + + type(limit_t), target :: sinpml_fullsize(6) + type(limit_t), pointer :: sinpml_fullsizePtr(:) + + type(taglist_t) :: tagNumbers + + type(XYZlimit_t) :: dummySweep(6) + type(XYZlimit_t) :: dummySinpmlSweep(6) + type(XYZlimit_t) :: allocationRange(6) + + type(Obses_t) :: movieCurrentObservable + type(Obses_t) :: movieElectricXObservable + type(Obses_t) :: movieMagneticYObservable + type(fields_reference_t) :: fields + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nTimeSteps = 100_SINGLE + + real(kind=RKIND), dimension(:), pointer :: x_steps, y_steps, z_steps + + integer(kind=SINGLE) :: expectedNumMeasurments + integer(kind=SINGLE) :: mpidir = 3 + integer(kind=SINGLE) :: iter + integer(kind=SINGLE) :: test_err = 0 + logical :: ThereAreWires = .false. + logical :: outputRequested + + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=10), parameter :: test_name = 'flushMovie' + + character(len=BUFSIZE) :: nEntrada + character(len=BUFSIZE) :: expectedPath + integer :: outputIdx + integer :: ios + + nEntrada = join_path(test_folder, test_name) + + err = 1 + + call sgg_init(dummysgg) + call init_time_array(timeArray, nTimeSteps, dt) + call sgg_set_tiempo(dummysgg, timeArray) + call sgg_set_dt(dummysgg, dt) + + call init_simulation_material_list(simulationMaterials) + simulationMaterialsPtr => simulationMaterials + call sgg_set_NumMedia(dummysgg, size(simulationMaterials)) + call sgg_set_Med(dummysgg, simulationMaterialsPtr) + + dummySweep = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Sweep(dummysgg, dummySweep) + dummySinpmlSweep = create_xyz_limit_array(1, 1, 1, 5, 5, 5) + call sgg_set_SINPMLSweep(dummysgg, dummySinpmlSweep) + call sgg_set_NumPlaneWaves(dummysgg, 1) + allocationRange = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Alloc(dummysgg, allocationRange) + + allocate(x_steps(6),source=1.0_RKIND) + allocate(y_steps(6),source=1.0_RKIND) + allocate(z_steps(6),source=1.0_RKIND) + call sgg_set_LineX(dummysgg, x_steps) + call sgg_set_LineY(dummysgg, y_steps) + call sgg_set_LineZ(dummysgg, z_steps) + + movieCurrentObservable = create_movie_observation(2, 2, 2, 5, 5, 5, iCur) + call sgg_add_observation(dummysgg, movieCurrentObservable) + + movieElectricXObservable = create_movie_observation(2, 2, 2, 5, 5, 5, iExC) + call sgg_add_observation(dummysgg, movieElectricXObservable) + + movieMagneticYObservable = create_movie_observation(2, 2, 2, 5, 5, 5, iHyC) + call sgg_add_observation(dummysgg, movieMagneticYObservable) + + call create_geometry_media(media, 0, 8, 0, 8, 0, 8) + + call assign_material_id_to_media_matrix_coordinate(media, iEx, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iHy, 3, 3, 3, simulationMaterials(0)%Id) + + call assign_material_id_to_media_matrix_coordinate(media, iEx, 3, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iHy, 3, 4, 3, simulationMaterials(0)%Id) + + call assign_material_id_to_media_matrix_coordinate(media, iEx, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iHy, 4, 4, 3, simulationMaterials(0)%Id) + + call assign_material_id_to_media_matrix_coordinate(media, iEx, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iHy, 4, 3, 3, simulationMaterials(0)%Id) + + expectedNumMeasurments = 4_SINGLE + mediaPtr => media + + do iter = 1, 6 + sinpml_fullsize(iter) = create_limit_t(0, 8, 0, 8, 0, 8, 10, 10, 10) + end do + sinpml_fullsizePtr => sinpml_fullsize + + dummyControl = create_control_flags(nEntradaRoot=nEntrada, mpidir=mpidir) + + call init_outputs(dummysgg, media, sinpml_fullsize, tagNumbers, dummyBound, dummyControl, & + outputRequested, ThereAreWires) + + outputs => GetOutputs() + + !--- Dummy first update --- + !movieCurrentObservable + outputs(1)%movieProbe%nTime = 1 + outputs(1)%movieProbe%timeStep(1) = 0.5_RKIND_tiempo + outputs(1)%movieProbe%xValueForTime(1, :) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] + outputs(1)%movieProbe%yValueForTime(1, :) = [0.3_RKIND, 0.4_RKIND, 0.5_RKIND, 0.6_RKIND] + outputs(1)%movieProbe%zValueForTime(1, :) = [0.7_RKIND, 0.8_RKIND, 0.9_RKIND, 1.0_RKIND] + + !movieElectricXObservable + outputs(2)%movieProbe%nTime = 1 + outputs(2)%movieProbe%timeStep(1) = 0.5_RKIND_tiempo + outputs(2)%movieProbe%xValueForTime(1, :) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] + + !movieMagneticYObservable + outputs(3)%movieProbe%nTime = 1 + outputs(3)%movieProbe%timeStep(1) = 0.5_RKIND_tiempo + outputs(3)%movieProbe%yValueForTime(1, :) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] + + !--- Dummy second update --- + !movieCurrentObservable + outputs(1)%movieProbe%nTime = 2 + outputs(1)%movieProbe%timeStep(2) = 0.75_RKIND_tiempo + outputs(1)%movieProbe%xValueForTime(2, :) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] + outputs(1)%movieProbe%yValueForTime(2, :) = [1.3_RKIND, 1.4_RKIND, 1.5_RKIND, 1.6_RKIND] + outputs(1)%movieProbe%zValueForTime(2, :) = [1.7_RKIND, 1.8_RKIND, 1.9_RKIND, 2.0_RKIND] + + !movieElectricXObservable + outputs(2)%movieProbe%nTime = 2 + outputs(2)%movieProbe%timeStep(2) = 0.75_RKIND_tiempo + outputs(2)%movieProbe%xValueForTime(2, :) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] + + !movieMagneticYObservable + outputs(3)%movieProbe%nTime = 2 + outputs(3)%movieProbe%timeStep(2) = 0.75_RKIND_tiempo + outputs(3)%movieProbe%yValueForTime(2, :) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] + + call flush_outputs(dummysgg%tiempo, 1_SINGLE, dummyControl, fields, dummyBound, .false.) + + call close_outputs() + + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_init_frequency_slice_probe() bind(c) result(err) +! This test initializes a frequency slice probe over a 3D region (2,2,2) to (5,5,5). +! It verifies that the probe is correctly set up, that the expected number of measurement +! points and frequencies are allocated, and that the output folder and PVD file exist. + use output_m + use outputTypes_m + use testOutputUtils_m + use FDETYPES_TOOLS + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + type(SGGFDTDINFO_t) :: dummysgg + type(sim_control_t) :: dummyControl + type(bounds_t) :: dummyBound + type(solver_output_t), pointer :: outputs(:) + + type(media_matrices_t), target :: media + type(media_matrices_t), pointer :: mediaPtr + + type(MediaData_t), allocatable, target :: simulationMaterials(:) + type(MediaData_t), pointer :: simulationMaterialsPtr(:) + + type(taglist_t) :: tagNumbers + + type(limit_t), target :: sinpml_fullsize(6) + type(limit_t), pointer :: sinpml_fullsizePtr(:) + + type(XYZlimit_t) :: dummySweep(6) + type(XYZlimit_t) :: dummySinpmlSweep(6) + type(XYZlimit_t) :: allocationRange(6) + + type(Obses_t) :: frequencySliceObservation + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nTimeSteps = 100_SINGLE + + integer(kind=SINGLE) :: expectedNumMeasurments + integer(kind=SINGLE) :: expectedTotalFrequnecies + integer(kind=SINGLE) :: mpidir = 3 + integer(kind=SINGLE) :: iter + integer(kind=SINGLE) :: test_err = 0 + logical :: ThereAreWires = .false. + logical :: outputRequested + + + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=13), parameter :: test_name = 'initFrequency' + + character(len=BUFSIZE) :: nEntrada + character(len=BUFSIZE) :: expectedProbePath + character(len=BUFSIZE) :: pdvFileName + integer :: ios + + nEntrada = join_path(test_folder, test_name) + err = 1 + + call sgg_init(dummysgg) + + call init_time_array(timeArray, nTimeSteps, dt) + call sgg_set_tiempo(dummysgg, timeArray) + call sgg_set_dt(dummysgg, dt) + + call init_simulation_material_list(simulationMaterials) + simulationMaterialsPtr => simulationMaterials + call sgg_set_NumMedia(dummysgg, size(simulationMaterials)) + call sgg_set_Med(dummysgg, simulationMaterialsPtr) + + dummySweep = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Sweep(dummysgg, dummySweep) + dummySinpmlSweep = create_xyz_limit_array(1, 1, 1, 5, 5, 5) + call sgg_set_SINPMLSweep(dummysgg, dummySinpmlSweep) + call sgg_set_NumPlaneWaves(dummysgg, 1) + allocationRange = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Alloc(dummysgg, allocationRange) + + frequencySliceObservation = create_frequency_slice_observation(2, 2, 2, 5, 5, 5, iCur) + call sgg_add_observation(dummysgg, frequencySliceObservation) + + expectedTotalFrequnecies = 6_SINGLE + + call create_geometry_media(media, 0, 8, 0, 8, 0, 8) + + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 4, 3, simulationMaterials(0)%Id) + + expectedNumMeasurments = 4_SINGLE + mediaPtr => media + + do iter = 1, 6 + sinpml_fullsize(iter) = create_limit_t(0, 8, 0, 8, 0, 8, 10, 10, 10) + end do + sinpml_fullsizePtr => sinpml_fullsize + + dummyControl = create_control_flags(nEntradaRoot=nEntrada, mpidir=mpidir) + + call init_outputs(dummysgg, media, sinpml_fullsize, tagNumbers, dummyBound, dummyControl, & + outputRequested, ThereAreWires) + + outputs => GetOutputs() + + test_err = test_err + assert_integer_equal(outputs(1)%outputID, FREQUENCY_SLICE_PROBE_ID, 'Unexpected probe id') + + test_err = test_err + assert_integer_equal(outputs(1)%frequencySliceProbe%nFreq, 6, 'Unexpected number of frequencies') + + test_err = test_err + assert_integer_equal(outputs(1)%frequencySliceProbe%nPoints, expectedNumMeasurments, 'Unexpected number of measurements') + + test_err = test_err + assert_integer_equal( & + size(outputs(1)%frequencySliceProbe%xValueForFreq), & + expectedNumMeasurments*expectedTotalFrequnecies, 'Unexpected allocation size') + + test_err = test_err + assert_integer_equal( & + size(outputs(1)%frequencySliceProbe%frequencySlice), & + expectedTotalFrequnecies, 'Unexpected frequency count') + + expectedProbePath = trim(nEntrada)//wordSeparation//'frequencySliceProbe_BC_2_2_2__5_5_5' + pdvFileName = trim(get_last_component(expectedProbePath))//pvdExtension + + test_err = test_err + assert_string_equal(outputs(1)%frequencySliceProbe%path, expectedProbePath, 'Unexpected path') + test_err = test_err + assert_true(folder_exists(expectedProbePath), 'Frequency Slice folder do not exist') + + call remove_folder(test_folder, ios) + + err = test_err +end function + +integer function test_update_frequency_slice_probe() bind(c) result(err) +! This test updates a frequency slice probe with field gradients. +! It checks that no current is detected along the X-axis for H-field gradients +! and verifies the correct relation between Y and Z values (Y = -Z), ensuring +! correct handling of field distributions across the frequency slice. + use output_m + use outputTypes_m + use testOutputUtils_m + use FDETYPES_TOOLS + use sggMethods_m + use assertionTools_m + use directoryUtils_m + implicit none + + type(SGGFDTDINFO_t) :: dummysgg + type(sim_control_t) :: dummyControl + type(bounds_t) :: dummyBound + type(solver_output_t), pointer :: outputs(:) + + type(media_matrices_t), target :: media + type(media_matrices_t), pointer :: mediaPtr + + type(MediaData_t), allocatable, target :: simulationMaterials(:) + type(MediaData_t), pointer :: simulationMaterialsPtr(:) + + type(limit_t), target :: sinpml_fullsize(6) + type(limit_t), pointer :: sinpml_fullsizePtr(:) + + type(taglist_t) :: tagNumbers + + type(XYZlimit_t) :: dummySweep(6) + type(XYZlimit_t) :: dummySinpmlSweep(6) + type(XYZlimit_t) :: allocationRange(6) + + type(Obses_t) :: frequencySliceObservation + + real(kind=RKIND_tiempo), pointer :: timeArray(:) + real(kind=RKIND_tiempo) :: dt = 0.1_RKIND_tiempo + integer(kind=SINGLE) :: nTimeSteps = 100_SINGLE + + type(dummyFields_t), target :: dummyFields + type(fields_reference_t) :: fields + + integer(kind=SINGLE) :: expectedNumMeasurments + integer(kind=SINGLE) :: expectedNumberFrequencies + integer(kind=SINGLE) :: mpidir = 3 + integer(kind=SINGLE) :: iter + integer(kind=SINGLE) :: test_err = 0 + logical :: ThereAreWires = .false. + logical :: outputRequested + + + character(len=14), parameter :: test_folder = 'testing_folder' + character(len=13), parameter :: test_name = 'initFrequency' + + character(len=BUFSIZE) :: nEntrada + integer :: ios + + nEntrada = join_path(test_folder, test_name) + + err = 1 + + call sgg_init(dummysgg) + call init_time_array(timeArray, nTimeSteps, dt) + call sgg_set_tiempo(dummysgg, timeArray) + call sgg_set_dt(dummysgg, dt) + + call init_simulation_material_list(simulationMaterials) + simulationMaterialsPtr => simulationMaterials + call sgg_set_NumMedia(dummysgg, size(simulationMaterials)) + call sgg_set_Med(dummysgg, simulationMaterialsPtr) + + dummySweep = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Sweep(dummysgg, dummySweep) + dummySinpmlSweep = create_xyz_limit_array(1, 1, 1, 5, 5, 5) + call sgg_set_SINPMLSweep(dummysgg, dummySinpmlSweep) + call sgg_set_NumPlaneWaves(dummysgg, 1) + allocationRange = create_xyz_limit_array(0, 0, 0, 6, 6, 6) + call sgg_set_Alloc(dummysgg, allocationRange) + + frequencySliceObservation = create_frequency_slice_observation(2, 2, 2, 5, 5, 5, iCur) + call sgg_add_observation(dummysgg, frequencySliceObservation) + + call create_geometry_media(media, 0, 8, 0, 8, 0, 8) + + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 3, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 4, 4, 3, simulationMaterials(0)%Id) + call assign_material_id_to_media_matrix_coordinate(media, iEy, 3, 4, 3, simulationMaterials(0)%Id) + expectedNumberFrequencies = 6_SINGLE + expectedNumMeasurments = 4_SINGLE + mediaPtr => media + + do iter = 1, 6 + sinpml_fullsize(iter) = create_limit_t(0, 8, 0, 8, 0, 8, 10, 10, 10) + end do + sinpml_fullsizePtr => sinpml_fullsize + + dummyControl = create_control_flags(nEntradaRoot=nEntrada, mpidir=mpidir) + + call init_outputs(dummysgg, media, sinpml_fullsize, tagNumbers, dummyBound, dummyControl, & + outputRequested, ThereAreWires) + + outputs => GetOutputs() + + call create_dummy_fields(dummyFields, 1, 5, 0.1_RKIND) + + fields%E%x => dummyFields%Ex + fields%E%y => dummyFields%Ey + fields%E%z => dummyFields%Ez + fields%E%deltax => dummyFields%dxe + fields%E%deltaY => dummyFields%dye + fields%E%deltaZ => dummyFields%dze + fields%H%x => dummyFields%Hx + fields%H%y => dummyFields%Hy + fields%H%z => dummyFields%Hz + fields%H%deltax => dummyFields%dxh + fields%H%deltaY => dummyFields%dyh + fields%H%deltaZ => dummyFields%dzh + + call fillGradient(dummyFields, 1, 0.0_RKIND, 10.0_RKIND) + + call update_outputs(dummyControl, dummysgg%tiempo, 2_SINGLE, fields) + + test_err = test_err + assert_integer_equal(outputs(1)%outputID, & + FREQUENCY_SLICE_PROBE_ID, 'Unexpected probe id') + + test_err = test_err + assert_integer_equal(outputs(1)%frequencySliceProbe%nPoints, & + expectedNumMeasurments, 'Unexpected number of measurements') + + test_err = test_err + assert_integer_equal( & + size(outputs(1)%frequencySliceProbe%frequencySlice), & + expectedNumberFrequencies, 'Unexpected allocation size') + + !This test generates X Gradient for H. It is expected to detect none Current accros X axis and Opposite values for Y and Z + + test_err = test_err + assert_array_value(outputs(1)%frequencySliceProbe%xValueForFreq, (0.0_CKIND , 0.0_CKIND), errormessage='Detected Current on X Axis for Hx gradient') + test_err = test_err + assert_arrays_equal(outputs(1)%frequencySliceProbe%yValueForFreq, & + -1.0_RKIND*outputs(1)%frequencySliceProbe%zValueForFreq, errormessage='Unequal values for Y and -Z') + + + call remove_folder(test_folder, ios) + + err = test_err +end function diff --git a/test/output/test_output_utils.F90 b/test/output/test_output_utils.F90 new file mode 100644 index 00000000..2b192ca9 --- /dev/null +++ b/test/output/test_output_utils.F90 @@ -0,0 +1,236 @@ +module testOutputUtils_m + use FDETYPES_m + use FDETYPES_TOOLS + use outputTypes_m + implicit none + private + + !=========================== + ! Public interface summary + !=========================== + public :: dummyFields_t + public :: create_point_probe_observation + public :: create_volumic_probe_observation + public :: create_movie_observation + public :: create_frequency_slice_observation + public :: create_dummy_fields + public :: fillGradient + public :: setup_dummy_problem_info + public :: clean_dummy_problem_info + !=========================== + + ! Storage for dummy targets + type(media_matrices_t), target :: dummyGeometry + type(limit_t), allocatable, target :: dummyProblemDim(:) + type(MediaData_t), allocatable, target :: dummyMaterialList(:) + type(bounds_t), target :: dummyBounds + + !=========================== + ! Private interface summary + !=========================== + + !=========================== + + type :: dummyFields_t + real(kind=RKIND), allocatable, dimension(:, :, :) :: Ex, Ey, Ez, Hx, Hy, Hz + real(kind=RKIND), allocatable, dimension(:) :: dxe, dye, dze, dxh, dyh, dzh + contains + procedure, public :: createDummyFields => create_dummy_fields + end type dummyFields_t +contains + function create_point_probe_observation(x, y, z) result(obs) + integer, intent(in) :: x, y, z + type(Obses_t) :: obs + + type(observable_t), dimension(:), allocatable :: P + type(observation_domain_t) :: domain + + allocate (P(1)) + P(1) = create_observable(x, y, z, x, y, z, iEx) + call initialize_observation_time_domain(domain, 0.0_RKIND, 10.0_RKIND, 0.1_RKIND) + + call set_observation(obs, P, 'pointProbe', domain, 'DummyFileNormalize') + end function + + function create_bulk_probe_observation(xi, yi, zi) result(obs) + integer, intent(in) :: xi, yi, zi + type(Obses_t) :: obs + + type(observable_t), dimension(:), allocatable :: P + type(observation_domain_t) :: domain + + allocate (P(1)) + P(1) = create_observable(xi, yi, zi, xi+1, yi+1, zi+1, iCurX) + + call initialize_observation_time_domain(domain, 0.0_RKIND, 10.0_RKIND, 0.1_RKIND) + call initialize_observation_frequency_domain(domain, 0.0_RKIND, 1000.0_RKIND, 50.0_RKIND) + + call set_observation(obs, P, 'bulkProbe', domain, 'DummyFileNormalize') + end function create_bulk_probe_observation + + function create_volumic_probe_observation(xi, yi, zi, xe, ye, ze) result(obs) + integer, intent(in) :: xi, yi, zi, xe, ye, ze + type(Obses_t) :: obs + + type(observable_t), dimension(:), allocatable :: P + type(observation_domain_t) :: domain + + allocate (P(1)) + P(1) = create_observable(xi, yi, zi, xe, ye, ze, iCurX) + + call initialize_observation_time_domain(domain, 0.0_RKIND, 10.0_RKIND, 0.1_RKIND) + call initialize_observation_frequency_domain(domain, 0.0_RKIND, 1000.0_RKIND, 50.0_RKIND) + + call set_observation(obs, P, 'volumicProbe', domain, 'DummyFileNormalize') + end function create_volumic_probe_observation + + function create_movie_observation(xi, yi, zi, xe, ye, ze, request) result(observation) + integer, intent(in) :: xi, yi, zi, xe, ye, ze, request + type(Obses_t) :: observation + + type(observable_t), dimension(:), allocatable :: P + type(observation_domain_t) :: domain + + allocate (P(1)) + P(1) = create_observable(xi, yi, zi, xe, ye, ze, request) + call initialize_observation_time_domain(domain, 0.0_RKIND, 10.0_RKIND, 0.1_RKIND) + + call set_observation(observation, P, 'movieProbe', domain, 'DummyFileNormalize') + end function create_movie_observation + + function create_frequency_slice_observation(xi, yi, zi, xe, ye, ze, request) result(observation) + integer, intent(in) :: xi, yi, zi, xe, ye, ze, request + type(Obses_t) :: observation + + type(observable_t), dimension(:), allocatable :: P + type(observation_domain_t) :: domain + + allocate (P(1)) + P(1) = create_observable(xi, yi, zi, xe, ye, ze, request) + call initialize_observation_frequency_domain(domain, 0.0_RKIND, 100.0_RKIND, 20.0_RKIND) + + call set_observation(observation, P, 'frequencySliceProbe', domain, 'DummyFileNormalize') + end function create_frequency_slice_observation + + subroutine create_dummy_fields(this, lower, upper, delta) + class(dummyFields_t), intent(inout) :: this + integer, intent(in) :: lower, upper + real(kind=rkind), intent(in) :: delta + allocate ( & + this%Ex(lower:upper, lower:upper, lower:upper), & + this%Ey(lower:upper, lower:upper, lower:upper), & + this%Ez(lower:upper, lower:upper, lower:upper), & + this%Hx(lower:upper, lower:upper, lower:upper), & + this%Hy(lower:upper, lower:upper, lower:upper), & + this%Hz(lower:upper, lower:upper, lower:upper) & + ) + + this%Ex = 0.0_RKIND + this%Ey = 0.0_RKIND + this%Ez = 0.0_RKIND + this%Hx = 0.0_RKIND + this%Hy = 0.0_RKIND + this%Hz = 0.0_RKIND + + allocate ( & + this%dxh(lower:upper), & + this%dyh(lower:upper), & + this%dzh(lower:upper), & + this%dxe(lower:upper), & + this%dye(lower:upper), & + this%dze(lower:upper) & + ) + this%dxh = delta + this%dyh = delta + this%dzh = delta + this%dxe = delta + this%dye = delta + this%dze = delta + end subroutine create_dummy_fields + + subroutine fillGradient(dummyFields, direction, minVal, maxVal) + !-------------------------------------------- + ! Fills dummyFields%Hx, Hy, Hz with a linear gradient + ! along the specified direction (1=x, 2=y, 3=z) + !-------------------------------------------- + implicit none + type(dummyFields_t), intent(inout) :: dummyFields + integer, intent(in) :: direction ! 1=x, 2=y, 3=z + real(RKIND), intent(in) :: minVal, maxVal + + integer :: i, j, k + integer :: nx, ny, nz + real(RKIND) :: factor + + ! Get array sizes + nx = size(dummyFields%Hx, 1) + ny = size(dummyFields%Hx, 2) + nz = size(dummyFields%Hx, 3) + + select case (direction) + case (1) ! x-direction + do i = 1, nx + factor = real(i - 1, RKIND)/real(nx - 1, RKIND) + dummyFields%Hx(i, :, :) = minVal + factor*(maxVal - minVal) + dummyFields%Hy(i, :, :) = minVal + factor*(maxVal - minVal) + dummyFields%Hz(i, :, :) = minVal + factor*(maxVal - minVal) + end do + case (2) ! y-direction + do j = 1, ny + factor = real(j - 1, RKIND)/real(ny - 1, RKIND) + dummyFields%Hx(:, j, :) = minVal + factor*(maxVal - minVal) + dummyFields%Hy(:, j, :) = minVal + factor*(maxVal - minVal) + dummyFields%Hz(:, j, :) = minVal + factor*(maxVal - minVal) + end do + case (3) ! z-direction + do k = 1, nz + factor = real(k - 1, RKIND)/real(nz - 1, RKIND) + dummyFields%Hx(:, :, k) = minVal + factor*(maxVal - minVal) + dummyFields%Hy(:, :, k) = minVal + factor*(maxVal - minVal) + dummyFields%Hz(:, :, k) = minVal + factor*(maxVal - minVal) + end do + case default + print *, "Error: direction must be 1, 2, or 3." + end select + + end subroutine fillGradient + + !-------------------------------------------------------------------------------- + ! Setup/Teardown + !-------------------------------------------------------------------------------- + subroutine setup_dummy_problem_info(problemInfo) + type(problem_info_t), intent(out) :: problemInfo + + integer :: i + + ! Create a 11x11x11 grid (0..10) + if (allocated(dummyProblemDim)) deallocate(dummyProblemDim) + allocate(dummyProblemDim(6)) + do i = 1,6 + dummyProblemDim(i) = create_limit_t(0, 10, 0, 10, 0, 10, 1, 1, 1) + end do + problemInfo%problemDimension => dummyProblemDim + + call create_geometry_media(dummyGeometry, 0, 10, 0, 10, 0, 10) + problemInfo%geometryToMaterialData => dummyGeometry + + call init_simulation_material_list(dummyMaterialList) + problemInfo%materialList => dummyMaterialList + + problemInfo%simulationBounds => dummyBounds + + end subroutine setup_dummy_problem_info + + subroutine clean_dummy_problem_info(problemInfo) + type(problem_info_t), intent(inout) :: problemInfo + + if (allocated(dummyProblemDim)) deallocate(dummyProblemDim) + if (allocated(dummyMaterialList)) deallocate(dummyMaterialList) + + nullify(problemInfo%problemDimension) + nullify(problemInfo%geometryToMaterialData) + nullify(problemInfo%materialList) + nullify(problemInfo%simulationBounds) + end subroutine clean_dummy_problem_info + +end module testOutputUtils_m diff --git a/test/output/test_volumic_utils.F90 b/test/output/test_volumic_utils.F90 new file mode 100644 index 00000000..08475374 --- /dev/null +++ b/test/output/test_volumic_utils.F90 @@ -0,0 +1,138 @@ +!-------------------------------------------------------------------------------- +! Test: count_required_coords +!-------------------------------------------------------------------------------- +integer function test_count_required_coords() bind(c) result(err) + use FDETYPES_m + use outputTypes_m + use volumicProbeUtils_m + use assertionTools_m + use testOutputUtils_m + implicit none + + type(cell_coordinate_t) :: lowerBound, upperBound + type(problem_info_t) :: problemInfo + integer(kind=SINGLE) :: count + integer :: test_err = 0 + integer, allocatable :: dummy_coords(:,:) + + ! Setup test case: 3x3x3 domain (1..3) + lowerBound = cell_coordinate_t(1, 1, 1) + upperBound = cell_coordinate_t(3, 3, 3) + + call setup_dummy_problem_info(problemInfo) + + ! Test Case 1: Field Request (iExC) + call find_and_store_important_coords(lowerBound, upperBound, iExC, problemInfo, count, dummy_coords) + + ! Expected: 3*3*3 = 27 points + test_err = test_err + assert_integer_equal(count, 27_SINGLE, "Failed count for iExC") + + if (allocated(dummy_coords)) deallocate(dummy_coords) + call clean_dummy_problem_info(problemInfo) + err = test_err +end function test_count_required_coords + +!-------------------------------------------------------------------------------- +! Test: store_required_coords +!-------------------------------------------------------------------------------- +integer function test_store_required_coords() bind(c) result(err) + use FDETYPES_m + use outputTypes_m + use outputUtils_m + use volumicProbeUtils_m + use assertionTools_m + use testOutputUtils_m + implicit none + + type(cell_coordinate_t) :: lowerBound, upperBound + type(problem_info_t) :: problemInfo + integer(kind=SINGLE) :: nPoints + integer(kind=SINGLE), allocatable :: stored_coords(:,:) + integer :: test_err = 0 + + lowerBound = new_cell_coordinate(1, 1, 1) + upperBound = new_cell_coordinate(2, 2, 2) + call setup_dummy_problem_info(problemInfo) + + call find_and_store_important_coords(lowerBound, upperBound, iHyC, problemInfo, nPoints, stored_coords) + + test_err = test_err + assert_integer_equal(nPoints, 8_SINGLE, "Failed nPoints for iHyC") + + if (allocated(stored_coords)) then + test_err = test_err + assert_integer_equal(int(size(stored_coords, 2), SINGLE), 8_SINGLE, "Allocated coords size error") + ! Verify first coord is (1,1,1) + test_err = test_err + assert_integer_equal(stored_coords(1,1), 1_SINGLE, "First x coord mismatch") + test_err = test_err + assert_integer_equal(stored_coords(2,1), 1_SINGLE, "First y coord mismatch") + test_err = test_err + assert_integer_equal(stored_coords(3,1), 1_SINGLE, "First z coord mismatch") + deallocate(stored_coords) + else + print *, "Coords not allocated." + test_err = test_err + 1 + end if + + call clean_dummy_problem_info(problemInfo) + err = test_err +end function test_store_required_coords + +!-------------------------------------------------------------------------------- +! Test: isValidPointForCurrent +!-------------------------------------------------------------------------------- +integer function test_is_valid_point_current() bind(c) result(err) + use FDETYPES_m + use outputTypes_m + use volumicProbeUtils_m + use testOutputUtils_m + implicit none + + type(problem_info_t) :: problemInfo + integer :: test_err = 0 + logical :: valid + + call setup_dummy_problem_info(problemInfo) + + ! By default, our dummy setup has NO PEC and NO Wires. + ! So isValidPointForCurrent should be FALSE (as it requires PEC or Wire) + valid = isValidPointForCurrent(iCur, 1, 1, 1, problemInfo) + + if (valid) then + print *, "Expected False for empty space current probe (no PEC/Wire)" + test_err = test_err + 1 + end if + + call clean_dummy_problem_info(problemInfo) + err = test_err +end function test_is_valid_point_current + +!-------------------------------------------------------------------------------- +! Test: isValidPointForField +!-------------------------------------------------------------------------------- +integer function test_is_valid_point_field() bind(c) result(err) + use FDETYPES_m + use outputTypes_m + use volumicProbeUtils_m + use testOutputUtils_m + implicit none + + type(problem_info_t) :: problemInfo + integer :: test_err = 0 + logical :: valid + + call setup_dummy_problem_info(problemInfo) + + ! Point inside boundary + valid = isValidPointForField(iEx, 5, 5, 5, problemInfo) + if (.not. valid) then + print *, "Expected True for field probe in bounds" + test_err = test_err + 1 + end if + + ! Point outside boundary (-1) + valid = isValidPointForField(iEx, -1, 5, 5, problemInfo) + if (valid) then + print *, "Expected False for field probe out of bounds" + test_err = test_err + 1 + end if + + call clean_dummy_problem_info(problemInfo) + err = test_err +end function test_is_valid_point_field diff --git a/test/output/test_vtkAPI.F90 b/test/output/test_vtkAPI.F90 new file mode 100644 index 00000000..7564dd90 --- /dev/null +++ b/test/output/test_vtkAPI.F90 @@ -0,0 +1,396 @@ +!============================== +! vtkAPI_m extended testsuite (class-based) +!============================== + +!============================== +! Test 1: Structured grid basic allocation +!============================== +integer function test_vtkAPI_points_allocation() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: points(:, :) + integer :: nx, ny, nz + + error_cnt = 0 + nx = 2; ny = 2; nz = 2 + grid%nx = nx; grid%ny = ny; grid%nz = nz + grid_base => grid + + allocate(points(3, nx*ny*nz)) + points = 0.0 + call grid_base%add_points(points) + + if (.not. allocated(grid%points)) error_cnt = error_cnt + 1 + if (size(grid%points,1) /= 3) error_cnt = error_cnt + 1 + if (size(grid%points,2) /= nx*ny*nz) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 2: Point scalar assignment +!============================== +integer function test_vtkAPI_point_scalar() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: scalars(:) + integer :: nx=2, ny=2, nz=2, i + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + allocate(scalars(nx*ny*nz)) + do i=1,nx*ny*nz + scalars(i) = real(i) + end do + call grid_base%add_scalar('Density', scalars) + + if (.not. allocated(grid%scalars)) error_cnt = error_cnt + 1 + if (grid%scalars(1)%data(1) /= 1.0) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 3: Point vector assignment +!============================== +integer function test_vtkAPI_point_vector() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: vec(:) + integer :: nx=2, ny=2, nz=2, i + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + allocate(vec(3*nx*ny*nz)) + do i=1,nx*ny*nz + vec(3*i-2) = real(i) + vec(3*i-1) = real(i*10) + vec(3*i) = real(i*100) + end do + call grid_base%add_vector('Momentum', vec) + + if (.not. allocated(grid%vectors)) error_cnt = error_cnt + 1 + if (grid%vectors(1)%data(2) /= 10.0) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 4: Cell scalar assignment +!============================== +integer function test_vtkAPI_cell_scalar() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: cell_data(:) + integer :: nx=2, ny=2, nz=2, n + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + allocate(cell_data((nx-1)*(ny-1)*(nz-1))) + do n=1,(nx-1)*(ny-1)*(nz-1) + cell_data(n) = real(n) + end do + call grid_base%add_cell_scalar('Pressure', cell_data) + + if (.not. allocated(grid%cell_scalars)) error_cnt = error_cnt + 1 + if (grid%cell_scalars(1)%data(1) /= 1.0) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 5: Cell vector assignment +!============================== +integer function test_vtkAPI_cell_vector() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: vec(:) + integer :: nx=2, ny=2, nz=2, n + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + allocate(vec(3*(nx-1)*(ny-1)*(nz-1))) + do n=1,(nx-1)*(ny-1)*(nz-1) + vec(3*n-2) = real(n) + vec(3*n-1) = real(n*10) + vec(3*n) = real(n*100) + end do + call grid_base%add_cell_vector('Flux', vec) + + if (.not. allocated(grid%cell_vectors)) error_cnt = error_cnt + 1 + if (grid%cell_vectors(1)%data(3) /= 100.0) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 6: VTS file creation +!============================== +integer function test_vtkAPI_vts_file_creation() bind(C) result(error_cnt) + use vtkAPI_m + use directoryUtils_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: points(:, :), scalars(:) + integer :: nx=2, ny=2, nz=2 + integer :: ierr + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + allocate(points(3, nx*ny*nz)); points = 0.0 + call grid_base%add_points(points) + + allocate(scalars(nx*ny*nz)); scalars = 1.0 + call grid_base%add_scalar('Density', scalars) + + file = join_path(folder, 'test.vts') + call create_folder(folder, ierr) + call grid_base%write_file(file) + + open(unit=10, file=file, status='old', action='read', iostat=ierr) + if (ierr /= 0) then + error_cnt = error_cnt + 1 + else + close(10) + end if + call remove_folder(folder, ierr) +end function + +!============================== +! Test 7: VTU file creation with cells and point data +!============================== +integer function test_vtkAPI_vtu_file_creation() bind(C) result(error_cnt) + use vtkAPI_m + use directoryUtils_m + implicit none + type(vtk_unstructured_grid), target :: ugrid + class(vtk_grid), pointer :: grid_base + real, allocatable :: points(:, :), scalars(:) + integer, allocatable :: conn(:), offsets(:), types(:) + integer :: ierr + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + + error_cnt = 0 + grid_base => ugrid + + ! Points + ugrid%num_points = 4 + allocate(points(3,4)) + points(:,1)=(/0.0,0.0,0.0/) + points(:,2)=(/1.0,0.0,0.0/) + points(:,3)=(/0.0,1.0,0.0/) + points(:,4)=(/0.0,0.0,1.0/) + call grid_base%add_points(points) + + ! Cells + allocate(conn(4)); conn = (/0,1,2,3/) + allocate(offsets(1)); offsets = (/4/) + allocate(types(1)); types = (/10/) + call ugrid%add_cell_connectivity(conn, offsets, types) + + ! Scalars + allocate(scalars(4)); scalars = (/1.0,2.0,3.0,4.0/) + call grid_base%add_scalar('Velocity', scalars) + + file = join_path(folder, 'test.vtu') + call create_folder(folder, ierr) + call grid_base%write_file(file) + + open(unit=10, file=file, status='old', action='read', iostat=ierr) + if (ierr /= 0) then + error_cnt = error_cnt + 1 + else + close(10) + end if + call remove_folder(folder, ierr) +end function + +!============================== +! Test 8: VTU file with cell data +!============================== +integer function test_vtkAPI_vtu_cell_data() bind(C) result(error_cnt) + use vtkAPI_m + implicit none + type(vtk_unstructured_grid), target :: ugrid + class(vtk_grid), pointer :: grid_base + real, allocatable :: cell_scalars(:), cell_vectors(:) + integer, allocatable :: conn(:), offsets(:), types(:) + + error_cnt = 0 + grid_base => ugrid + + ! Cells + ugrid%num_cells = 1 + allocate(conn(4)); conn = (/0,1,2,3/) + allocate(offsets(1)); offsets = (/4/) + allocate(types(1)); types = (/10/) + call ugrid%add_cell_connectivity(conn, offsets, types) + + ! Cell scalar + allocate(cell_scalars(1)); cell_scalars(1) = 5.0 + call grid_base%add_cell_scalar('Pressure', cell_scalars) + if (ugrid%cell_scalars(1)%data(1) /= 5.0) error_cnt = error_cnt + 1 + + ! Cell vector + allocate(cell_vectors(3*1)); cell_vectors = (/1.0,2.0,3.0/) + call grid_base%add_cell_vector('Flux', cell_vectors) + if (ugrid%cell_vectors(1)%data(3) /= 3.0) error_cnt = error_cnt + 1 +end function + +!============================== +! Test 9: Verificación de VTS contenido +!============================== +integer function test_vtkAPI_vts_content() bind(C) result(error_cnt) + use vtkAPI_m + use directoryUtils_m + implicit none + type(vtk_structured_grid), target :: grid + class(vtk_grid), pointer :: grid_base + real, allocatable :: points(:, :), scalars(:), vectors(:) + integer :: nx=2, ny=2, nz=2 + integer :: ierr, i + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + character(len=256) :: line + logical :: found_scalar, found_vector + + error_cnt = 0 + grid%nx=nx; grid%ny=ny; grid%nz=nz + grid_base => grid + + ! Points + allocate(points(3, nx*ny*nz)); points=0.0 + call grid_base%add_points(points) + + ! Scalar + allocate(scalars(nx*ny*nz)) + do i=1,nx*ny*nz + scalars(i) = real(i) + end do + call grid_base%add_scalar('Density', scalars) + + ! Vector + allocate(vectors(3*nx*ny*nz)) + do i=1,nx*ny*nz + vectors(3*i-2) = real(i) + vectors(3*i-1) = real(i*10) + vectors(3*i) = real(i*100) + end do + call grid_base%add_vector('Momentum', vectors) + + ! Create VTS file + file = join_path(folder,'test_content.vts') + call create_folder(folder,ierr) + call grid_base%write_file(file) + + ! Read file and verify PointData + found_scalar = .false.; found_vector = .false. + open(unit=10, file=file, status='old', action='read', iostat=ierr) + if (ierr /= 0) then + error_cnt = error_cnt + 1 + return + end if + + do + read(10,'(A)', iostat=ierr) line + if (ierr /= 0) exit + if (index(line,'Scalars="Density"') /= 0) found_scalar = .true. + if (index(line,'Vectors="Momentum"') /= 0) found_vector = .true. + if (found_scalar .and. found_vector) exit + end do + close(10) + + if (.not. found_scalar) error_cnt = error_cnt + 1 + if (.not. found_vector) error_cnt = error_cnt + 1 + + call remove_folder(folder, ierr) +end function + +!============================== +! Test 10 +!============================== +integer function test_vtkAPI_vtu_content() bind(C) result(error_cnt) + use vtkAPI_m + use directoryUtils_m + implicit none + type(vtk_unstructured_grid), target :: ugrid + class(vtk_grid), pointer :: grid_base + real, allocatable :: points(:, :), scalars(:), cell_scalars(:) + integer, allocatable :: conn(:), offsets(:), types(:) + integer :: ierr + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + character(len=256) :: line + logical :: found_point_scalar, found_cell_scalar, found_cells, found_points + + error_cnt = 0 + grid_base => ugrid + + ! Points + ugrid%num_points = 4 + allocate(points(3,4)) + points(:,1)=(/0.0,0.0,0.0/); points(:,2)=(/1.0,0.0,0.0/) + points(:,3)=(/0.0,1.0,0.0/); points(:,4)=(/0.0,0.0,1.0/) + call grid_base%add_points(points) + + ! Cells + ugrid%num_cells = 1 + allocate(conn(4)); conn=(/0,1,2,3/) + allocate(offsets(1)); offsets=(/4/) + allocate(types(1)); types=(/10/) + call ugrid%add_cell_connectivity(conn, offsets, types) + + ! Point scalarll + allocate(scalars(4)); scalars=(/1.0,2.0,3.0,4.0/) + call grid_base%add_scalar('Velocity', scalars) + + ! Cell scalar + allocate(cell_scalars(1)); cell_scalars=(/5.0/) + call grid_base%add_cell_scalar('Pressure', cell_scalars) + + ! Create VTU file + file = join_path(folder,'test_content.vtu') + call create_folder(folder,ierr) + call grid_base%write_file(file) + + ! Read file + found_point_scalar = .false.; found_cell_scalar = .false. + found_cells = .false.; found_points = .false. + open(unit=10, file=file, status='old', action='read', iostat=ierr) + if (ierr /= 0) then + error_cnt = error_cnt + 1 + return + end if + + do + read(10,'(A)', iostat=ierr) line + if (ierr /= 0) exit + if (index(line,'PointData') /= 0) found_point_scalar = .true. + if (index(line,'CellData') /= 0) found_cell_scalar = .true. + if (index(line,'') /= 0) found_cells = .true. + if (index(line,'') /= 0) found_points = .true. + if (found_point_scalar .and. found_cell_scalar .and. found_cells .and. found_points) exit + end do + close(10) + + if (.not. found_point_scalar) error_cnt = error_cnt + 1 + if (.not. found_cell_scalar) error_cnt = error_cnt + 1 + if (.not. found_cells) error_cnt = error_cnt + 1 + if (.not. found_points) error_cnt = error_cnt + 1 + + call remove_folder(folder, ierr) +end function \ No newline at end of file diff --git a/test/output/test_xdmfAPI.F90 b/test/output/test_xdmfAPI.F90 new file mode 100644 index 00000000..12debd2f --- /dev/null +++ b/test/output/test_xdmfAPI.F90 @@ -0,0 +1,286 @@ +integer function test_create_h5_file() bind(c) result(err) + + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + integer(HID_T) :: file_id + integer :: error + logical :: exists + integer :: test_err = 0 + + call create_folder(folder, error) + file = join_path(folder, "test_api.h5") + + call H5open_f(error) + + call create_h5_file(trim(adjustl(file)), file_id) + + test_err = test_err + assert_true(file_id > 0, "create_h5_file returned invalid id") + + call H5Fclose_f(file_id, error) + + inquire(file=trim(adjustl(file)), exist=exists) + test_err = test_err + assert_true(exists, "HDF5 file was not created") + + call H5close_f(error) + + err = test_err + call remove_folder(folder, error) +end function + +integer function test_write_1d_dataset() bind(c) result(err) + + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + integer(HID_T) :: file_id + real(8), allocatable :: data(:) + integer :: error + integer :: test_err = 0 + integer :: n, i + + call create_folder(folder, error) + file = join_path(folder, "test_api.h5") + + call H5open_f(error) + + n = 10 + allocate(data(n)) + data = [(real(i,8), i=1,n)] + + call create_h5_file(trim(adjustl(file)), file_id) + + call h5_write_dataset(file_id, "/data1d", data) + + test_err = test_err + assert_integer_equal(size(data), 10, "1D dataset size mismatch") + + call H5Fclose_f(file_id, error) + + deallocate(data) + + call H5close_f(error) + + err = test_err + call remove_folder(folder, error) +end function + +integer function test_write_2d_dataset() bind(c) result(err) + + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + integer(HID_T) :: file_id + real(8), allocatable :: data(:,:) + integer :: error + integer :: test_err = 0 + + call create_folder(folder, error) + file = join_path(folder, "test_api.h5") + + call H5open_f(error) + + allocate(data(4,5)) + data = 1.0d0 + + call create_h5_file(trim(adjustl(file)), file_id) + + call h5_write_dataset(file_id, "/data2d", data) + + test_err = test_err + assert_integer_equal(size(data,1),4,"2D dim1 mismatch") + test_err = test_err + assert_integer_equal(size(data,2),5,"2D dim2 mismatch") + + call H5Fclose_f(file_id, error) + + deallocate(data) + + call H5close_f(error) + + err = test_err + call remove_folder(folder, error) +end function + +integer function test_write_3d_dataset() bind(c) result(err) + + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + integer(HID_T) :: file_id + real(8), allocatable :: data(:,:,:) + integer :: error + integer :: test_err = 0 + + call create_folder(folder, error) + file = join_path(folder, "test_api.h5") + + call H5open_f(error) + + allocate(data(3,3,3)) + data = 2.0d0 + + call create_h5_file(trim(adjustl(file)), file_id) + + call h5_write_dataset(file_id, "/data3d", data) + + test_err = test_err + assert_integer_equal(size(data,1),3,"3D dim1 mismatch") + test_err = test_err + assert_integer_equal(size(data,2),3,"3D dim2 mismatch") + test_err = test_err + assert_integer_equal(size(data,3),3,"3D dim3 mismatch") + + call H5Fclose_f(file_id, error) + + deallocate(data) + + call H5close_f(error) + + err = test_err + + call remove_folder(folder, error) +end function + +integer function test_xdmf_file_creation() bind(c) result(err) + + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=14), parameter :: folder='testing_folder' + character(len=1024) :: file + integer :: test_err = 0 + integer :: error, unit + logical :: exists + integer :: dims(4) + + call create_folder(folder, error) + file = join_path(folder, "test_api.xdmf") + + dims = [4,4,4,1] + + + open(newunit=unit, file=trim(file), position='append') + call xdmf_write_header_file(unit, 'movieProbe') + + call xdmf_create_grid_step_info(unit,"step0",0.0,"data.h5",dims(1)*dims(2)*dims(3)) + + call xdmf_write_attribute(unit,"Efield") + + call xdmf_write_scalar_attribute(unit, dims, "data.h5","/Efield") + + call xdmf_close_data_item(unit) + call xdmf_close_attribute(unit) + call xdmf_close_grid(unit) + + call xdmf_write_footer_file(unit) + close(unit) + + + inquire(file=trim(file), exist=exists) + + test_err = test_err + assert_true(exists, "XDMF file not created") + + err = test_err + call remove_folder(folder, error) + +end function + +integer function test_xdmf_file_with_h5data() bind(c) result(err) + ! use HDF5 + use xdmfAPI_m + use assertionTools_m + use directoryUtils_m + implicit none + + character(len=20), parameter :: folder = "testing_folder" + character(len=1024) :: xdmf_file, h5_file + integer :: test_err = 0 + integer :: error, t, unit + logical :: exists + integer :: dims(4) + integer(HID_T) :: file_id + real(dp), allocatable :: Efield(:,:), coords(:,:) + real(dp) :: time + character(len=20) :: ts + integer :: i,j, npoints + + call create_folder(folder, error) + + xdmf_file = join_path(folder, "test_api.xdmf") + h5_file = join_path(folder, "data.h5") + + call H5open_f(error) + + ! Create HDF5 file + call create_h5_file(trim(h5_file), file_id) + + dims = [3,3,1,1] ! 2D grid stored as 3D with depth 1 + npoints = dims(1)*dims(2) + + ! Allocate and write coords data: shape (npoints,3) + allocate(coords(npoints,3)) + do j = 1, dims(2) + do i = 1, dims(1) + coords((j-1)*dims(1)+i,1) = real(i-1, dp) ! X + coords((j-1)*dims(1)+i,2) = real(j-1, dp) ! Y + coords((j-1)*dims(1)+i,3) = 0.0_dp ! Z + end do + end do + call h5_write_dataset(file_id,"coords",coords) + deallocate(coords) + + ! Create XDMF file + open(newunit=unit,file=trim(xdmf_file),position='append') + call xdmf_write_header_file(unit, 'movieProbe') + + do t = 1, 5 + time = real(t-1,dp)*0.1_dp + + allocate(Efield(dims(1),dims(2))) + do j=1,dims(2) + do i=1,dims(1) + Efield(i,j) = i + j + t - 1 + end do + end do + + write(ts,'("Efield_",I0)') t + + ! Write timestep data + call h5_write_dataset(file_id,trim(ts),Efield) + + ! XDMF grid + call xdmf_create_grid_step_info(unit,trim(ts),real(time),trim(h5_file),npoints) + call xdmf_write_attribute(unit,"Efield") + call xdmf_write_scalar_attribute(unit,dims, trim(h5_file),"/"//trim(ts)) + + call xdmf_close_data_item(unit) + call xdmf_close_attribute(unit) + call xdmf_close_grid(unit) + + deallocate(Efield) + end do + call xdmf_write_footer_file(unit) + close(unit) + call H5Fclose_f(file_id,error) + + inquire(file=trim(xdmf_file),exist=exists) + test_err = test_err + assert_true(exists,"XDMF file not created") + + call remove_folder(folder,error) + call H5close_f(error) + + err = test_err +end function \ No newline at end of file diff --git a/test/output/vtkAPI_tests.cpp b/test/output/vtkAPI_tests.cpp new file mode 100644 index 00000000..01684e2b --- /dev/null +++ b/test/output/vtkAPI_tests.cpp @@ -0,0 +1 @@ +#include "vtkAPI_tests.h" \ No newline at end of file diff --git a/test/output/vtkAPI_tests.h b/test/output/vtkAPI_tests.h new file mode 100644 index 00000000..814c0944 --- /dev/null +++ b/test/output/vtkAPI_tests.h @@ -0,0 +1,27 @@ +#ifdef CompileWithNewOutputModule +#include + +extern "C" int test_vtkapi_points_allocation(); +extern "C" int test_vtkapi_point_scalar(); +extern "C" int test_vtkapi_point_vector(); +extern "C" int test_vtkapi_cell_scalar(); +extern "C" int test_vtkapi_cell_vector(); +extern "C" int test_vtkapi_vts_file_creation(); +extern "C" int test_vtkapi_vtu_file_creation(); +extern "C" int test_vtkapi_vtu_cell_data(); +extern "C" int test_vtkapi_vts_content(); +extern "C" int test_vtkapi_vtu_content(); + +TEST(vtkapi, test_points_allocation) {EXPECT_EQ(0, test_vtkapi_points_allocation());} +TEST(vtkapi, test_point_scalar) {EXPECT_EQ(0, test_vtkapi_point_scalar());} +TEST(vtkapi, test_point_vector) {EXPECT_EQ(0, test_vtkapi_point_vector());} +TEST(vtkapi, test_cell_scalar) {EXPECT_EQ(0, test_vtkapi_cell_scalar());} +TEST(vtkapi, test_cell_vector) {EXPECT_EQ(0, test_vtkapi_cell_vector());} +TEST(vtkapi, test_vts_file_creation) {EXPECT_EQ(0, test_vtkapi_vts_file_creation());} +TEST(vtkapi, test_vtu_file_creation) {EXPECT_EQ(0, test_vtkapi_vtu_file_creation());} +TEST(vtkapi, test_vtu_cell_data) {EXPECT_EQ(0, test_vtkapi_vtu_cell_data());} +TEST(vtkapi, test_vts_content) {EXPECT_EQ(0, test_vtkapi_vts_content());} +TEST(vtkapi, test_vtu_content) {EXPECT_EQ(0, test_vtkapi_vtu_content());} + + +#endif diff --git a/test/output/xdmfAPI_tests.cpp b/test/output/xdmfAPI_tests.cpp new file mode 100644 index 00000000..e0167c57 --- /dev/null +++ b/test/output/xdmfAPI_tests.cpp @@ -0,0 +1 @@ +#include "xdmfAPI_tests.h" \ No newline at end of file diff --git a/test/output/xdmfAPI_tests.h b/test/output/xdmfAPI_tests.h new file mode 100644 index 00000000..6747793f --- /dev/null +++ b/test/output/xdmfAPI_tests.h @@ -0,0 +1,18 @@ +#ifdef CompileWithNewOutputModule +#include + +extern "C" int test_create_h5_file(); +extern "C" int test_write_1d_dataset(); +extern "C" int test_write_2d_dataset(); +extern "C" int test_write_3d_dataset(); +extern "C" int test_xdmf_file_creation(); +extern "C" int test_xdmf_file_with_h5data(); + +TEST(xdmfapi, test_create_h5) { EXPECT_EQ(0, test_create_h5_file()); } +TEST(xdmfapi, test_write_1d) { EXPECT_EQ(0, test_write_1d_dataset()); } +TEST(xdmfapi, test_write_2d) { EXPECT_EQ(0, test_write_2d_dataset()); } +TEST(xdmfapi, test_write_3d) { EXPECT_EQ(0, test_write_3d_dataset()); } +TEST(xdmfapi, test_xdmf_file) { EXPECT_EQ(0, test_xdmf_file_creation()); } +TEST(xdmfapi, test_xdmf_file_with_h5) { EXPECT_EQ(0, test_xdmf_file_with_h5data()); } + +#endif \ No newline at end of file diff --git a/test/utils/CMakeLists.txt b/test/utils/CMakeLists.txt index ad087a19..96e569ad 100644 --- a/test/utils/CMakeLists.txt +++ b/test/utils/CMakeLists.txt @@ -3,8 +3,12 @@ message(STATUS "Creating build system for test/observation") add_library( test_utils_fortran "fdetypes_tools.F90" + "assertion_tools.F90" + "array_assertion_tools.F90" + "sgg_setters.F90" ) target_link_libraries(test_utils_fortran semba-types + fdtd-utils ) diff --git a/test/utils/array_assertion_tools.F90 b/test/utils/array_assertion_tools.F90 new file mode 100644 index 00000000..770d458d --- /dev/null +++ b/test/utils/array_assertion_tools.F90 @@ -0,0 +1,337 @@ +module arrayAssertionTools_m + use FDETYPES_m + implicit none + real(RKIND), parameter :: tol = 1.0e-12_RKIND + private + !----------------------------- + ! Public assertion procedures + !----------------------------- + public :: assert_arrays_equal + public :: assert_array_value + + !--------------------------------------- + ! GENERIC INTERFACES + !--------------------------------------- + interface assert_arrays_equal + module procedure & + assert_arrays_equal_int1, assert_arrays_equal_int2, assert_arrays_equal_int3, & + assert_arrays_equal_real1, assert_arrays_equal_real2, assert_arrays_equal_real3, & + assert_arrays_equal_complex1, assert_arrays_equal_complex2, assert_arrays_equal_complex3 + end interface + + interface assert_array_value + module procedure & + assert_array_value_int1, assert_array_value_int2, assert_array_value_int3, & + assert_array_value_real1, assert_array_value_real2, assert_array_value_real3, & + assert_array_value_complex1, assert_array_value_complex2, assert_array_value_complex3 + end interface + +contains + + !--------------------------------------- + ! 1D Integer arrays + !--------------------------------------- + integer function assert_arrays_equal_int1(A, B, errorMessage) + integer, intent(in) :: A(:), B(:) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_int1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(A == B)) then + assert_arrays_equal_int1 = 0 + else + assert_arrays_equal_int1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_int1(A, val, errorMessage) + integer, intent(in) :: A(:) + integer, intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(A == val)) then + assert_array_value_int1 = 0 + else + assert_array_value_int1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! 2D Integer arrays + !--------------------------------------- + integer function assert_arrays_equal_int2(A, B, errorMessage) + integer, intent(in) :: A(:, :), B(:, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_int2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(A == B)) then + assert_arrays_equal_int2 = 0 + else + assert_arrays_equal_int2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_int2(A, val, errorMessage) + integer, intent(in) :: A(:, :) + integer, intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(A == val)) then + assert_array_value_int2 = 0 + else + assert_array_value_int2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! 3D Integer arrays + !--------------------------------------- + integer function assert_arrays_equal_int3(A, B, errorMessage) + integer, intent(in) :: A(:, :, :), B(:, :, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_int3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(A == B)) then + assert_arrays_equal_int3 = 0 + else + assert_arrays_equal_int3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_int3(A, val, errorMessage) + integer, intent(in) :: A(:, :, :) + integer, intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(A == val)) then + assert_array_value_int3 = 0 + else + assert_array_value_int3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! REAL arrays (1D, 2D, 3D) + !--------------------------------------- + integer function assert_arrays_equal_real1(A, B, errorMessage) + real(RKIND), intent(in) :: A(:), B(:) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_real1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_real1 = 0 + else + assert_arrays_equal_real1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_real1(A, val, errorMessage) + real(RKIND), intent(in) :: A(:) + real(RKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_real1 = 0 + else + assert_array_value_real1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! REAL 2D + !--------------------------------------- + integer function assert_arrays_equal_real2(A, B, errorMessage) + real(RKIND), intent(in) :: A(:, :), B(:, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_real2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_real2 = 0 + else + assert_arrays_equal_real2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_real2(A, val, errorMessage) + real(RKIND), intent(in) :: A(:, :) + real(RKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_real2 = 0 + else + assert_array_value_real2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! REAL 3D + !--------------------------------------- + integer function assert_arrays_equal_real3(A, B, errorMessage) + real(RKIND), intent(in) :: A(:, :, :), B(:, :, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_real3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_real3 = 0 + else + assert_arrays_equal_real3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_real3(A, val, errorMessage) + real(RKIND), intent(in) :: A(:, :, :) + real(RKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_real3 = 0 + else + assert_array_value_real3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! COMPLEX 1D arrays + !--------------------------------------- + integer function assert_arrays_equal_complex1(A, B, errorMessage) + complex(CKIND), intent(in) :: A(:), B(:) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_complex1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_complex1 = 0 + else + assert_arrays_equal_complex1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_complex1(A, val, errorMessage) + complex(CKIND), intent(in) :: A(:) + complex(CKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_complex1 = 0 + else + assert_array_value_complex1 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + +!--------------------------------------- +! COMPLEX 2D arrays +!--------------------------------------- + integer function assert_arrays_equal_complex2(A, B, errorMessage) + complex(CKIND), intent(in) :: A(:, :), B(:, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_complex2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_complex2 = 0 + else + assert_arrays_equal_complex2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_complex2(A, val, errorMessage) + complex(CKIND), intent(in) :: A(:, :) + complex(CKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_complex2 = 0 + else + assert_array_value_complex2 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + +!--------------------------------------- +! COMPLEX 3D arrays +!--------------------------------------- + integer function assert_arrays_equal_complex3(A, B, errorMessage) + complex(CKIND), intent(in) :: A(:, :, :), B(:, :, :) + character(*), intent(in), optional :: errorMessage + + if (any(shape(A) /= shape(B))) then + assert_arrays_equal_complex3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + return + end if + + if (all(abs(A - B) < tol)) then + assert_arrays_equal_complex3 = 0 + else + assert_arrays_equal_complex3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + integer function assert_array_value_complex3(A, val, errorMessage) + complex(CKIND), intent(in) :: A(:, :, :) + complex(CKIND), intent(in) :: val + character(*), intent(in), optional :: errorMessage + + if (all(abs(A - val) < tol)) then + assert_array_value_complex3 = 0 + else + assert_array_value_complex3 = 1 + if (present(errorMessage)) print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + +end module arrayAssertionTools_m diff --git a/test/utils/assertion_tools.F90 b/test/utils/assertion_tools.F90 new file mode 100644 index 00000000..3a865eaf --- /dev/null +++ b/test/utils/assertion_tools.F90 @@ -0,0 +1,180 @@ +module assertionTools_m + use FDETYPES_m + use arrayAssertionTools_m + use iso_fortran_env, only: real32, real64 + implicit none + + private :: assert_real_equal_impl +#ifndef CompileWithReal8 + private :: assert_real_time_equal_impl +#endif + ! Generic interface for real assertions + interface assert_real_equal + module procedure assert_real_equal_impl +#ifndef CompileWithReal8 + module procedure assert_real_time_equal_impl +#endif + end interface + +contains + + !--------------------------------------- + ! Logical assertion + !--------------------------------------- + function assert_true(boolean, errorMessage) result(err) + logical, intent(in) :: boolean + character(*), intent(in) :: errorMessage + integer :: err + if (boolean) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED: ', trim(errorMessage) + end if + end function + + !--------------------------------------- + ! Integer equality + !--------------------------------------- + function assert_integer_equal(val, expected, errorMessage) result(err) + integer, intent(in) :: val, expected + character(*), intent(in) :: errorMessage + integer :: err + if (val == expected) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED: ', trim(errorMessage) + print *, " Value: ", val, ". Expected: ", expected + end if + end function + + !--------------------------------------- + ! Real equality implementations + !--------------------------------------- + function assert_real_equal_impl(val, expected, tolerance, errorMessage) result(err) + real(kind=RKIND), intent(in) :: val, expected, tolerance + character(*), intent(in) :: errorMessage + integer :: err + if (abs(val - expected) <= tolerance) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED (RKIND): ', trim(errorMessage) + print *, ' Value: ', val, '. Expected: ', expected, '. Tolerance: ', tolerance + end if + end function + + function assert_real_time_equal_impl(val, expected, tolerance, errorMessage) result(err) + real(kind=RKIND_tiempo), intent(in) :: val, expected, tolerance + character(*), intent(in) :: errorMessage + integer :: err + if (abs(val - expected) <= tolerance) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED (time): ', trim(errorMessage) + print *, ' Value: ', val, '. Expected: ', expected, '. Tolerance: ', tolerance + end if + end function + + !--------------------------------------- + ! Complex equality + !--------------------------------------- + function assert_complex_equal(val, expected, tolerance, errorMessage) result(err) + complex(kind=CKIND), intent(in) :: val, expected + real(kind=RKIND), intent(in) :: tolerance + character(len=*), intent(in) :: errorMessage + integer :: err + if (abs(val - expected) <= tolerance) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED: ', trim(errorMessage) + print *, ' Value: ', val + print *, ' Expected: ', expected + print *, ' Delta: ', abs(val - expected) + print *, ' Tolerance:', tolerance + end if + end function + + !--------------------------------------- + ! String equality + !--------------------------------------- + function assert_string_equal(val, expected, errorMessage) result(err) + character(*), intent(in) :: val, expected + character(*), intent(in) :: errorMessage + integer :: err + if (trim(val) == trim(expected)) then + err = 0 + else + err = 1 + print *, 'ASSERTION FAILED: ', trim(errorMessage) + print *, ' Value: "', trim(val), '". Expected: "', trim(expected), '"' + end if + end function + + !--------------------------------------- + ! Check if file was written + !--------------------------------------- + integer function assert_written_output_file(filename) result(code) + character(len=*), intent(in) :: filename + logical :: ex + integer :: filesize + code = 0 + inquire (file=filename, exist=ex, size=filesize) + if (.not. ex) then + print *, "ERROR: Output file not created:", trim(filename) + code = 1 + else if (filesize <= 0) then + print *, "ERROR: Output file is empty:", trim(filename) + code = 2 + end if + end function + + !--------------------------------------- + ! Check file content + !--------------------------------------- + integer function assert_file_content(unit, expectedValues, nRows, nCols, tolerance, headers) result(flag) + integer(kind=SINGLE), intent(in) :: unit + real(kind=RKIND), intent(in) :: expectedValues(:, :) + integer(kind=SINGLE), intent(in) :: nRows, nCols + real(kind=RKIND), intent(in) :: tolerance + character(len=*), intent(in), optional :: headers(:) + integer(kind=SINGLE) :: i, j, ios + real(kind=RKIND), dimension(nCols) :: val + character(len=BUFSIZE) :: line + flag = 0 + + if (present(headers)) then + read (unit, '(F12.6,1X,F12.6)', iostat=ios) line + if (ios /= 0) return + end if + + do i = 1, nRows + read (unit, *, iostat=ios) val + if (ios /= 0) then + flag = flag + 1 + return + end if + do j = 1, nCols + if (abs(val(j) - expectedValues(i, j)) > tolerance) then + flag = flag + 1 + end if + end do + end do + end function + + !--------------------------------------- + ! Check file exists + !--------------------------------------- + integer function assert_file_exists(fileName) result(err) + character(len=*), intent(in) :: filename + integer :: unit, ios + err = 0 + open (newunit=unit, file=filename, status='old', iostat=ios) + close (unit) + if (ios /= 0) err = 1 + end function + +end module assertionTools_m diff --git a/test/utils/fdetypes_tools.F90 b/test/utils/fdetypes_tools.F90 index aec321a9..489350fb 100644 --- a/test/utils/fdetypes_tools.F90 +++ b/test/utils/fdetypes_tools.F90 @@ -1,63 +1,808 @@ module FDETYPES_TOOLS - use FDETYPES_m - contains - function create_limit_t(XI,XE,YI,YE,ZI,ZE,NX,NY,NZ) result(r) - type(limit_t) :: r - integer(kind=4), intent(in) :: XI,XE,YI,YE,ZI,ZE,NX,NY,NZ - r%XI = XI - r%XE = XE - r%YI = YI - r%YE = YE - r%ZI = ZI - r%ZE = ZE - r%NX = NX - r%NY = NY - r%NZ = NZ - end function create_limit_t - function create_tag_list(sggAlloc) result(r) - type(XYZlimit_t), dimension(6), intent(in) :: sggAlloc - type(taglist_t) :: r - - - allocate (r%edge%x(sggAlloc(iEx)%XI:sggAlloc(iEx)%XE, sggAlloc(iEx)%YI:sggAlloc(iEx)%YE, sggAlloc(iEx)%ZI:sggAlloc(iEx)%ZE)) - allocate (r%edge%y(sggAlloc(iEy)%XI:sggAlloc(iEy)%XE, sggAlloc(iEy)%YI:sggAlloc(iEy)%YE, sggAlloc(iEy)%ZI:sggAlloc(iEy)%ZE)) - allocate (r%edge%z(sggAlloc(iEz)%XI:sggAlloc(iEz)%XE, sggAlloc(iEz)%YI:sggAlloc(iEz)%YE, sggAlloc(iEz)%ZI:sggAlloc(iEz)%ZE)) - allocate (r%face%x(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHx)%YI:sggAlloc(iHx)%YE, sggAlloc(iHx)%ZI:sggAlloc(iHx)%ZE)) - allocate (r%face%y(sggAlloc(iHy)%XI:sggAlloc(iHy)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHy)%ZI:sggAlloc(iHy)%ZE)) - allocate (r%face%z(sggAlloc(iHz)%XI:sggAlloc(iHz)%XE, sggAlloc(iHz)%YI:sggAlloc(iHz)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) - - - r%edge%x(:,:,:) = 0 - r%edge%y(:,:,:) = 0 - r%edge%z(:,:,:) = 0 - r%face%x(:,:,:) = 0 - r%face%y(:,:,:) = 0 - r%face%z(:,:,:) = 0 - end function create_tag_list - - function create_media(sggAlloc) result(r) - type(XYZlimit_t), dimension(6), intent(in) :: sggAlloc - type(media_matrices_t) :: r - - allocate (r%sggMtag(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) - allocate (r%sggMiNo(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) - - allocate (r%sggMiEx(sggAlloc(iEx)%XI:sggAlloc(iEx)%XE, sggAlloc(iEx)%YI:sggAlloc(iEx)%YE, sggAlloc(iEx)%ZI:sggAlloc(iEx)%ZE)) - allocate (r%sggMiEy(sggAlloc(iEy)%XI:sggAlloc(iEy)%XE, sggAlloc(iEy)%YI:sggAlloc(iEy)%YE, sggAlloc(iEy)%ZI:sggAlloc(iEy)%ZE)) - allocate (r%sggMiEz(sggAlloc(iEz)%XI:sggAlloc(iEz)%XE, sggAlloc(iEz)%YI:sggAlloc(iEz)%YE, sggAlloc(iEz)%ZI:sggAlloc(iEz)%ZE)) - - allocate (r%sggMiHx(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHx)%YI:sggAlloc(iHx)%YE, sggAlloc(iHx)%ZI:sggAlloc(iHx)%ZE)) - allocate (r%sggMiHy(sggAlloc(iHy)%XI:sggAlloc(iHy)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHy)%ZI:sggAlloc(iHy)%ZE)) - allocate (r%sggMiHz(sggAlloc(iHz)%XI:sggAlloc(iHz)%XE, sggAlloc(iHz)%YI:sggAlloc(iHz)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) - - r%sggMtag (:, :, :) = 0 - r%sggMiNo (:, :, :) = 1 - r%sggMiEx (:, :, :) = 1 - r%sggMiEy (:, :, :) = 1 - r%sggMiEz (:, :, :) = 1 - r%sggMiHx (:, :, :) = 1 - r%sggMiHy (:, :, :) = 1 - r%sggMiHz (:, :, :) = 1 - end function create_media - -end module FDETYPES_TOOLS \ No newline at end of file + use FDETYPES_m + use utils_m + use NFDETypes_m + implicit none + private + + !=========================== + ! Public interface summary + !=========================== + public :: observation_domain_t + public :: initialize_observation_domain_logical_flags + public :: initialize_observation_time_domain + public :: initialize_observation_frequency_domain + public :: initialize_observation_phi_domain + public :: initialize_observation_theta_domain + public :: create_observable + public :: set_observation + public :: init_time_array + public :: create_limit_t + public :: create_xyzlimit_t + public :: create_xyz_limit_array + public :: create_tag_list + public :: create_geometry_media + public :: create_geometry_media_from_sggAlloc + public :: create_thinWire_simulation_material + public :: init_simulation_material_list + public :: create_facesNF2FF + public :: create_control_flags + public :: add_simulation_material + public :: assign_material_id_to_media_matrix_coordinate + !=========================== + + !=========================== + ! Private interface summary + !=========================== + + !=========================== + + real(kind=rkind) :: UTILEPS0 = 8.8541878176203898505365630317107502606083701665994498081024171524053950954599821142852891607182008932e-12 + real(kind=rkind) :: UTILMU0 = 1.2566370614359172953850573533118011536788677597500423283899778369231265625144835994512139301368468271e-6 + type :: observation_domain_t + real(kind=RKIND) :: InitialTime = 0.0_RKIND + real(kind=RKIND) :: FinalTime = 0.0_RKIND + real(kind=RKIND) :: TimeStep = 0.0_RKIND + + real(kind=RKIND) :: InitialFreq = 0.0_RKIND + real(kind=RKIND) :: FinalFreq = 0.0_RKIND + real(kind=RKIND) :: FreqStep = 0.0_RKIND + + real(kind=RKIND) :: thetaStart = 0.0_RKIND + real(kind=RKIND) :: thetaStop = 0.0_RKIND + real(kind=RKIND) :: thetaStep = 0.0_RKIND + + real(kind=RKIND) :: phiStart = 0.0_RKIND + real(kind=RKIND) :: phiStop = 0.0_RKIND + real(kind=RKIND) :: phiStep = 0.0_RKIND + + logical :: FreqDomain = .FALSE. + logical :: TimeDomain = .FALSE. + logical :: Saveall = .FALSE. + logical :: TransFer = .FALSE. + logical :: Volumic = .FALSE. + end type observation_domain_t + +contains + + function create_xyzlimit_t(XI, XE, YI, YE, ZI, ZE) result(r) + type(limit_t) :: r + integer(kind=4), intent(in) :: XI, XE, YI, YE, ZI, ZE + r%XI = XI + r%XE = XE + r%YI = YI + r%YE = YE + r%ZI = ZI + r%ZE = ZE + end function create_xyzlimit_t + + function create_limit_t(XI, XE, YI, YE, ZI, ZE, NX, NY, NZ) result(r) + type(limit_t) :: r + integer(kind=4), intent(in) :: XI, XE, YI, YE, ZI, ZE, NX, NY, NZ + r%XI = XI + r%XE = XE + r%YI = YI + r%YE = YE + r%ZI = ZI + r%ZE = ZE + r%NX = NX + r%NY = NY + r%NZ = NZ + end function create_limit_t + + function create_tag_list(sggAlloc) result(r) + type(XYZlimit_t), dimension(6), intent(in) :: sggAlloc + type(taglist_t) :: r + + allocate (r%edge%x(sggAlloc(iEx)%XI:sggAlloc(iEx)%XE, sggAlloc(iEx)%YI:sggAlloc(iEx)%YE, sggAlloc(iEx)%ZI:sggAlloc(iEx)%ZE)) + allocate (r%edge%y(sggAlloc(iEy)%XI:sggAlloc(iEy)%XE, sggAlloc(iEy)%YI:sggAlloc(iEy)%YE, sggAlloc(iEy)%ZI:sggAlloc(iEy)%ZE)) + allocate (r%edge%z(sggAlloc(iEz)%XI:sggAlloc(iEz)%XE, sggAlloc(iEz)%YI:sggAlloc(iEz)%YE, sggAlloc(iEz)%ZI:sggAlloc(iEz)%ZE)) + allocate (r%face%x(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHx)%YI:sggAlloc(iHx)%YE, sggAlloc(iHx)%ZI:sggAlloc(iHx)%ZE)) + allocate (r%face%y(sggAlloc(iHy)%XI:sggAlloc(iHy)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHy)%ZI:sggAlloc(iHy)%ZE)) + allocate (r%face%z(sggAlloc(iHz)%XI:sggAlloc(iHz)%XE, sggAlloc(iHz)%YI:sggAlloc(iHz)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) + + r%edge%x(:, :, :) = 0 + r%edge%y(:, :, :) = 0 + r%edge%z(:, :, :) = 0 + r%face%x(:, :, :) = 0 + r%face%y(:, :, :) = 0 + r%face%z(:, :, :) = 0 + end function create_tag_list + + subroutine create_geometry_media(res, xi, xe, yi, ye, zi, ze) + integer(kind=SINGLE), intent(in) :: xi, xe, yi, ye, zi, ze + type(media_matrices_t), intent(inout) :: res + + ! Allocate each array with its own kind + call alloc_and_init(res%sggMtag, xi, xe, yi, ye, zi, ze, 1_IKINDMTAG) + call alloc_and_init(res%sggMiNo, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiEx, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiEy, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiEz, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiHx, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiHy, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + call alloc_and_init(res%sggMiHz, xi, xe, yi, ye, zi, ze, 1_INTEGERSIZEOFMEDIAMATRICES) + end subroutine create_geometry_media + + function create_geometry_media_from_sggAlloc(sggAlloc) result(r) + type(XYZlimit_t), dimension(6), intent(in) :: sggAlloc + type(media_matrices_t) :: r + + allocate (r%sggMtag(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) + allocate (r%sggMiNo(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) + + allocate (r%sggMiEx(sggAlloc(iEx)%XI:sggAlloc(iEx)%XE, sggAlloc(iEx)%YI:sggAlloc(iEx)%YE, sggAlloc(iEx)%ZI:sggAlloc(iEx)%ZE)) + allocate (r%sggMiEy(sggAlloc(iEy)%XI:sggAlloc(iEy)%XE, sggAlloc(iEy)%YI:sggAlloc(iEy)%YE, sggAlloc(iEy)%ZI:sggAlloc(iEy)%ZE)) + allocate (r%sggMiEz(sggAlloc(iEz)%XI:sggAlloc(iEz)%XE, sggAlloc(iEz)%YI:sggAlloc(iEz)%YE, sggAlloc(iEz)%ZI:sggAlloc(iEz)%ZE)) + + allocate (r%sggMiHx(sggAlloc(iHx)%XI:sggAlloc(iHx)%XE, sggAlloc(iHx)%YI:sggAlloc(iHx)%YE, sggAlloc(iHx)%ZI:sggAlloc(iHx)%ZE)) + allocate (r%sggMiHy(sggAlloc(iHy)%XI:sggAlloc(iHy)%XE, sggAlloc(iHy)%YI:sggAlloc(iHy)%YE, sggAlloc(iHy)%ZI:sggAlloc(iHy)%ZE)) + allocate (r%sggMiHz(sggAlloc(iHz)%XI:sggAlloc(iHz)%XE, sggAlloc(iHz)%YI:sggAlloc(iHz)%YE, sggAlloc(iHz)%ZI:sggAlloc(iHz)%ZE)) + + r%sggMtag(:, :, :) = 1 + r%sggMiNo(:, :, :) = 1 + r%sggMiEx(:, :, :) = 1 + r%sggMiEy(:, :, :) = 1 + r%sggMiEz(:, :, :) = 1 + r%sggMiHx(:, :, :) = 1 + r%sggMiHy(:, :, :) = 1 + r%sggMiHz(:, :, :) = 1 + end function create_geometry_media_from_sggAlloc + + function create_control_flags(layoutnumber, size, mpidir, finaltimestep, & + nEntradaRoot, wiresflavor, wirecrank, & + resume, saveall, NF2FFDecim, simu_devia, singlefilewrite, & + facesNF2FF) result(control) + + type(sim_control_t) :: control + + integer(kind=SINGLE), intent(in), optional :: layoutnumber, size, mpidir, finaltimestep + character(len=*), intent(in), optional :: nEntradaRoot, wiresflavor + logical, intent(in), optional :: wirecrank, resume, saveall, NF2FFDecim, simu_devia, singlefilewrite + type(nf2ff_t), intent(in), optional :: facesNF2FF + + ! 1. Set explicit defaults for all components + control%layoutnumber = 0 + control%num_procs = 0 + control%mpidir = 3 + control%finaltimestep = 0 + control%nEntradaRoot = "" + control%wiresflavor = "" + control%wirecrank = .false. + control%resume = .false. + control%saveall = .false. + control%NF2FFDecim = .false. + control%simu_devia = .false. + control%singlefilewrite = .false. + ! Note: control%facesNF2FF retains its default initialized state + + ! 2. Overwrite defaults only if the optional argument is present + if (present(layoutnumber)) control%layoutnumber = layoutnumber + if (present(size)) control%num_procs = size + if (present(mpidir)) control%mpidir = mpidir + if (present(finaltimestep)) control%finaltimestep = finaltimestep + if (present(nEntradaRoot)) control%nEntradaRoot = nEntradaRoot + if (present(wiresflavor)) control%wiresflavor = wiresflavor + if (present(wirecrank)) control%wirecrank = wirecrank + if (present(resume)) control%resume = resume + if (present(saveall)) control%saveall = saveall + if (present(NF2FFDecim)) control%NF2FFDecim = NF2FFDecim + if (present(simu_devia)) control%simu_devia = simu_devia + if (present(singlefilewrite)) control%singlefilewrite = singlefilewrite + if (present(facesNF2FF)) control%facesNF2FF = facesNF2FF + + end function create_control_flags + + subroutine init_simulation_material_list(simulationMaterials) + implicit none + type(MediaData_t), dimension(:), allocatable, intent(out) :: simulationMaterials + if (allocated(simulationMaterials)) deallocate (simulationMaterials) + allocate (simulationMaterials(0:2)) + simulationMaterials(0) = create_pec_simulation_material() + simulationMaterials(1) = get_default_mediadata() + simulationMaterials(2) = create_pmc_simulation_material() + + end subroutine init_simulation_material_list + + subroutine init_time_array(arr, array_size, interval) + integer, intent(in), optional :: array_size + real(kind=RKIND_tiempo), intent(in), optional :: interval + integer(kind=4) :: i + integer :: size_val + real(kind=RKIND_tiempo) :: interval_val + real(kind=RKIND_tiempo), pointer, dimension(:), intent(out) :: arr + + size_val = merge(array_size, 100, present(array_size)) + interval_val = merge(interval, 1.0_RKIND_tiempo, present(interval)) + + allocate (arr(size_val)) + + DO i = 1, size_val + arr(i) = (i - 1)*interval_val + END DO + end subroutine init_time_array + + function create_xyz_limit_array(XI, YI, ZI, XE, YE, ZE) result(arr) + type(XYZlimit_t), dimension(1:6) :: arr + integer(kind=4), intent(in), optional :: XI, YI, ZI, XE, YE, ZE + integer :: i + integer(kind=4) :: xi_val, yi_val, zi_val, xe_val, ye_val, ze_val + + ! Use merge for compact handling of optional inputs with defaults + xi_val = merge(XI, 0, present(XI)) + yi_val = merge(YI, 0, present(YI)) + zi_val = merge(ZI, 0, present(ZI)) + xe_val = merge(XE, 6, present(XE)) + ye_val = merge(YE, 6, present(YE)) + ze_val = merge(ZE, 6, present(ZE)) + + do i = 1, 6 + arr(i)%XI = xi_val + arr(i)%XE = xe_val + arr(i)%YI = yi_val + arr(i)%YE = ye_val + arr(i)%ZI = zi_val + arr(i)%ZE = ze_val + end do + end function create_xyz_limit_array + + function create_facesNF2FF(tr, fr, iz, de, ab, ar) result(faces) + type(nf2ff_t) :: faces + logical, intent(in), optional :: tr, fr, iz, de, ab, ar + + faces%tr = .false. + faces%fr = .false. + faces%iz = .false. + faces%de = .false. + faces%ab = .false. + faces%ar = .false. + + if (present(tr)) faces%tr = tr + if (present(fr)) faces%fr = fr + if (present(iz)) faces%iz = iz + if (present(de)) faces%de = de + if (present(ab)) faces%ab = ab + if (present(ar)) faces%ar = ar + end function create_facesNF2FF + + function define_point_observation() result(obs) + type(Obses_t) :: obs + + obs%nP = 1 + allocate (obs%P(obs%nP)) + obs%P(1) = create_observable(1_SINGLE, 1_SINGLE, 1_SINGLE, 1_SINGLE, 1_SINGLE, 1_SINGLE, iEx) + + obs%InitialTime = 0.0_RKIND_tiempo + obs%FinalTime = 1.0_RKIND_tiempo + obs%TimeStep = 0.1_RKIND_tiempo + + obs%InitialFreq = 0.0_RKIND + obs%FinalFreq = 0.0_RKIND + obs%FreqStep = 0.0_RKIND + + obs%outputrequest = 'pointProbe' + + obs%FreqDomain = .false. + obs%TimeDomain = .true. + obs%Saveall = .false. + obs%TransFer = .false. + obs%Volumic = .false. + obs%Done = .false. + obs%Begun = .false. + obs%Flushed = .false. + + end function define_point_observation + + function define_wire_current_observation() result(obs) + type(Obses_t) :: obs + + obs%nP = 1 + allocate (obs%P(obs%nP)) + obs%P(1) = create_observable(3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, iJx) + + obs%InitialTime = 0.0_RKIND_tiempo + obs%FinalTime = 1.0_RKIND_tiempo + obs%TimeStep = 0.1_RKIND_tiempo + + obs%InitialFreq = 0.0_RKIND + obs%FinalFreq = 0.0_RKIND + obs%FreqStep = 0.0_RKIND + + obs%outputrequest = 'pointProbe' + + obs%FreqDomain = .false. + obs%TimeDomain = .true. + obs%Saveall = .false. + obs%TransFer = .false. + obs%Volumic = .false. + obs%Done = .false. + obs%Begun = .false. + obs%Flushed = .false. + end function define_wire_current_observation + + function define_wire_charge_observation() result(obs) + type(Obses_t) :: obs + + obs%nP = 1 + allocate (obs%P(obs%nP)) + obs%P(1) = create_observable(3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, 3_SINGLE, iQx) + + obs%InitialTime = 0.0_RKIND_tiempo + obs%FinalTime = 1.0_RKIND_tiempo + obs%TimeStep = 0.1_RKIND_tiempo + + obs%InitialFreq = 0.0_RKIND + obs%FinalFreq = 0.0_RKIND + obs%FreqStep = 0.0_RKIND + + obs%outputrequest = 'pointProbe' + + obs%FreqDomain = .false. + obs%TimeDomain = .true. + obs%Saveall = .false. + obs%TransFer = .false. + obs%Volumic = .false. + obs%Done = .false. + obs%Begun = .false. + obs%Flushed = .false. + end function define_wire_charge_observation + + function create_observable(XI, YI, ZI, XE, YE, ZE, what, line_in) result(observable) + type(observable_t) :: observable + integer(kind=4), intent(in) :: XI, YI, ZI, XE, YE, ZE, what + type(direction_t), dimension(:), optional, intent(in) :: line_in + + integer(kind=SINGLE) :: line_size + + observable%XI = XI + observable%YI = YI + observable%ZI = ZI + + observable%XE = XE + observable%YE = YE + observable%ZE = ZE + + observable%Xtrancos = 1 + observable%Ytrancos = 1 + observable%Ztrancos = 1 + + observable%What = what + + if (present(line_in)) then + line_size = size(line_in) + + if (line_size > 0) then + allocate (observable%line(1:line_size)) + + observable%line = line_in + else + + end if + end if + end function create_observable + + subroutine add_simulation_material(simulationMaterials, newSimulationMaterial) + type(MediaData_t), dimension(:), intent(inout), allocatable :: simulationMaterials + type(MediaData_t), intent(in) :: newSimulationMaterial + + type(MediaData_t), dimension(:), target, allocatable :: tempSimulationMaterials + integer(kind=SINGLE) :: oldSize, istat + oldSize = size(simulationMaterials) + allocate (tempSimulationMaterials(0:oldSize), stat=istat) + if (istat /= 0) then + stop "Allocation failed for temporary media array." + end if + + if (oldSize > 0) then + tempSimulationMaterials(0:oldSize - 1) = simulationMaterials + deallocate (simulationMaterials) + end if + tempSimulationMaterials(oldSize) = newSimulationMaterial + + simulationMaterials = tempSimulationMaterials + end subroutine add_simulation_material + + subroutine add_media_data_to_sgg(sgg, mediaData) + implicit none + + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(MediaData_t), intent(in) :: mediaData + + type(MediaData_t), dimension(:), target, allocatable :: temp_Med + integer :: new_size, istat + + new_size = sgg%NumMedia + 1 + + allocate (temp_Med(new_size), stat=istat) + if (istat /= 0) then + stop "Allocation failed for temporary media array." + end if + + if (sgg%NumMedia > 0) then + temp_Med(1:sgg%NumMedia) = sgg%Med + + deallocate (sgg%Med) + end if + + temp_Med(new_size) = mediaData + + sgg%Med => temp_Med + + sgg%NumMedia = new_size + + end subroutine add_media_data_to_sgg + + subroutine assign_material_id_to_media_matrix_coordinate(media, fieldComponent, i, j, k, materialId) + type(media_matrices_t), intent(inout) :: media + integer(kind=SINGLE), intent(in) :: fieldComponent, i, j, k, materialId + selectcase (fieldComponent) + case (iEx); media%sggMiEx(i, j, k) = materialId + case (iEy); media%sggMiEy(i, j, k) = materialId + case (iEz); media%sggMiEz(i, j, k) = materialId + case (iHx); media%sggMiHx(i, j, k) = materialId + case (iHy); media%sggMiHy(i, j, k) = materialId + case (iHz); media%sggMiHz(i, j, k) = materialId + end select + + end subroutine assign_material_id_to_media_matrix_coordinate + + function get_default_mediadata() result(res) + implicit none + + type(MediaData_t) :: res + !Vacuum id + res%Id = 0 + + ! Reals + res%Priority = 10 + res%Epr = 1.0_RKIND + res%Sigma = 0.0_RKIND + res%Mur = 1.0_RKIND + res%SigmaM = 0.0_RKIND + + ! Logical + res%sigmareasignado = .false. + + ! exists_t logicals + res%Is%PML = .false. + res%Is%PEC = .false. + res%Is%PMC = .false. + res%Is%ThinWire = .false. + res%Is%SlantedWire = .false. + res%Is%EDispersive = .false. + res%Is%MDispersive = .false. + res%Is%EDispersiveAnis = .false. + res%Is%MDispersiveAnis = .false. + res%Is%ThinSlot = .false. + res%Is%PMLbody = .false. + res%Is%SGBC = .false. + res%Is%SGBCDispersive = .false. + res%Is%Lumped = .false. + res%Is%Lossy = .false. + res%Is%AnisMultiport = .false. + res%Is%Multiport = .false. + res%Is%MultiportPadding = .false. + res%Is%Dielectric = .false. + res%Is%Anisotropic = .false. + res%Is%Volume = .false. + res%Is%Line = .false. + res%Is%Surface = .false. + res%Is%Needed = .true. + res%Is%Interfase = .false. + res%Is%already_YEEadvanced_byconformal = .false. + res%Is%split_and_useless = .false. + + ! Pointers: They are automatically unassociated (nullified) + ! when a function returns a type with pointer components, + ! unless explicitly associated before return. + ! For safety, we can explicitly nullify them, although Fortran often handles this. + nullify (res%Wire) + nullify (res%SlantedWire) + nullify (res%PMLbody) + nullify (res%Multiport) + nullify (res%AnisMultiport) + nullify (res%EDispersive) + nullify (res%MDispersive) + nullify (res%Anisotropic) + nullify (res%Lumped) + + end function get_default_mediadata + + function create_pec_simulation_material() result(res) + implicit none + + type(MediaData_t) :: res + type(Material_t) :: mat + + mat = create_pec_material() + res = get_default_mediadata() + res%Id = mat%id + res%Is%PEC = .TRUE. + + res%Priority = 150 + res%Epr = mat%eps/UTILEPS0 + res%Sigma = mat%sigma + res%Mur = mat%mu/UTILMU0 + res%SigmaM = mat%sigmam + + end function create_pec_simulation_material + + function create_pmc_simulation_material() result(res) + implicit none + + type(MediaData_t) :: res + type(Material_t) :: mat + + mat = create_pmc_material() + res = get_default_mediadata() + + res%Id = mat%id + res%Is%PMC = .TRUE. + + res%Priority = 160 + res%Epr = mat%eps/UTILEPS0 + res%Sigma = mat%sigma + res%Mur = mat%mu/UTILMU0 + res%SigmaM = mat%sigmam + + end function create_pmc_simulation_material + + function create_thinWire_simulation_material(materialId) result(res) + implicit none + integer(kind=SINGLE) :: materialId + + type(MediaData_t) :: res + type(Material_t) :: mat + + type(Wires_t), target, dimension(1) :: wire + + res = get_default_mediadata() + res%Id = materialId + res%Is%ThinWire = .TRUE. + + allocate (res%Wire(1)) + wire(1) = get_default_wire() + res%wire => wire + + res%Priority = 15 + + end function create_thinWire_simulation_material + + function create_empty_material() result(mat) + implicit none + type(Material_t) :: mat + end function create_empty_material + + function create_material(eps_in, mu_in, sigma_in, sigmam_in, id_in) result(mat) + implicit none + real(kind=RK), intent(in) :: eps_in, mu_in, sigma_in, sigmam_in + integer(kind=4), intent(in) :: id_in + type(Material_t) :: mat + + mat%eps = eps_in + mat%mu = mu_in + mat%sigma = sigma_in + mat%sigmam = sigmam_in + mat%id = id_in + end function create_material + + function create_vacuum_material() result(mat) + type(Material_t) :: mat + mat = create_material(EPSILON_VACUUM, MU_VACUUM, 0.0_RKIND, 0.0_RKIND, 1) + end function create_vacuum_material + + function create_pec_material() result(mat) + type(Material_t) :: mat + mat = create_material(EPSILON_VACUUM, MU_VACUUM, SIGMA_PEC, 0.0_RKIND, 0) + end function create_pec_material + + function create_pmc_material() result(mat) + type(Material_t) :: mat + mat = create_material(EPSILON_VACUUM, MU_VACUUM, 0.0_RKIND, SIGMA_PMC, 2) + end function create_pmc_material + + function create_empty_materials() result(mats) + implicit none + type(Materials_t) :: mats + end function create_empty_materials + + subroutine add_material_to_materials(mats_collection, new_mat) + implicit none + type(Materials_t), intent(inout) :: mats_collection + type(Material_t), intent(in) :: new_mat + + type(Material_t), dimension(:), target, allocatable :: temp_Mats + integer :: old_size, new_size + + old_size = mats_collection%n_Mats + new_size = old_size + 1 + + allocate (temp_Mats(new_size)) + + if (old_size > 0) then + temp_Mats(1:old_size) = mats_collection%Mats + + deallocate (mats_collection%Mats) + end if + + temp_Mats(new_size) = new_mat + + mats_collection%Mats => temp_Mats + + mats_collection%n_Mats = new_size + mats_collection%n_Mats_max = new_size + + end subroutine add_material_to_materials + + function get_default_wire() result(wire) + implicit none + type(Wires_t) :: wire + + wire%Radius = 0.0_RKIND_wires + wire%R = 0.0_RKIND_wires + wire%L = 0.0_RKIND_wires + wire%C = 0.0_RKIND_wires + wire%P_R = 0.0_RKIND_wires + wire%P_L = 0.0_RKIND_wires + wire%P_C = 0.0_RKIND_wires + wire%Radius_devia = 0.0_RKIND_wires + wire%R_devia = 0.0_RKIND_wires + wire%L_devia = 0.0_RKIND_wires + wire%C_devia = 0.0_RKIND_wires + + wire%numsegmentos = 0 + wire%NUMVOLTAGESOURCES = 0 + wire%NUMCURRENTSOURCES = 0 + + nullify (wire%segm) + nullify (wire%Vsource) + nullify (wire%Isource) + + wire%VsourceExists = .false. + wire%IsourceExists = .false. + wire%HasParallel_LeftEnd = .false. + wire%HasParallel_RightEnd = .false. + wire%HasSeries_LeftEnd = .false. + wire%HasSeries_RightEnd = .false. + wire%HasAbsorbing_LeftEnd = .false. + wire%HasAbsorbing_RightEnd = .false. + + wire%Parallel_R_RightEnd = 0.0_RKIND_wires + wire%Parallel_R_LeftEnd = 0.0_RKIND_wires + wire%Series_R_RightEnd = 0.0_RKIND_wires + wire%Series_R_LeftEnd = 0.0_RKIND_wires + wire%Parallel_L_RightEnd = 0.0_RKIND_wires + wire%Parallel_L_LeftEnd = 0.0_RKIND_wires + wire%Series_L_RightEnd = 0.0_RKIND_wires + wire%Series_L_LeftEnd = 0.0_RKIND_wires + wire%Parallel_C_RightEnd = 0.0_RKIND_wires + wire%Parallel_C_LeftEnd = 0.0_RKIND_wires + wire%Series_C_RightEnd = 2.0e7_RKIND ! Valor por defecto de corto + wire%Series_C_LeftEnd = 2.0e7_RKIND ! Valor por defecto de corto + + wire%Parallel_R_RightEnd_devia = 0.0_RKIND_wires + wire%Parallel_R_LeftEnd_devia = 0.0_RKIND_wires + wire%Series_R_RightEnd_devia = 0.0_RKIND_wires + wire%Series_R_LeftEnd_devia = 0.0_RKIND_wires + wire%Parallel_L_RightEnd_devia = 0.0_RKIND_wires + wire%Parallel_L_LeftEnd_devia = 0.0_RKIND_wires + wire%Series_L_RightEnd_devia = 0.0_RKIND_wires + wire%Series_L_LeftEnd_devia = 0.0_RKIND_wires + wire%Parallel_C_RightEnd_devia = 0.0_RKIND_wires + wire%Parallel_C_LeftEnd_devia = 0.0_RKIND_wires + wire%Series_C_RightEnd_devia = 0.0_RKIND_wires + wire%Series_C_LeftEnd_devia = 0.0_RKIND_wires + + wire%LeftEnd = 0 + wire%RightEnd = 0 + end function get_default_wire + + subroutine set_observation(obs, P_in, outputrequest_in, domain_params, FileNormalize_in) + implicit none + + type(observable_t), dimension(:), intent(in) :: P_in + character(LEN=*), intent(in) :: outputrequest_in, FileNormalize_in + type(observation_domain_t), intent(in) :: domain_params + + type(Obses_t), intent(out) :: obs + integer(kind=4) :: n_count + + n_count = size(P_in) + obs%nP = n_count + + allocate (obs%P(1:n_count)) + + obs%P(1:n_count) = P_in(1:n_count) + + obs%outputrequest = outputrequest_in + obs%FileNormalize = FileNormalize_in + + obs%InitialTime = domain_params%InitialTime + obs%FinalTime = domain_params%FinalTime + obs%TimeStep = domain_params%TimeStep + + obs%InitialFreq = domain_params%InitialFreq + obs%FinalFreq = domain_params%FinalFreq + obs%FreqStep = domain_params%FreqStep + + obs%thetaStart = domain_params%thetaStart + obs%thetaStop = domain_params%thetaStop + obs%thetaStep = domain_params%thetaStep + + obs%phiStart = domain_params%phiStart + obs%phiStop = domain_params%phiStop + obs%phiStep = domain_params%phiStep + + obs%FreqDomain = domain_params%FreqDomain + obs%TimeDomain = domain_params%TimeDomain + obs%Saveall = domain_params%Saveall + obs%TransFer = domain_params%TransFer + obs%Volumic = domain_params%Volumic + + end subroutine set_observation + + subroutine initialize_observation_time_domain(domain, InitialTime, FinalTime, TimeStep) + implicit none + + type(observation_domain_t), intent(inout) :: domain + real(kind=RKIND), intent(in) :: InitialTime, FinalTime, TimeStep + + domain%InitialTime = InitialTime + domain%FinalTime = FinalTime + domain%TimeStep = TimeStep + + domain%TimeDomain = .true. + + end subroutine initialize_observation_time_domain + + subroutine initialize_observation_frequency_domain(domain, InitialFreq, FinalFreq, FreqStep) + implicit none + + type(observation_domain_t), intent(inout) :: domain + real(kind=RKIND), intent(in) :: InitialFreq, FinalFreq, FreqStep + + domain%InitialFreq = InitialFreq + domain%FinalFreq = FinalFreq + domain%FreqStep = FreqStep + + domain%FreqDomain = .true. + + end subroutine initialize_observation_frequency_domain + + subroutine initialize_observation_theta_domain(domain, thetaStart, thetaStop, thetaStep) + implicit none + + type(observation_domain_t), intent(inout) :: domain + real(kind=RKIND), intent(in) :: thetaStart, thetaStop, thetaStep + + domain%thetaStart = thetaStart + domain%thetaStop = thetaStop + domain%thetaStep = thetaStep + + end subroutine initialize_observation_theta_domain + + subroutine initialize_observation_phi_domain(domain, phiStart, phiStop, phiStep) + implicit none + + type(observation_domain_t), intent(inout) :: domain + real(kind=RKIND), intent(in) :: phiStart, phiStop, phiStep + + domain%phiStart = phiStart + domain%phiStop = phiStop + domain%phiStep = phiStep + + end subroutine initialize_observation_phi_domain + + subroutine initialize_observation_domain_logical_flags(domain, Saveall_flag, TransFer_flag, Volumic_flag) + implicit none + + type(observation_domain_t), intent(inout) :: domain + logical, intent(in) :: Saveall_flag, TransFer_flag, Volumic_flag + + domain%Saveall = Saveall_flag + domain%TransFer = TransFer_flag + domain%Volumic = Volumic_flag + + end subroutine initialize_observation_domain_logical_flags + +end module FDETYPES_TOOLS diff --git a/test/utils/sgg_setters.F90 b/test/utils/sgg_setters.F90 new file mode 100644 index 00000000..9234d379 --- /dev/null +++ b/test/utils/sgg_setters.F90 @@ -0,0 +1,416 @@ +module sggMethods_m + use FDETYPES_m + implicit none + private + + + public :: sgg_init + + public :: sgg_set_tiempo + public :: sgg_set_dt + public :: sgg_set_extraswitches + + public :: sgg_set_NumMedia + public :: sgg_set_AllocMed + public :: sgg_set_IniPMLMedia + public :: sgg_set_EndPMLMedia + + public :: sgg_set_NumPlaneWaves + public :: sgg_set_TimeSteps + public :: sgg_set_InitialTimeStep + + public :: sgg_set_NumNodalSources + public :: sgg_set_NumberRequest + + public :: sgg_set_LineX + public :: sgg_set_LineY + public :: sgg_set_LineZ + public :: sgg_set_DX + public :: sgg_set_DY + public :: sgg_set_DZ + + public :: sgg_set_AllocDxI + public :: sgg_set_AllocDyI + public :: sgg_set_AllocDzI + public :: sgg_set_AllocDxE + public :: sgg_set_AllocDyE + public :: sgg_set_AllocDzE + + public :: sgg_set_PlaneWave + public :: sgg_set_Border + public :: sgg_set_PML + public :: sgg_set_Eshared + public :: sgg_set_Hshared + + public :: sgg_set_Alloc + public :: sgg_set_Sweep + public :: sgg_set_SINPMLSweep + + public :: sgg_set_Med + public :: sgg_set_NodalSource + public :: sgg_set_Observation + + public :: sgg_set_thereAreMagneticMedia + public :: sgg_set_thereArePMLMagneticMedia + + public :: sgg_set_nEntradaRoot + public :: sgg_set_Punto + + public :: sgg_add_observation +contains + subroutine sgg_init(obj, & + tiempo, dt, extraswitches, & + NumMedia, AllocMed, & + IniPMLMedia, EndPMLMedia, & + NumPlaneWaves, TimeSteps, InitialTimeStep, & + NumNodalSources, NumberRequest, & + thereAreMagneticMedia, thereArePMLMagneticMedia, & + nEntradaRoot) + + implicit none + + type(SGGFDTDINFO_t), intent(inout) :: obj + + ! ===== Optional arguments ===== + real(kind=RKIND_tiempo), pointer, optional :: tiempo(:) + real(kind=RKIND_tiempo), optional :: dt + character(len=*), optional :: extraswitches + + integer(kind=SINGLE), optional :: NumMedia, AllocMed + integer(kind=SINGLE), optional :: IniPMLMedia, EndPMLMedia + integer(kind=SINGLE), optional :: NumPlaneWaves, TimeSteps, InitialTimeStep + integer(kind=SINGLE), optional :: NumNodalSources, NumberRequest + + logical, optional :: thereAreMagneticMedia + logical, optional :: thereArePMLMagneticMedia + + character(len=*), optional :: nEntradaRoot + + ! ===== Defaults ===== + + nullify (obj%tiempo) + obj%dt = 0.0_RKIND_tiempo + obj%extraswitches = "" + + obj%NumMedia = 0_SINGLE + obj%AllocMed = 0_SINGLE + obj%IniPMLMedia = 0_SINGLE + obj%EndPMLMedia = 0_SINGLE + obj%NumPlaneWaves = 0_SINGLE + obj%TimeSteps = 0_SINGLE + obj%InitialTimeStep = 0_SINGLE + obj%NumNodalSources = 0_SINGLE + obj%NumberRequest = 0_SINGLE + + nullify (obj%LineX, obj%LineY, obj%LineZ) + nullify (obj%DX, obj%DY, obj%DZ) + + obj%AllocDxI = 0_SINGLE + obj%AllocDyI = 0_SINGLE + obj%AllocDzI = 0_SINGLE + obj%AllocDxE = 0_SINGLE + obj%AllocDyE = 0_SINGLE + obj%AllocDzE = 0_SINGLE + + nullify (obj%PlaneWave) + nullify (obj%Med) + nullify (obj%NodalSource) + nullify (obj%Observation) + + obj%thereAreMagneticMedia = .false. + obj%thereArePMLMagneticMedia = .false. + + obj%nEntradaRoot = "" + + ! NOTE: + ! Derived-type components (Border, PML, Shared_t, XYZlimit_t, Punto) + ! are automatically default-initialized if they define their own defaults. + + ! ===== Overrides from arguments ===== + + if (present(tiempo)) obj%tiempo => tiempo + if (present(dt)) obj%dt = dt + if (present(extraswitches)) obj%extraswitches = extraswitches + + if (present(NumMedia)) obj%NumMedia = NumMedia + if (present(AllocMed)) obj%AllocMed = AllocMed + if (present(IniPMLMedia)) obj%IniPMLMedia = IniPMLMedia + if (present(EndPMLMedia)) obj%EndPMLMedia = EndPMLMedia + if (present(NumPlaneWaves)) obj%NumPlaneWaves = NumPlaneWaves + if (present(TimeSteps)) obj%TimeSteps = TimeSteps + if (present(InitialTimeStep)) obj%InitialTimeStep = InitialTimeStep + if (present(NumNodalSources)) obj%NumNodalSources = NumNodalSources + if (present(NumberRequest)) obj%NumberRequest = NumberRequest + + if (present(thereAreMagneticMedia)) & + obj%thereAreMagneticMedia = thereAreMagneticMedia + + if (present(thereArePMLMagneticMedia)) & + obj%thereArePMLMagneticMedia = thereArePMLMagneticMedia + + if (present(nEntradaRoot)) obj%nEntradaRoot = nEntradaRoot + + end subroutine sgg_init + + subroutine sgg_set_tiempo(sgg, tiempo) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND_tiempo), pointer :: tiempo(:) + sgg%tiempo => tiempo + end subroutine + + subroutine sgg_set_dt(sgg, dt) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND_tiempo), intent(in) :: dt + sgg%dt = dt + end subroutine + + subroutine sgg_set_extraswitches(sgg, extraswitches) + type(SGGFDTDINFO_t), intent(inout) :: sgg + character(len=*), intent(in) :: extraswitches + sgg%extraswitches = extraswitches + end subroutine + + subroutine sgg_set_NumMedia(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%NumMedia = newValue + end subroutine + + subroutine sgg_set_AllocMed(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocMed = newValue + end subroutine + + subroutine sgg_set_IniPMLMedia(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%IniPMLMedia = newValue + end subroutine + + subroutine sgg_set_EndPMLMedia(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%EndPMLMedia = newValue + end subroutine + + subroutine sgg_set_NumPlaneWaves(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%NumPlaneWaves = newValue + end subroutine + + subroutine sgg_set_TimeSteps(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%TimeSteps = newValue + end subroutine + + subroutine sgg_set_InitialTimeStep(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%InitialTimeStep = newValue + end subroutine + + subroutine sgg_set_NumNodalSources(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%NumNodalSources = newValue + end subroutine + + subroutine sgg_set_NumberRequest(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%NumberRequest = newValue + end subroutine + + subroutine sgg_set_LineX(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%LineX => newValue + end subroutine + + subroutine sgg_set_LineY(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%LineY => newValue + end subroutine + + subroutine sgg_set_LineZ(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%LineZ => newValue + end subroutine + + subroutine sgg_set_DX(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%DX => newValue + end subroutine + + subroutine sgg_set_DY(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%DY => newValue + end subroutine + + subroutine sgg_set_DZ(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + real(kind=RKIND), pointer :: newValue(:) + sgg%DZ => newValue + end subroutine + + subroutine sgg_set_AllocDxI(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDxI = newValue + end subroutine + + subroutine sgg_set_AllocDyI(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDyI = newValue + end subroutine + + subroutine sgg_set_AllocDzI(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDzI = newValue + end subroutine + + subroutine sgg_set_AllocDxE(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDxE = newValue + end subroutine + + subroutine sgg_set_AllocDyE(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDyE = newValue + end subroutine + + subroutine sgg_set_AllocDzE(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + integer(kind=SINGLE), intent(in) :: newValue + sgg%AllocDzE = newValue + end subroutine + + subroutine sgg_set_PlaneWave(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(planeonde_t), pointer :: newValue(:) + sgg%PlaneWave => newValue + end subroutine + + subroutine sgg_set_Med(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(MediaData_t), pointer :: newValue(:) + sgg%Med => newValue + end subroutine + + subroutine sgg_set_NodalSource(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(NodalSource_t), pointer :: newValue(:) + sgg%NodalSource => newValue + end subroutine + + subroutine sgg_set_Observation(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(obses_t), pointer :: newValue(:) + sgg%Observation => newValue + end subroutine + + subroutine sgg_set_Border(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(Border_t), intent(in) :: newValue + sgg%Border = newValue + end subroutine + + subroutine sgg_set_PML(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(PML_t), intent(in) :: newValue + sgg%PML = newValue + end subroutine + + subroutine sgg_set_Eshared(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(Shared_t), intent(in) :: newValue + sgg%Eshared = newValue + end subroutine + + subroutine sgg_set_Hshared(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(Shared_t), intent(in) :: newValue + sgg%Hshared = newValue + end subroutine + + subroutine sgg_set_Alloc(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(XYZlimit_t), intent(in) :: newValue(1:6) + sgg%Alloc = newValue + end subroutine + + subroutine sgg_set_Sweep(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(XYZlimit_t), intent(in) :: newValue(1:6) + sgg%Sweep = newValue + end subroutine + + subroutine sgg_set_SINPMLSweep(sgg, newValue) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(XYZlimit_t), intent(in) :: newValue(1:6) + sgg%SINPMLSweep = newValue + end subroutine + + subroutine sgg_set_thereAreMagneticMedia(sgg, value) + type(SGGFDTDINFO_t), intent(inout) :: sgg + logical, intent(in) :: value + sgg%thereAreMagneticMedia = value + end subroutine + + subroutine sgg_set_thereArePMLMagneticMedia(sgg, value) + type(SGGFDTDINFO_t), intent(inout) :: sgg + logical, intent(in) :: value + sgg%thereArePMLMagneticMedia = value + end subroutine + + subroutine sgg_set_nEntradaRoot(sgg, value) + type(SGGFDTDINFO_t), intent(inout) :: sgg + character(len=*), intent(in) :: value + sgg%nEntradaRoot = value + end subroutine + + subroutine sgg_set_Punto(sgg, value) + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(coorsxyzP_t), intent(in) :: value + sgg%Punto = value + end subroutine + + subroutine sgg_add_observation(sgg, new_observation) + implicit none + + type(SGGFDTDINFO_t), intent(inout) :: sgg + type(Obses_t), intent(in), target :: new_observation + + type(Obses_t), dimension(:), pointer :: temp_obs + integer :: old_size, new_size + + old_size = sgg%NumberRequest + new_size = old_size + 1 + + allocate (temp_obs(1:new_size)) + + if (old_size > 0) then + temp_obs(1:old_size) = sgg%Observation(1:old_size) + deallocate (sgg%Observation) + end if + + temp_obs(new_size) = new_observation + + sgg%Observation => temp_obs + + sgg%NumberRequest = new_size + + end subroutine sgg_add_observation + +end module sggMethods_m diff --git a/testData/input_examples/probes/movie_and_frequency_slices.fdtd.json b/testData/input_examples/probes/movie_and_frequency_slices.fdtd.json new file mode 100644 index 00000000..a6686d01 --- /dev/null +++ b/testData/input_examples/probes/movie_and_frequency_slices.fdtd.json @@ -0,0 +1,197 @@ +{ + "_format": "FDTD Input file", + "general": { + "timeStep": 3.0813e-12, + "numberOfSteps": 1298 + }, + "boundary": { + "all": { + "type": "mur" + } + }, + "mesh": { + "grid": { + "numberOfCells": [ + 30, + 30, + 30 + ], + "steps": { + "x": [ + 0.002 + ], + "y": [ + 0.002 + ], + "z": [ + 0.002 + ] + } + }, + "coordinates": [ + { + "id": 1, + "relativePosition": [ + 0.0, + 10.0, + 10.0 + ] + }, + { + "id": 2, + "relativePosition": [ + 10.0, + 10.0, + 10.0 + ] + }, + { + "id": 3, + "relativePosition": [ + 10.0, + 0.0, + 10.0 + ] + } + ], + "elements": [ + { + "id": 1, + "type": "cell", + "intervals": [ + [ + [ + 0.0, + 20.0, + 20.0 + ], + [ + 20.0, + 20.0, + 20.0 + ] + ], + [ + [ + 20.0, + 20.0, + 20.0 + ], + [ + 20.0, + 20.0, + 10.0 + ] + ], + [ + [ + 20.0, + 20.0, + 10.0 + ], + [ + 20.0, + 0.0, + 10.0 + ] + ] + ] + }, + { + "id": 2, + "type": "cell", + "intervals": [ + [ + [ + 15, + 15, + 15 + ], + [ + 25, + 25, + 25 + ] + ] + ] + } + ] + }, + "materials": [], + "materialAssociations": [], + "sources": [ + { + "name": "nodalSource", + "type": "nodalSource", + "magnitudeFile": "predefinedExcitation.1.exc", + "elementIds": [ + 1 + ] + } + ], + "probes": [ + { + "name": "electric_field_movie_x", + "type": "movie", + "field": "electric", + "component": "x", + "elementIds": [2] + }, + { + "name": "magnetic_field_movie_y", + "type": "movie", + "field": "magnetic", + "component": "y", + "elementIds": [2] + }, + { + "name": "current_density_movie_z", + "type": "movie", + "field": "currentDensity", + "component": "z", + "elementIds": [2] + }, + { + "name": "electric_field_frequency_slice_x", + "type": "movie", + "field": "electric", + "component": "x", + "elementIds": [2], + "domain": { + "type": "frequency", + "initialFrequency": 1e6, + "finalFrequency": 1e9, + "numberOfFrequencies": 30, + "frequencySpacing": "logarithmic" + } + }, + { + "name": "magnetic_field_frequency_slice_y", + "type": "movie", + "field": "magnetic", + "component": "y", + "elementIds": [2], + "domain": { + "type": "frequency", + "initialFrequency": 1e6, + "finalFrequency": 1e9, + "numberOfFrequencies": 30, + "frequencySpacing": "logarithmic" + } + }, + { + "name": "current_density_frequency_slice_z", + "type": "movie", + "field": "currentDensity", + "component": "z", + "elementIds": [2], + "domain": { + "type": "frequency", + "initialFrequency": 1e6, + "finalFrequency": 1e9, + "numberOfFrequencies": 30, + "frequencySpacing": "logarithmic" + } + } + ] +} \ No newline at end of file diff --git a/testData/input_examples/probes/time_movie_over_cube.fdtd.json b/testData/input_examples/probes/time_movie_over_cube.fdtd.json index 663f1512..771039e4 100644 --- a/testData/input_examples/probes/time_movie_over_cube.fdtd.json +++ b/testData/input_examples/probes/time_movie_over_cube.fdtd.json @@ -48,7 +48,7 @@ ] ] ] - }, + } ] }, "materials": [ From d2104f0dd88c1310e8653c3ead157bdec5a58885 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Mon, 6 Jul 2026 15:31:24 +0200 Subject: [PATCH 03/10] FDTD | ENV | #409 | Allow usage of isolated IA on containers --- .devcontainer/Dockerfile | 1 - .devcontainer/devcontainer-lock.json | 14 ++++ .devcontainer/devcontainer.json | 90 +++++++++++---------- .devcontainer/notes.md | 12 --- .devcontainer/setup-python.sh | 6 ++ .dockerignore | 26 +++++- .vscode/launch.dev.json | 74 +++++++++++++++++ CMakeLists.txt | 3 + CMakePresets.json | 53 +++++++++++- Dockerfile | 115 ++++++++++++++++++++++++--- docker-compose.yml | 100 ++++++++++++++++++++++- scripts/devcontainer-post-start.sh | 48 +++++++++++ simulations/SEMBA_FDTD_temp.log | 21 +++++ test/pyWrapper/utils.py | 20 ++++- 14 files changed, 507 insertions(+), 76 deletions(-) delete mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer-lock.json delete mode 100644 .devcontainer/notes.md create mode 100755 .devcontainer/setup-python.sh create mode 100644 .vscode/launch.dev.json create mode 100755 scripts/devcontainer-post-start.sh create mode 100644 simulations/SEMBA_FDTD_temp.log diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 6c632061..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM intel/oneapi-hpckit \ No newline at end of file diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 00000000..0500829a --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "1.8.0", + "resolved": "ghcr.io/devcontainers/features/python@sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511", + "integrity": "sha256:fbcad6955caeecc5ad3f7886baf652e25cba5225a6c4c2287c536de2e5607511" + }, + "ghcr.io/msclock/features/vcpkg:2": { + "version": "2.0.0", + "resolved": "ghcr.io/msclock/features/vcpkg@sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471", + "integrity": "sha256:bcb75d475252af1f9ef742860387cb15e059f57041748abdd02030cd1a181471" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bed4a83e..075c5643 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,46 +1,48 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/cpp { - "name": "OpenSEMBA dev. framework", - "build": { - "dockerfile": "Dockerfile" - }, - "features": { - "ghcr.io/msclock/features/vcpkg:2": {}, - "ghcr.io/devcontainers/features/python:1": {} - }, - - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - - // Configure tool-specific properties. - "customizations": { - "codespaces": { - "repositories": { - "lmdiazangulo/json-fortran": { "permissions": "read-all" }, - "opensemba/fhash": { "permissions": "read-all" }, - "reference-lapack/lapack": { "permissions": "read-all" }, - "opensemba/ngtest": {"permissions": "read-all" }, - "google/googletest": { "permissions": "read-all" } - } - }, - "vscode": { - "extensions": [ - "ms-toolsai.jupyter", - "fortran-lang.linter-gfortran", - "ms-vscode.cmake-tools", - "ms-python.autopep8", - "matepek.vscode-catch2-test-adapter" - ] - } - } - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + "name": "semba-fdtd dev", + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "dev", + "workspaceFolder": "/home/developer/workspaces/fdtd", + "remoteUser": "developer", + "containerUser": "developer", + "overrideCommand": false, + "features": { + "ghcr.io/msclock/features/vcpkg:2": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + "remoteEnv": { + "HOME": "/home/developer", + "XDG_CONFIG_HOME": "/home/developer/.config", + "XDG_DATA_HOME": "/home/developer/.local/share", + "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}" + }, + "postCreateCommand": ".devcontainer/setup-python.sh", + "postStartCommand": "bash scripts/devcontainer-post-start.sh", + "customizations": { + "codespaces": { + "repositories": { + "lmdiazangulo/json-fortran": { "permissions": "read-all" }, + "opensemba/fhash": { "permissions": "read-all" }, + "reference-lapack/lapack": { "permissions": "read-all" }, + "opensemba/ngtest": { "permissions": "read-all" }, + "google/googletest": { "permissions": "read-all" } + } + }, + "vscode": { + "extensions": [ + "fortran-lang.linter-gfortran", + "ms-vscode.cmake-tools", + "ms-python.python", + "eamodio.gitlens" + ], + "settings": { + "cmake.buildDirectory": "${workspaceFolder}/build-dev", + "cmake.useCMakePresets": "always", + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "terminal.integrated.defaultProfile.linux": "bash" + } + } + } } diff --git a/.devcontainer/notes.md b/.devcontainer/notes.md deleted file mode 100644 index 26d157f4..00000000 --- a/.devcontainer/notes.md +++ /dev/null @@ -1,12 +0,0 @@ - -``` -git submodule init -git submodule --recursive -``` - -Install python packages -``` -python3 -m venv ~/py_envs -source ~/py_envs/bin/activate -python3 -m pip install -r requirements.txt -``` \ No newline at end of file diff --git a/.devcontainer/setup-python.sh b/.devcontainer/setup-python.sh new file mode 100755 index 00000000..5912f178 --- /dev/null +++ b/.devcontainer/setup-python.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +python3 -m venv .venv +.venv/bin/python -m pip install --upgrade pip +.venv/bin/python -m pip install -r requirements.txt diff --git a/.dockerignore b/.dockerignore index be16480f..5924ac63 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,10 @@ .git build/ +build-*/ .venv/ +.devcontainer/ +.vscode/ +.github/ __pycache__/ *.pyc *.pyo @@ -9,4 +13,24 @@ __pycache__/ dist/ *.o *.mod -# Keep precompiled_libraries and external/ submodule contents + +# Runtime/development bind mounts do not need to be copied into image layers. +simulations/ + +# Linux Docker builds only use the GNU LAPACK bundle. +precompiled_libraries/** +!precompiled_libraries/ +!precompiled_libraries/linux-gcc/ +!precompiled_libraries/linux-gcc/** + +# The top-level build only consumes selected external sources. +external/lapack/ +external/ngspice/examples/ +external/ngspice/tests/ +external/ngspice/visualc/ +external/ngspice/man/ +external/ngspice/doc/ +external/ngspice/.git/ +external/googletest/.git/ +external/json-fortran/.git/ +external/fhash/.git/ diff --git a/.vscode/launch.dev.json b/.vscode/launch.dev.json new file mode 100644 index 00000000..28d56134 --- /dev/null +++ b/.vscode/launch.dev.json @@ -0,0 +1,74 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug semba-fdtd (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/semba-fdtd", + "args": ["${input:inputFile}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: build Debug no-MPI" + }, + { + "name": "Debug semba-fdtd (dbg-mpi)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg-mpi/bin/semba-fdtd", + "args": ["${input:inputFile}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "Debug fdtd_tests (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build-dbg/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: build Debug no-MPI" + }, + { + "name": "Attach to process", + "type": "cppdbg", + "request": "attach", + "program": "${workspaceFolder}/build-dbg/bin/semba-fdtd", + "processId": "${command:pickProcess}", + "MIMode": "gdb" + } + ], + "inputs": [ + { + "id": "inputFile", + "type": "promptString", + "description": "Path to .fdtd.json input file", + "default": "testData/input_examples/sphere.fdtd.json" + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c65d14a..36714a85 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,9 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux") if(CMAKE_Fortran_COMPILER_ID MATCHES "GNU") message(STATUS "Using GNU flags") + set(CMAKE_C_STANDARD 17) + set(CMAKE_C_STANDARD_REQUIRED ON) + set(CMAKE_C_EXTENSIONS ON) set(CMAKE_CXX_FLAGS "-fopenmp") set(CMAKE_Fortran_FLAGS "-fopenmp -ffree-form -ffree-line-length-none -fdec -fallow-argument-mismatch") diff --git a/CMakePresets.json b/CMakePresets.json index 3f068ff0..f61d693e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -6,7 +6,8 @@ "generator": "Ninja", "binaryDir": "build-rls/", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "SEMBA_FDTD_ENABLE_MPI": "OFF" } }, { @@ -17,6 +18,22 @@ "CMAKE_BUILD_TYPE": "Debug" } }, + { + "name": "rls-mpi", + "inherits": "rls", + "binaryDir": "build-rls-mpi/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, + { + "name": "dbg-mpi", + "inherits": "dbg", + "binaryDir": "build-dbg-mpi/", + "cacheVariables": { + "SEMBA_FDTD_ENABLE_MPI": "ON" + } + }, { "name": "dbg-nomtln", "inherits": "dbg", @@ -58,5 +75,39 @@ "SEMBA_FDTD_ENABLE_MTLN": "OFF" } } + ], + "buildPresets": [ + { + "name": "rls", + "configurePreset": "rls" + }, + { + "name": "dbg", + "configurePreset": "dbg" + }, + { + "name": "rls-mpi", + "configurePreset": "rls-mpi" + }, + { + "name": "dbg-mpi", + "configurePreset": "dbg-mpi" + }, + { + "name": "rls-nomtln", + "configurePreset": "rls-nomtln" + }, + { + "name": "dbg-nomtln", + "configurePreset": "dbg-nomtln" + }, + { + "name": "intel-rls", + "configurePreset": "intel-rls" + }, + { + "name": "intel-rls-nomtln", + "configurePreset": "intel-rls-nomtln" + } ] } diff --git a/Dockerfile b/Dockerfile index cf0830e4..66316a46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,58 @@ -# ─── Stage 1: Builder ───────────────────────────────────────────────────────── -FROM ubuntu:22.04@sha256:eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 AS builder +# syntax=docker/dockerfile:1.7 + +# ─── Base ───────────────────────────────────────────────────────────────────── +FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS base ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ + locales \ gfortran \ g++ \ cmake \ make \ + ninja-build \ libhdf5-dev \ libopenmpi-dev \ python3 \ python3-pip \ + python3-venv \ gdb \ gdbserver \ + && locale-gen en_US.UTF-8 \ && rm -rf /var/lib/apt/lists/* +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +# ─── Stage 1: Builder ───────────────────────────────────────────────────────── +FROM base AS builder + WORKDIR /src COPY . . -# Install Python test/wrapper dependencies -RUN python3 -m pip install --no-cache-dir -r requirements.txt - # Build (MPI off by default; override at build time with --build-arg ENABLE_MPI=ON) ARG ENABLE_MPI=OFF ARG ENABLE_MTLN=ON ARG BUILD_TYPE=Release +ARG ENABLE_TEST=OFF + +RUN cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DSEMBA_FDTD_ENABLE_MPI=${ENABLE_MPI} \ + -DSEMBA_FDTD_ENABLE_HDF=ON \ + -DSEMBA_FDTD_ENABLE_MTLN=${ENABLE_MTLN} \ + -DSEMBA_FDTD_ENABLE_SMBJSON=ON \ + -DSEMBA_FDTD_ENABLE_TEST=${ENABLE_TEST} \ + && cmake --build build -j$(nproc) \ + && cp build/bin/semba-fdtd /tmp/semba-fdtd \ + && if [ "${BUILD_TYPE}" = "Release" ]; then strip /tmp/semba-fdtd; fi + +# ─── Stage 1b: Test Builder ─────────────────────────────────────────────────── +FROM builder AS test-builder RUN cmake -S . -B build \ -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ @@ -36,22 +63,84 @@ RUN cmake -S . -B build \ -DSEMBA_FDTD_ENABLE_TEST=ON \ && cmake --build build -j$(nproc) -# ─── Stage 2: Runtime ───────────────────────────────────────────────────────── +# Install Python test/wrapper dependencies only for images that run tests. +RUN python3 -m venv /opt/fdtd-venv \ + && /opt/fdtd-venv/bin/python -m pip install --upgrade pip \ + && /opt/fdtd-venv/bin/python -m pip install --no-cache-dir -r requirements.txt + +ENV PATH=/opt/fdtd-venv/bin:${PATH} + +# ─── Stage 2: Development Tools ──────────────────────────────────────────────── +FROM base AS dev-tools + +USER root + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ + git \ + gh \ + openssh-client \ + nodejs \ + npm \ + sudo \ + && rm -rf /var/lib/apt/lists/* + +RUN --mount=type=cache,target=/root/.npm \ + npm install -g opencode-ai + +RUN --mount=type=cache,target=/root/.cache/pip \ + python3 -m pip install --break-system-packages fortls fprettify + +# ─── Stage 2b: Development User ──────────────────────────────────────────────── +FROM dev-tools AS dev + +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 + +ENV USERNAME=${USERNAME} \ + USER_UID=${USER_UID} \ + USER_GID=${USER_GID} + +RUN userdel --remove ubuntu 2>/dev/null || true \ + && groupdel ubuntu 2>/dev/null || true \ + && groupadd --gid ${USER_GID} ${USERNAME} \ + && useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME} \ + && mkdir -p /home/${USERNAME}/.config \ + && mkdir -p /home/${USERNAME}/.ssh \ + && mkdir -p /home/${USERNAME}/.local/share/opencode \ + && chmod 700 /home/${USERNAME}/.ssh \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.ssh \ + && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.local \ + && usermod -aG sudo ${USERNAME} \ + && echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} + +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd + +# ─── Stage 3: Runtime ───────────────────────────────────────────────────────── # Minimal image with only the shared libraries the binary needs at runtime. -# HDF5, LAPACK, BLAS, and ngspice are all statically linked on Linux, -# so only the Fortran/OpenMP runtimes and HDF5 transitive deps are required. -FROM ubuntu:22.04@sha256:eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 AS runtime +# HDF5, LAPACK, BLAS, and ngspice are all statically linked on Linux. +# Keep OpenMPI runtime libraries available for MPI-enabled image builds. +FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS runtime ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ libgfortran5 \ libgomp1 \ + libcurl4t64 \ zlib1g \ - libaec2 \ + libaec0 \ + libsz2 \ && rm -rf /var/lib/apt/lists/* -COPY --from=builder /src/build/bin/semba-fdtd /usr/local/bin/semba-fdtd +COPY --from=builder /tmp/semba-fdtd /usr/local/bin/semba-fdtd WORKDIR /work diff --git a/docker-compose.yml b/docker-compose.yml index b9fd2ffa..b456a9a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,43 @@ services: - ./simulations:/work working_dir: /work + dev: + image: fdtd-dev:latest + build: + context: . + target: dev + environment: + HOME: /home/developer + XDG_CONFIG_HOME: /home/developer/.config + XDG_DATA_HOME: /home/developer/.local/share + volumes: + - type: bind + source: . + target: /home/developer/workspaces/fdtd + - type: bind + source: ${HOME}/.gitconfig + target: /home/developer/.gitconfig + - type: bind + source: ${HOME}/.ssh + target: /home/developer/.ssh + read_only: true + - type: bind + source: ${HOME}/.config/gh + target: /home/developer/.config/gh + - type: bind + source: ${HOME}/.config/opencode + target: /home/developer/.config/opencode + - type: bind + source: ${HOME}/.local/share/opencode + target: /home/developer/.local/share/opencode + - type: bind + source: ./build-dev + target: /home/developer/workspaces/fdtd/build-dev + - type: bind + source: ./build-dev-mpi + target: /home/developer/workspaces/fdtd/build-dev-mpi + working_dir: /home/developer/workspaces/fdtd + tty: true # Debug a simulation with gdbserver (connect from VSCode with launch.json) # Usage: docker compose run --rm -p 2345:2345 debug case.fdtd.json debug: @@ -25,11 +62,11 @@ services: working_dir: /work entrypoint: ["gdbserver", ":2345", "/src/build/bin/semba-fdtd", "-i"] - # Build the project and run all tests + # Build the project and run non-MPI tests test: build: context: . - target: builder + target: test-builder args: BUILD_TYPE: Release ENABLE_MPI: "OFF" @@ -38,6 +75,63 @@ services: SEMBA_FDTD_ENABLE_MPI: "OFF" SEMBA_FDTD_ENABLE_MTLN: "ON" SEMBA_FDTD_ENABLE_HDF: "ON" + SEMBA_EXE: /src/build/bin/semba-fdtd working_dir: /src command: > - sh -c "build/bin/fdtd_tests && python3 -m pytest test/ --durations=20" + sh -c "build/bin/fdtd_tests && python3 -m pytest test/ -m 'not mpi' --durations=20" + + # Build with MPI enabled and run MPI integration tests + test-mpi: + build: + context: . + target: test-builder + args: + BUILD_TYPE: Release + ENABLE_MPI: "ON" + ENABLE_MTLN: "ON" + environment: + SEMBA_FDTD_ENABLE_MPI: "ON" + SEMBA_FDTD_ENABLE_MTLN: "ON" + SEMBA_FDTD_ENABLE_HDF: "ON" + SEMBA_EXE: /src/build/bin/semba-fdtd + OMPI_ALLOW_RUN_AS_ROOT: "1" + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: "1" + working_dir: /src + command: > + sh -c "build/bin/fdtd_tests && python3 -m pytest test/ -m mpi --durations=20" + + # Interactive shell in the non-MPI test image + shell: + build: + context: . + target: test-builder + args: + BUILD_TYPE: Debug + ENABLE_MPI: "OFF" + ENABLE_MTLN: "ON" + environment: + SEMBA_FDTD_ENABLE_MPI: "OFF" + SEMBA_FDTD_ENABLE_MTLN: "ON" + SEMBA_FDTD_ENABLE_HDF: "ON" + SEMBA_EXE: /src/build/bin/semba-fdtd + working_dir: /src + entrypoint: ["bash"] + + # Interactive shell in the MPI-enabled test image + shell-mpi: + build: + context: . + target: test-builder + args: + BUILD_TYPE: Debug + ENABLE_MPI: "ON" + ENABLE_MTLN: "ON" + environment: + SEMBA_FDTD_ENABLE_MPI: "ON" + SEMBA_FDTD_ENABLE_MTLN: "ON" + SEMBA_FDTD_ENABLE_HDF: "ON" + SEMBA_EXE: /src/build/bin/semba-fdtd + OMPI_ALLOW_RUN_AS_ROOT: "1" + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: "1" + working_dir: /src + entrypoint: ["bash"] diff --git a/scripts/devcontainer-post-start.sh b/scripts/devcontainer-post-start.sh new file mode 100755 index 00000000..db494d5d --- /dev/null +++ b/scripts/devcontainer-post-start.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -u + +expected_home="/home/developer" + +warn() { + printf 'devcontainer warning: %s\n' "$*" >&2 +} + +if [ "$(id -un)" != "developer" ]; then + warn "running as $(id -un), expected developer" +fi + +if [ "${HOME:-}" != "$expected_home" ]; then + warn "HOME is ${HOME:-unset}, expected $expected_home" +fi + +if [ "${XDG_CONFIG_HOME:-}" != "$expected_home/.config" ]; then + warn "XDG_CONFIG_HOME is ${XDG_CONFIG_HOME:-unset}, expected $expected_home/.config" +fi + +if [ "${XDG_DATA_HOME:-}" != "$expected_home/.local/share" ]; then + warn "XDG_DATA_HOME is ${XDG_DATA_HOME:-unset}, expected $expected_home/.local/share" +fi + +if ! command -v opencode >/dev/null 2>&1; then + warn "opencode is not available in PATH" +fi + +if command -v git >/dev/null 2>&1; then + git config --global --add safe.directory /home/developer/workspaces/fdtd >/dev/null 2>&1 || \ + warn "could not add workspace to git safe.directory" +fi + +if [ ! -d "$expected_home/.config/opencode" ]; then + warn "$expected_home/.config/opencode is not mounted" +fi + +if [ ! -d "$expected_home/.local/share/opencode" ]; then + warn "$expected_home/.local/share/opencode is not mounted" +fi + +if [ ! -f "$expected_home/.local/share/opencode/auth.json" ]; then + warn "OpenCode provider auth is missing at $expected_home/.local/share/opencode/auth.json" + warn "run opencode /connect once on the host or in this container if providers are unavailable" +fi + +exit 0 diff --git a/simulations/SEMBA_FDTD_temp.log b/simulations/SEMBA_FDTD_temp.log new file mode 100644 index 00000000..9c32d64c --- /dev/null +++ b/simulations/SEMBA_FDTD_temp.log @@ -0,0 +1,21 @@ +========================= +semba-fdtd +========================= +__________________________________________ +Compilation date: Jul 7 2026 10:24:21 +Compiler Id: GNU 15.2.0 +git commit: UNKNOWN +cmake build type: Release +cmake compilation flags: -fopenmp -ffree-form -ffree-line-length-none -fdec -fallow-argument-mismatch -Ofast +__________________________________________ +__________________________________________ +All rights reserved by the University of Granada (Spain) +Contact person: Luis D. Angulo + +__________________________________________ +Compiled WITH .h5 HDF support +Compiled WITH MTLN support +Compiled WITH SMBJSON support +__________________________________________ +Launched on 07/07/2026 10:26 +( 1/ 1) ERROR: The input file was not found semba-fdtd.nfde diff --git a/test/pyWrapper/utils.py b/test/pyWrapper/utils.py index 5b2612cf..d454745a 100644 --- a/test/pyWrapper/utils.py +++ b/test/pyWrapper/utils.py @@ -1,4 +1,5 @@ from src_pyWrapper.pyWrapper import * +import os import shutil import glob import re @@ -32,6 +33,23 @@ reason="MPI is not available", ) +def _default_semba_exe(): + exe_name = 'semba-fdtd.exe' if platform == "win32" else 'semba-fdtd' + build_dirs = ['build'] + + if os.getenv("SEMBA_FDTD_ENABLE_MPI") == "ON": + build_dirs.extend(['build-rls-mpi', 'build-dbg-mpi']) + else: + build_dirs.extend(['build-rls', 'build-dbg']) + + for build_dir in build_dirs: + candidate = os.path.join(os.getcwd(), build_dir, 'bin', exe_name) + if os.path.isfile(candidate): + return candidate + + return os.path.join(os.getcwd(), 'build', 'bin', exe_name) + + # Use of absolute path to avoid conflicts when changing directory. if platform == "linux": SEMBA_EXE = os.path.join(os.getcwd(), 'build', 'bin', 'semba-fdtd') @@ -212,4 +230,4 @@ def corrcoef_on_common_time(t_ref, y_ref, t_cmp, y_cmp): t_common = t_ref[mask] y_ref_common = y_ref[mask] y_cmp_interp = np.interp(t_common, t_cmp, y_cmp) - return np.corrcoef(y_ref_common, y_cmp_interp)[0, 1] \ No newline at end of file + return np.corrcoef(y_ref_common, y_cmp_interp)[0, 1] From 33cc2288dfd19f151d2232b89588a834b463f0c1 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Mon, 13 Jul 2026 06:35:49 +0000 Subject: [PATCH 04/10] FDTD | ENV | #409 | Include specific fortran skills --- .../fortran-module-readability/SKILL.md | 203 ++++++++++++++++++ .../fortran-performance-awareness/SKILL.md | 169 +++++++++++++++ .../fortran-refactor-cleanliness/SKILL.md | 179 +++++++++++++++ 3 files changed, 551 insertions(+) create mode 100644 .opencode/skills/fortran-module-readability/SKILL.md create mode 100644 .opencode/skills/fortran-performance-awareness/SKILL.md create mode 100644 .opencode/skills/fortran-refactor-cleanliness/SKILL.md diff --git a/.opencode/skills/fortran-module-readability/SKILL.md b/.opencode/skills/fortran-module-readability/SKILL.md new file mode 100644 index 00000000..164ca78d --- /dev/null +++ b/.opencode/skills/fortran-module-readability/SKILL.md @@ -0,0 +1,203 @@ +--- +name: fortran-module-readability +description: Use whenever creating, splitting, moving, or substantially restructuring Fortran modules in semba-fdtd. This is the right skill for .F90 module boundaries, public/private APIs, use/import lists, derived-type ownership, procedure ordering, CMake source placement, and naming. Prefer this skill for module-level design questions even if the user only says "clean up this module" or "add a new type"; combine it with fortran-performance-awareness when the module contains hot FDTD kernels, MPI/OpenMP paths, or large array operations. +--- + +# Fortran Module Readability + +Use this skill when writing a new Fortran module, moving code across modules, adding exported types or procedures, or substantially restructuring an existing module in this repository. +The goal is a module that has one clear purpose, a small public surface, readable internal organization, and dependencies that fit the existing `semba-fdtd` library layering. +For modules containing hot kernels, large array operations, MPI/OpenMP code, or repeated allocation paths, also use `fortran-performance-awareness`. + +## First Pass + +Before changing a module boundary, build enough context to avoid accidental API or dependency damage: + +1. Identify the module's current responsibility, public exports, and callers. +2. Check nearby modules in the same source directory for established naming, import, and visibility style. +3. Search for public type, component, procedure, and module-name references before renaming or moving anything. +4. Note optional preprocessor branches that may expose different dependencies from the local build. +5. Decide whether the task is module design, ordinary refactoring, performance work, or a mix; use the narrowest applicable change. + +## Core Principles + +Write modules as explicit boundaries: + +- Give each module one primary responsibility that can be stated in one sentence. +- Keep public APIs small, intentional, and stable. +- Prefer clear domain names over generic implementation names. +- Separate data definitions, parsing/conversion, numerical operations, and output concerns unless they are naturally part of the same abstraction. +- Avoid introducing new global state; pass data through arguments or owned derived-type components when practical. +- Keep changes local unless a broader module boundary change is explicitly requested. + +## Module Skeleton + +Prefer this structure for new or cleaned-up modules: + +```fortran +module example_module + use dependency_module, only: dependency_type, dependency_routine + + implicit none + + private + + + public :: example_type + public :: init_example + + + type :: example_type + ! Components here. + end type example_type + +contains + + subroutine init_example(example) + type(example_type), intent(out) :: example + + + end subroutine init_example + +end module example_module +``` + +Use existing local formatting when editing established files, but keep the same conceptual order when it does not create churn: + +1. `module` statement. +2. `use` statements. +3. `implicit none`. +4. default visibility, usually `private`. +5. explicit `public` declarations. +6. parameters, interfaces, and derived types. +7. `contains` procedures. +8. `end module module_name`. + +## Public API Design + +Default to a private module and explicit exports: + +- Use `private` near the top of the module unless the existing module style makes that too disruptive. +- Export only the types, constants, interfaces, and procedures intended for callers. +- Keep helper procedures private by default. +- Avoid exporting implementation details just because tests or nearby code can reach them; prefer testing through the real public behaviour. +- Avoid renaming public procedures or derived-type components that are already used broadly unless the task explicitly includes API cleanup. +- Preserve names that map to input formats, output labels, physics terms, or legacy NFDE/JSON concepts unless the user approves a compatibility-impacting change. + +Good public APIs read like a short capability list. If the public list is long, look for mixed responsibilities before adding more exports. + +When changing an existing public API, distinguish between three cases: + +- Internal-only exports with few callers can often be cleaned up in one small change after checking references. +- Input/output-facing names, serialized labels, and physics terms are compatibility-sensitive; preserve them unless the user asked for that change. +- Widely used interfaces should be changed only when the replacement makes callers clearer enough to justify the churn. + +## Dependency Hygiene + +Keep imports narrow and dependency direction clear: + +- Prefer `use module_name, only: symbol_a, symbol_b` for new modules and touched imports. +- Do not introduce circular dependencies. +- Respect the existing CMake library layering: lower-level type/report/parser/component code should not depend on higher-level solver, launcher, or output orchestration code. +- Keep optional-feature boundaries intact for `CompileWithMPI`, `CompileWithMTLN`, `CompileWithSMBJSON`, `CompileWithNewOutputModule`, `CompileWithReal8`, and similar preprocessor paths. +- Avoid a shared utility module unless there is a concrete repeated pattern across multiple callers. +- Prefer passing dependencies as arguments over reaching into unrelated modules for mutable state. + +When a new module needs symbols from several distant layers, pause and check whether the responsibility belongs somewhere else. + +## Derived Types + +Use derived types to express ownership and domain concepts clearly: + +- Name types after the domain object they represent, not just storage shape. +- Keep components cohesive; avoid derived types that become bags of unrelated solver, parser, and output state. +- Make allocation ownership obvious from initialization and finalization routines. +- Prefer type-bound procedures only when they clarify ownership or behaviour; do not add them mechanically. +- Keep pointer components only when aliasing or association is required. Prefer existing project patterns over changing ownership semantics. +- Document non-obvious invariants such as array bounds, coordinate ordering, field staggering, or MPI-local versus global indexing. + +If a derived type needs many setup steps, provide one clear initializer instead of requiring callers to know internal ordering. + +## Procedure Organization + +Write procedures that expose intent without hiding numerical behaviour: + +- Put the public, high-level procedures first when that matches the reading flow. +- Keep private helpers close to the public procedure that uses them when they are local to that concept. +- Use explicit `intent(in)`, `intent(out)`, or `intent(inout)` for arguments. +- Avoid long argument lists when a cohesive derived type already exists, but do not introduce a derived type just to reduce line length. +- Extract helpers for named concepts, not for every small block. +- Preserve loop order, update ordering, and array shape semantics in numerical code. +- Keep error handling and validation near the boundary where invalid data enters the module. + +A good procedure name should let a caller understand what happens without reading the body. A good body should still make the numerical or data-flow steps visible. + +## Naming And Comments + +Prefer names that match the repository's domain language: + +- Use established field, material, geometry, parser, output, MTLN, and boundary-condition terminology. +- Avoid overly broad names such as `manager`, `handler`, `processor`, or `data` unless that is already the local convention. +- Use consistent prefixes only when they help group related procedures or avoid ambiguity. +- Name boolean procedures and variables as predicates when practical, for example `is_valid`, `has_source`, or `uses_mpi`. +- Add comments for surprising constraints, physical assumptions, indexing conventions, preprocessor requirements, or file-format compatibility. +- Do not add comments that restate the code line-by-line. + +## File And Build Integration + +When adding a module: + +1. Place the file in the directory matching its layer and responsibility. +2. Add it to the correct CMake target or source list without changing unrelated target boundaries. +3. Confirm optional-feature guards still compile for relevant configurations. +4. Search for similarly named modules before creating a new one. +5. Prefer a small module over modifying a broad catch-all module, but do not fragment one concept across many files. + +When moving code between existing modules, update CMake only if the file list or target ownership changes. Avoid moving a source file into a higher-level library just to access a dependency; that often signals the responsibility belongs elsewhere. + +## Modularity Guardrails + +Avoid these patterns unless there is an explicit reason: + +- A module that mixes parsing, solver state updates, and output formatting. +- A new dependency from `src_conformal`, `src_json_parser`, `src_mtln`, or lower-level type code into `src_main_pub` orchestration code. +- A public helper module created for one caller. +- A module-level mutable variable used as hidden communication between procedures. +- A generic abstraction that erases important electromagnetic, indexing, or file-format meaning. +- A rewrite from procedural module style to object-oriented style just for style consistency. + +## Review Checklist + +Before finishing a new or restructured module, check: + +- The module purpose is clear from its name and public API. +- `implicit none` is present. +- Visibility is explicit, preferably `private` by default. +- Public exports are minimal and intentional. +- Imports use `only:` where practical. +- The module sits in the correct source directory and CMake layer. +- Derived types have clear ownership and initialization rules. +- Procedures have explicit argument intents. +- Numerical order, array bounds, precision kinds, and preprocessor branches are preserved. +- Comments explain constraints or intent, not obvious syntax. +- The module can be tested through public behaviour. + +## Verification + +For module-writing changes, verify with the narrowest practical command first: + +```bash +cmake --build build -j +``` + +When the change affects parser, output, MTLN, or solver behaviour, run the corresponding focused tests when available. If a build or test cannot be run because the local environment is not configured, state that clearly. + +## Final Response Expectations + +When reporting module changes, include: + +- The module files changed or added. +- The public API shape introduced or modified. +- The dependency or CMake integration point touched. +- Whether behaviour was intended to change. +- The build or tests run, including any skipped verification. diff --git a/.opencode/skills/fortran-performance-awareness/SKILL.md b/.opencode/skills/fortran-performance-awareness/SKILL.md new file mode 100644 index 00000000..ccca0e87 --- /dev/null +++ b/.opencode/skills/fortran-performance-awareness/SKILL.md @@ -0,0 +1,169 @@ +--- +name: fortran-performance-awareness +description: Use whenever writing, reviewing, diagnosing, or refactoring performance-sensitive Fortran in semba-fdtd. Trigger for FDTD time-step loops, field/material/boundary/wire/MTLN updates, array access patterns, allocation or temporary-array concerns, MPI/OpenMP communication, I/O cadence, profiling results, or any user request about speed, scaling, memory use, or "making this faster." Use this even for small-looking cleanups inside numerical kernels because preserving update order and array semantics matters more than cosmetic refactoring. +--- + +# Fortran Performance Awareness + +Use this skill when working on performance-sensitive Fortran code in this repository, including review and diagnosis tasks where no code may be edited. +The goal is efficient code that preserves numerical correctness, keeps the physics readable, and avoids premature or unmeasured rewrites. + +## First Pass + +Start by locating the performance risk before proposing changes: + +1. Determine whether the code is in a repeated time-step path, setup path, output path, parser path, or test-only path. +2. Identify loop bounds, array ranks, lower bounds, halo regions, and any OpenMP or MPI ownership assumptions. +3. Look for obvious repeated costs such as allocation, I/O, string formatting, polymorphic dispatch, map lookups, or large temporary arrays. +4. Separate proven bottlenecks from plausible risks. A structural cleanup can be useful, but do not report it as a measured speedup without measurement. +5. If the task is mostly module/API restructuring, also use `fortran-module-readability`; if it is a broad cleanup, also use `fortran-refactor-cleanliness`. + +## Core Principles + +Optimize deliberately: + +- Preserve numerical behaviour unless the task explicitly asks for a behaviour change. +- Prefer simple, predictable hot loops over clever abstractions. +- Keep performance-sensitive code readable enough to audit for physics and indexing correctness. +- Optimize the actual bottleneck when measurements or code structure make it clear. +- Avoid broad rewrites when a local change removes the cost. +- Treat MPI/OpenMP synchronization, allocation, and I/O as explicit performance costs. + +## Hot Path Priorities + +Pay special attention to code inside time-step loops, field-update kernels, material updates, boundary-condition application, MPI exchange paths, and output sampling. + +Prefer code that: + +- Avoids allocation and deallocation inside repeated update loops. +- Avoids repeated string operations, file operations, type conversions, and lookups in numerical kernels. +- Keeps loop bodies small and branch structure understandable. +- Reuses precomputed constants when they are truly invariant over the loop. +- Avoids unnecessary temporary arrays, especially large field-sized temporaries. +- Keeps I/O cadence intentional and outside kernels when possible. +- Keeps synchronization points minimal and tied to actual data dependencies. + +When code is not on a hot path, avoid adding complexity for hypothetical speed. A parser or one-time setup routine can often favor clarity unless it allocates field-sized data repeatedly or dominates large-case startup. + +## FDTD-Specific Guardrails + +Treat update order as part of correctness: + +- Do not reorder electric and magnetic field updates without understanding the time-stepping scheme. +- Do not reorder boundary-condition, material, wire, MTLN, source, or observation operations casually. +- Preserve Yee-grid staggering, coordinate indexing, lower bounds, and halo assumptions. +- Preserve dispersive and anisotropic material update sequencing. +- Preserve CPML, Mur, MPI halo exchange, and far-field sampling ordering. +- Treat output labels, sampling cadence, and serialized array layout as observable behaviour. + +When an optimization touches these areas, verify with tests or a representative case whenever feasible. + +## Fortran Efficiency Guidelines + +Avoid accidental costs common in Fortran: + +- Prefer contiguous memory access patterns in hot loops. +- Be careful with array slices passed to procedures; non-contiguous slices can create temporaries. +- Avoid whole-array expressions on large arrays when they obscure allocation or temporary creation in hot paths. +- Use explicit interfaces and argument `intent` so compilers can reason about calls. +- Avoid unnecessary `pointer` aliasing in numerical kernels. +- Prefer `allocatable` for owned storage in new code, but do not change existing pointer ownership semantics just for style. +- Preserve established kind choices such as `RKIND`, `RKIND_tiempo`, `SINGLE`, and integer kinds unless precision or portability is the task. +- Avoid converting scalar helper functions into calls inside deeply nested loops unless the compiler can inline them or the readability benefit is worth the cost. +- Keep frequently used scalar values local when that avoids repeated component dereferences in hot loops. +- Be cautious with assumed-shape dummy arguments and array expressions in helper procedures called from kernels; check whether they can introduce copying or inhibit compiler optimization. +- Keep data layout and loop nesting aligned with Fortran column-major storage when this does not conflict with stencil dependencies or established code style. + +Do not make code less obviously correct for a theoretical speedup. If an optimization relies on a non-obvious compiler or memory-layout assumption, document it briefly. + +## Loop And Array Changes + +Before changing loops over field arrays: + +1. Identify the array dimensions, lower bounds, and memory layout. +2. Check whether loop order is chosen for cache locality, stencil dependencies, MPI halos, or readability. +3. Confirm whether OpenMP directives, reductions, or private variables depend on the current structure. +4. Preserve boundary ranges and off-by-one behaviour exactly unless fixing a known bug. +5. Compare results after the change when feasible. + +Avoid combining loop-order changes with unrelated cleanup. They should be reviewable as performance-sensitive changes. + +When changing a loop for locality, vectorization, or OpenMP scheduling, state the intended performance property in the code review summary. This helps reviewers distinguish deliberate numerical-kernel work from incidental formatting. + +## Parallel Code + +For MPI and OpenMP paths: + +- Keep data-sharing attributes explicit and correct. +- Preserve reductions and their numerical implications. +- Avoid moving MPI communication across computation phases unless dependencies are fully understood. +- Avoid adding barriers, critical sections, or atomics unless they are required for correctness. +- Watch for false sharing when introducing per-thread scratch data. +- Keep thread-local scratch allocation outside repeated parallel regions when practical. +- Preserve deterministic output ordering where the code currently guarantees it. + +Parallel performance changes must not weaken correctness under configurations that are not active in the local build. + +## Allocation And Ownership + +Allocation changes are performance and correctness changes: + +- Allocate once at setup time when the size is known and reused across time steps. +- Deallocate at clear ownership boundaries. +- Avoid hidden reallocation from assignment to allocatable arrays in hot paths. +- Keep scratch arrays local only when their size is small or the routine is not hot. +- Avoid module-level scratch state unless there is a clear ownership and thread-safety story. +- Preserve pointer association and aliasing semantics when editing legacy structures. + +## Readability Balance + +Efficient code should still be maintainable: + +- Keep domain names visible in formulas and update steps. +- Prefer a clear local scalar or named coefficient over repeated dense expressions. +- Avoid abstractions that hide stencil shape, update ordering, or boundary handling. +- Add short comments for non-obvious performance constraints such as loop order, contiguous assumptions, or synchronization placement. +- Do not add comments that simply say code is faster. + +## Measurement And Verification + +Use the narrowest practical verification first: + +```bash +cmake --build build -j +``` + +Then run targeted tests for the touched area when available. For performance-focused changes, also compare a representative case runtime when practical, ideally with the same input, build type, MPI rank count, thread count, and output cadence. + +When reporting results, distinguish between: + +- Measured speedup or reduced runtime. +- Structural improvement likely to reduce overhead, such as removing allocation from a loop. +- Readability-neutral cleanup that only prepares for future optimization. + +Do not claim a performance improvement as measured unless it was actually measured. + +If measurement is not practical, report the change as a structural improvement, for example "moves allocation out of the time-step loop" or "preserves loop order while reducing repeated component lookups." + +## Review Checklist + +Before finishing performance-sensitive Fortran work, check: + +- Numerical behaviour is intended to remain unchanged, or the intended change is explicit. +- No unnecessary allocation, deallocation, I/O, string work, or large temporaries were added to hot paths. +- Array access and loop order are deliberate and preserve existing dependencies. +- MPI/OpenMP synchronization and data-sharing remain correct. +- Optional preprocessor branches remain valid. +- Public APIs were not expanded for performance helpers that only have one caller. +- Any non-obvious performance constraint is documented briefly. +- Verification or measurement is reported accurately. + +## Final Response Expectations + +When reporting performance-related changes, include: + +- The files and hot paths touched. +- The specific overhead avoided or performance property preserved. +- Whether behaviour was intended to change. +- The build, tests, or measurements run. +- Any performance claims that are unmeasured and should be treated as expectations rather than results. diff --git a/.opencode/skills/fortran-refactor-cleanliness/SKILL.md b/.opencode/skills/fortran-refactor-cleanliness/SKILL.md new file mode 100644 index 00000000..8b69cc48 --- /dev/null +++ b/.opencode/skills/fortran-refactor-cleanliness/SKILL.md @@ -0,0 +1,179 @@ +--- +name: fortran-refactor-cleanliness +description: Use whenever cleaning up, simplifying, renaming, reorganizing, or behaviour-preservingly refactoring Fortran FDTD code in semba-fdtd. Trigger for .F90 modules, solver loops, parser code, output code, CMake Fortran target changes, tests, dead-code cleanup, import cleanup, naming cleanup, and readability-only edits. Use this even when the user says "just tidy this" or "make it easier to read"; combine with fortran-performance-awareness for hot loops and with fortran-module-readability for module API or boundary changes. +--- + +# Fortran Refactor Cleanliness + +Use this skill when refactoring this repository's Fortran code, especially when the intended outcome is easier reading or maintenance rather than new behaviour. +The goal is cleaner, more readable code without changing numerical behaviour unless the user explicitly asks for a behavioural fix. +For performance-sensitive refactors in hot loops, MPI/OpenMP paths, allocation-heavy code, or numerical kernels, also use `fortran-performance-awareness`. + +## First Pass + +Before editing, classify the refactor so the diff stays reviewable: + +1. Name the single readability problem being fixed, such as duplicated local logic, unclear branching, misleading names, broad imports, or tangled setup steps. +2. Check whether the code is a hot numerical path, public module API, parser/output compatibility path, or optional-feature branch. +3. Search for callers before renaming procedures, types, components, public exports, files, or CMake source entries. +4. Decide the smallest behaviour-preserving change that solves the readability problem. +5. Keep feature work, bug fixes, formatting churn, and broad style normalization out of the same patch unless the user explicitly asked for them. + +## Project Context + +This project is `semba-fdtd`, a Finite-Difference Time-Domain electromagnetic solver written primarily in Fortran. +It contains legacy numerical kernels, newer typed modules, conditional compilation, MPI/OpenMP paths, optional HDF5 output, SMBJSON parsing, and MTLN/SPICE coupling. + +Key areas: + +- `src_main_pub/`: core solver, preprocessing, postprocessing, time stepping, geometry, main types. +- `src_json_parser/`: `.fdtd.json` parser and conversion helpers. +- `src_output/`: probe, VTK, XDMF, HDF5, and output utility code. +- `src_mtln/`: multiconductor transmission-line solver and ngspice integration. +- `src_conformal/`: conformal mapping and staircase reduction. +- `src_wires_pub/`: wire and thin-wire models. +- `test/`: Fortran/C++ unit tests and Python integration tests. + +The build is layered through CMake static libraries. +Respect the existing dependency direction and avoid introducing upward dependencies between lower-level libraries and higher-level solver/output code. + +## Refactoring Priorities + +Prefer small, behaviour-preserving changes: + +- Improve names when the domain meaning is clear from nearby code. +- Reduce duplicated local logic when a helper makes the numerical intent easier to read. +- Clarify long conditionals, `select case` blocks, and repeated string/coordinate formatting. +- Add `only:` to `use` statements when touching imports and when it does not create excessive churn. +- Tighten visibility with `private` and explicit `public` lists when working in already-structured modules. +- Add or improve `intent` declarations where missing and obvious. +- Replace magic literals only when their meaning is certain and the new name is local or already established. +- Remove dead local variables only after checking preprocessor branches and nearby compile options. +- Improve CMake source organization only when it directly follows from a file move, new module, or target-boundary cleanup. + +Avoid broad rewrites: + +- Do not redesign modules, data ownership, or library boundaries as part of a readability task. +- Do not change physics formulas, update ordering, time-step sequencing, boundary semantics, MPI exchange order, or output formats unless explicitly requested. +- Do not rename domain terms with historical or input-file significance without asking. +- Do not split every long routine mechanically; extract only natural concepts with clear names. +- Do not introduce compatibility shims or abstraction layers unless there is a concrete caller or persisted-data need. +- Do not normalize formatting across an entire file when the functional refactor touches only a small region; formatting-only churn hides semantic review. + +## Fortran-Specific Guardrails + +Treat numerical and memory semantics as part of behaviour: + +- Preserve `implicit none`. +- Preserve `kind` choices such as `RKIND`, `RKIND_tiempo`, `SINGLE`, and existing integer kinds unless the task is specifically about precision or portability. +- Preserve array rank, shape, lower bounds, allocation ownership, pointer association, and `contiguous` assumptions. +- Be careful when changing `pointer` to `allocatable` or vice versa; this can alter aliasing and ownership. +- Do not reorder loops over field arrays unless there is a measured performance or correctness reason. +- Preserve OpenMP and MPI assumptions around shared data, halo exchanges, reductions, and output ordering. +- Keep preprocessor branches such as `CompileWithMPI`, `CompileWithMTLN`, `CompileWithSMBJSON`, `CompileWithNewOutputModule`, and `CompileWithReal8` valid even if the local build uses only one configuration. +- Preserve file formats, exact labels, and serialized names used by JSON, VTK, XDMF, HDF5, probe `.dat` files, or legacy `.fdtd` inputs. + +## Workflow + +Before editing: + +1. Read the target module and enough neighbouring code to understand ownership and callers. +2. Search for procedure/type/module references before changing names, signatures, public exports, or file-level interfaces. +3. Identify relevant compile flags around the code, especially `#ifdef` blocks. +4. Choose one coherent refactor with a small reviewable diff. + +During editing: + +1. Keep the public interface stable unless changing it is the point of the task. +2. Prefer local changes over new global helpers. +3. Preserve surrounding formatting style unless the formatting itself harms readability. +4. Add comments only to explain non-obvious domain constraints, numerical assumptions, or compiler/preprocessor constraints. +5. Do not combine cleanup with unrelated feature work. +6. Preserve exact strings and ordering in user-visible output unless the task is explicitly about changing output. + +After editing: + +1. Build or at least compile the affected target when feasible. +2. Run the most targeted tests available. +3. State that no behavioural change is intended, or describe the exact intended behaviour change if there is one. +4. Mention any build/test coverage gaps caused by unavailable dependencies or disabled options. +5. Re-read the diff from a reviewer perspective and remove incidental churn that does not support the stated refactor. + +## Readability Checklist + +Use this checklist before finishing a refactor: + +- The code reads in the same order as the operation it performs. +- Names reflect domain meaning, not just type or storage. +- Conditionals have clear cases and meaningful default/error handling. +- Public module surface is no larger than necessary. +- Imports are understandable and preferably constrained with `only:` where practical. +- Local variables are declared near the routine where they are used and are not misleadingly reused. +- Comments explain why the code exists or why a surprising choice is necessary. +- The diff is small enough to review for numerical equivalence. +- The final response names the intended behaviour-preserving nature of the change. + +## Testing And Verification + +Useful build commands: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build -j +``` + +Unit tests: + +```bash +./build/bin/fdtd_tests +``` + +Python integration tests: + +```bash +pytest test/ --durations=20 +``` + +Marker-specific tests: + +```bash +pytest test/ -m mtln +pytest test/ -m hdf +pytest test/ -m mpi +``` + +Run the narrowest relevant command first. +For parser changes, prefer `test/smbjson` tests. +For output changes, prefer `test/output` and VTK/XDMF tests. +For MTLN changes, prefer `test/mtln` tests. +For solver loop changes, build and run the broad unit suite if feasible. + +## Good Refactor Examples + +Good changes: + +- Replace repeated coordinate string assembly in one module with a local helper that preserves exact output text. +- Extract a named local predicate from a long conditional when it is used multiple times in the same routine. +- Convert an unclear `if/elseif` chain to `select case` without changing defaults. +- Add `intent(in)`, `intent(out)`, or `intent(inout)` to arguments where usage is unambiguous. +- Split a long parser routine into parse, validate, and convert steps when those steps already exist conceptually. + +Risky changes that need explicit justification: + +- Changing loop order over `Ex`, `Ey`, `Ez`, `Hx`, `Hy`, or `Hz` arrays. +- Replacing pointer arrays with allocatables. +- Changing `real` or `integer` kinds. +- Renaming JSON labels, probe labels, field prefixes, or legacy NFDE terms. +- Removing preprocessor branches that are not active in the current build. +- Introducing generic abstractions across solver, parser, output, and MTLN code without a concrete repeated pattern. + +## Final Response Expectations + +When reporting a completed refactor, include: + +- The files changed. +- The readability improvement made. +- Whether behaviour was intended to change. +- The build or tests run, including failures or skipped verification. + +Keep the response concise and factual. From b6dc8c2cf9989d8ad9e38b7dfe9d33b7fae90215 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Mon, 13 Jul 2026 07:23:02 +0000 Subject: [PATCH 05/10] FDTD | ENV | #409 | Paraview included on dev container tool --- Dockerfile | 1 + doc/docker.md | 14 ++++++++++++-- docker-compose.yml | 4 ++++ scripts/devcontainer-post-start.sh | 8 ++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 66316a46..a448b53b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,6 +83,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ openssh-client \ nodejs \ npm \ + paraview \ sudo \ && rm -rf /var/lib/apt/lists/* diff --git a/doc/docker.md b/doc/docker.md index 03baae93..03c0d44e 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -14,8 +14,8 @@ git submodule update --init --recursive | File | Description | |---|---| -| `Dockerfile` | Multi-stage build: `builder` (compilation + tests) and `runtime` (binary only) | -| `docker-compose.yml` | Services `solver` (run simulations) and `test` (build and test) | +| `Dockerfile` | Multi-stage build: `builder` (compilation + tests), `dev` (development container), and `runtime` (binary only) | +| `docker-compose.yml` | Services `solver` (run simulations), `dev` (development container), and `test` (build and test) | | `.dockerignore` | Excludes unnecessary files from the build context | --- @@ -29,6 +29,16 @@ docker compose build solver # runtime image for simulations `build` only constructs and saves the image to disk — it does not start any container. You only need to re-run it when the code changes. +### Development container tools + +The `dev` image target includes developer-only tools used from the dev container, including ParaView for inspecting generated VTK/XDMF/HDF5 outputs. ParaView is installed in the `dev-tools` stage only, so runtime and test images do not carry the extra GUI package dependencies. + +If `paraview` or `pvpython` is missing inside the dev container, rebuild the dev image: + +```bash +docker compose build dev +``` + ### Build arguments The build mode and optional features can be configured via `--build-arg`: diff --git a/docker-compose.yml b/docker-compose.yml index b456a9a0..a2db575f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,10 @@ services: - type: bind source: ${HOME}/.config/opencode target: /home/developer/.config/opencode + - type: bind + source: ${HOME}/.agents + target: /home/developer/.agents + read_only: true - type: bind source: ${HOME}/.local/share/opencode target: /home/developer/.local/share/opencode diff --git a/scripts/devcontainer-post-start.sh b/scripts/devcontainer-post-start.sh index db494d5d..932985b5 100755 --- a/scripts/devcontainer-post-start.sh +++ b/scripts/devcontainer-post-start.sh @@ -27,6 +27,10 @@ if ! command -v opencode >/dev/null 2>&1; then warn "opencode is not available in PATH" fi +if ! command -v paraview >/dev/null 2>&1 && ! command -v pvpython >/dev/null 2>&1; then + warn "ParaView is not available in PATH; rebuild the dev container image" +fi + if command -v git >/dev/null 2>&1; then git config --global --add safe.directory /home/developer/workspaces/fdtd >/dev/null 2>&1 || \ warn "could not add workspace to git safe.directory" @@ -36,6 +40,10 @@ if [ ! -d "$expected_home/.config/opencode" ]; then warn "$expected_home/.config/opencode is not mounted" fi +if [ ! -d "$expected_home/.agents/skills" ]; then + warn "$expected_home/.agents/skills is not mounted" +fi + if [ ! -d "$expected_home/.local/share/opencode" ]; then warn "$expected_home/.local/share/opencode is not mounted" fi From 2070bacf256f301461e3373a8e628dee96acd2d8 Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Tue, 14 Jul 2026 12:13:21 +0000 Subject: [PATCH 06/10] FDTD | Tests | Fix probe output file units --- src_output/bulkProbeOutput.F90 | 2 +- src_output/frequencySliceProbeOutput.F90 | 2 +- src_output/movieProbeOutput.F90 | 2 +- src_output/pointProbeOutput.F90 | 5 ++--- test/output/test_output.F90 | 8 ++++---- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src_output/bulkProbeOutput.F90 b/src_output/bulkProbeOutput.F90 index d97626c8..9bede40b 100644 --- a/src_output/bulkProbeOutput.F90 +++ b/src_output/bulkProbeOutput.F90 @@ -163,7 +163,7 @@ subroutine flush_bulk_probe_output(this) return end if - open (unit=unit, file=this%filePathTime, status="old", action="write", position="append") + open (newunit=unit, file=this%filePathTime, status="old", action="write", position="append") do i = 1, this%nTime write (unit, fmt) this%timeStep(i), this%valueForTime(i) diff --git a/src_output/frequencySliceProbeOutput.F90 b/src_output/frequencySliceProbeOutput.F90 index 128c7598..784b2d7e 100644 --- a/src_output/frequencySliceProbeOutput.F90 +++ b/src_output/frequencySliceProbeOutput.F90 @@ -93,7 +93,7 @@ subroutine write_bin_file(this) type(frequency_slice_probe_output_t), intent(inout) :: this integer :: i, f, unit !We rewrite the binary as simulation continues - open (unit=unit, file=add_extension(this%filesPath, binaryExtension), & + open (newunit=unit, file=add_extension(this%filesPath, binaryExtension), & status='old', form='unformatted', action='write', access='stream') do f = 1, this%nFreq do i = 1, this%nPoints diff --git a/src_output/movieProbeOutput.F90 b/src_output/movieProbeOutput.F90 index b937eac2..6ffdee26 100644 --- a/src_output/movieProbeOutput.F90 +++ b/src_output/movieProbeOutput.F90 @@ -208,7 +208,7 @@ subroutine write_bin_file(this) type(movie_probe_output_t), intent(inout) :: this integer :: i, t, unit - open (unit=unit, file=add_extension(this%filesPath, binaryExtension), & + open (newunit=unit, file=add_extension(this%filesPath, binaryExtension), & status='old', form='unformatted', position='append', access='stream') do t = 1, this%nTime do i = 1, this%nPoints diff --git a/src_output/pointProbeOutput.F90 b/src_output/pointProbeOutput.F90 index 5dd4e815..5c3212c5 100644 --- a/src_output/pointProbeOutput.F90 +++ b/src_output/pointProbeOutput.F90 @@ -118,8 +118,7 @@ subroutine flush_time_domain(this) print *, "No data to write." return end if - - open (unit=unit, file=this%filePathTime, status="old", action="write", position="append") + open (newunit=unit, file=this%filePathTime, status="old", action="write", position="append") do i = 1, this%nTime write (unit, '(F12.6,1X,F12.6)') this%timeStep(i), this%valueForTime(i) @@ -142,7 +141,7 @@ subroutine flush_frequency_domain(this) print *, "No data to write." return end if - open (unit=unit, file=this%filePathFreq, status="replace", action="write") + open (newunit=unit, file=this%filePathFreq, status="replace", action="write") do i = 1, this%nFreq write (unit, '(F12.6,1X,F12.6,1X,F12.6)') this%frequencySlice(i), real(this%valueForFreq(i)), aimag(this%valueForFreq(i)) diff --git a/test/output/test_output.F90 b/test/output/test_output.F90 index 00ff2f87..a679a9b3 100644 --- a/test/output/test_output.F90 +++ b/test/output/test_output.F90 @@ -789,12 +789,12 @@ integer function test_flush_movie_probe() bind(c) result(err) !movieElectricXObservable outputs(2)%movieProbe%nTime = 1 outputs(2)%movieProbe%timeStep(1) = 0.5_RKIND_tiempo - outputs(2)%movieProbe%xValueForTime(1, :) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] + outputs(2)%movieProbe%xValueForTime(1, :4) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] !movieMagneticYObservable outputs(3)%movieProbe%nTime = 1 outputs(3)%movieProbe%timeStep(1) = 0.5_RKIND_tiempo - outputs(3)%movieProbe%yValueForTime(1, :) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] + outputs(3)%movieProbe%yValueForTime(1, :4) = [0.1_RKIND, 0.2_RKIND, 0.3_RKIND, 0.4_RKIND] !--- Dummy second update --- !movieCurrentObservable @@ -807,12 +807,12 @@ integer function test_flush_movie_probe() bind(c) result(err) !movieElectricXObservable outputs(2)%movieProbe%nTime = 2 outputs(2)%movieProbe%timeStep(2) = 0.75_RKIND_tiempo - outputs(2)%movieProbe%xValueForTime(2, :) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] + outputs(2)%movieProbe%xValueForTime(2, :4) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] !movieMagneticYObservable outputs(3)%movieProbe%nTime = 2 outputs(3)%movieProbe%timeStep(2) = 0.75_RKIND_tiempo - outputs(3)%movieProbe%yValueForTime(2, :) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] + outputs(3)%movieProbe%yValueForTime(2, :4) = [1.1_RKIND, 1.2_RKIND, 1.3_RKIND, 1.4_RKIND] call flush_outputs(dummysgg%tiempo, 1_SINGLE, dummyControl, fields, dummyBound, .false.) From 8a353dcdd4a15543d46931cc86d4bde977d94efa Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Tue, 14 Jul 2026 13:03:22 +0000 Subject: [PATCH 07/10] FDTD | ENV | #409 | simplify docker file --- Dockerfile | 137 ++++++++----------------- doc/docker.md | 243 ++++++++++----------------------------------- docker-compose.yml | 113 ++------------------- 3 files changed, 99 insertions(+), 394 deletions(-) diff --git a/Dockerfile b/Dockerfile index a448b53b..7cdb1c06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 -# ─── Base ───────────────────────────────────────────────────────────────────── -FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS base +# Keep shared build dependencies first so both environments reuse this layer. +FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS quality-base ENV DEBIAN_FRONTEND=noninteractive @@ -19,8 +19,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ python3 \ python3-pip \ python3-venv \ - gdb \ - gdbserver \ + paraview \ && locale-gen en_US.UTF-8 \ && rm -rf /var/lib/apt/lists/* @@ -28,50 +27,37 @@ ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 -# ─── Stage 1: Builder ───────────────────────────────────────────────────────── -FROM base AS builder - -WORKDIR /src -COPY . . - -# Build (MPI off by default; override at build time with --build-arg ENABLE_MPI=ON) -ARG ENABLE_MPI=OFF -ARG ENABLE_MTLN=ON -ARG BUILD_TYPE=Release -ARG ENABLE_TEST=OFF - -RUN cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DSEMBA_FDTD_ENABLE_MPI=${ENABLE_MPI} \ - -DSEMBA_FDTD_ENABLE_HDF=ON \ - -DSEMBA_FDTD_ENABLE_MTLN=${ENABLE_MTLN} \ - -DSEMBA_FDTD_ENABLE_SMBJSON=ON \ - -DSEMBA_FDTD_ENABLE_TEST=${ENABLE_TEST} \ - && cmake --build build -j$(nproc) \ - && cp build/bin/semba-fdtd /tmp/semba-fdtd \ - && if [ "${BUILD_TYPE}" = "Release" ]; then strip /tmp/semba-fdtd; fi - -# ─── Stage 1b: Test Builder ─────────────────────────────────────────────────── -FROM builder AS test-builder - -RUN cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ - -DSEMBA_FDTD_ENABLE_MPI=${ENABLE_MPI} \ - -DSEMBA_FDTD_ENABLE_HDF=ON \ - -DSEMBA_FDTD_ENABLE_MTLN=${ENABLE_MTLN} \ - -DSEMBA_FDTD_ENABLE_SMBJSON=ON \ - -DSEMBA_FDTD_ENABLE_TEST=ON \ - && cmake --build build -j$(nproc) - -# Install Python test/wrapper dependencies only for images that run tests. -RUN python3 -m venv /opt/fdtd-venv \ - && /opt/fdtd-venv/bin/python -m pip install --upgrade pip \ - && /opt/fdtd-venv/bin/python -m pip install --no-cache-dir -r requirements.txt - -ENV PATH=/opt/fdtd-venv/bin:${PATH} - -# ─── Stage 2: Development Tools ──────────────────────────────────────────────── -FROM base AS dev-tools +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -eux; \ + existing_group="$(getent group ${USER_GID} | cut -d: -f1)"; \ + if [ -n "${existing_group}" ] && [ "${existing_group}" != "${USERNAME}" ] && ! getent group ${USERNAME} >/dev/null; then \ + groupmod -n ${USERNAME} ${existing_group}; \ + elif [ -z "${existing_group}" ]; then \ + groupadd --gid ${USER_GID} ${USERNAME}; \ + fi; \ + existing_user="$(getent passwd ${USER_UID} | cut -d: -f1)"; \ + if [ -n "${existing_user}" ] && [ "${existing_user}" != "${USERNAME}" ]; then \ + usermod -l ${USERNAME} -d /home/${USERNAME} -m ${existing_user}; \ + elif [ -z "${existing_user}" ]; then \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME}; \ + fi; \ + mkdir -p /home/${USERNAME}/workspaces/fdtd; \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/workspaces + +# Minimal environment for compiling and running the project. +FROM quality-base AS quality + +ENV HOME=/home/${USERNAME} + +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd + +# Full development environment. This target deliberately follows quality so its +# build reuses every compiler and ParaView layer. +FROM quality AS dev USER root @@ -83,7 +69,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ openssh-client \ nodejs \ npm \ - paraview \ + gdb \ + gdbserver \ sudo \ && rm -rf /var/lib/apt/lists/* @@ -93,56 +80,16 @@ RUN --mount=type=cache,target=/root/.npm \ RUN --mount=type=cache,target=/root/.cache/pip \ python3 -m pip install --break-system-packages fortls fprettify -# ─── Stage 2b: Development User ──────────────────────────────────────────────── -FROM dev-tools AS dev - -ARG USERNAME=developer -ARG USER_UID=1000 -ARG USER_GID=1000 - -ENV USERNAME=${USERNAME} \ - USER_UID=${USER_UID} \ - USER_GID=${USER_GID} - -RUN userdel --remove ubuntu 2>/dev/null || true \ - && groupdel ubuntu 2>/dev/null || true \ - && groupadd --gid ${USER_GID} ${USERNAME} \ - && useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME} \ - && mkdir -p /home/${USERNAME}/.config \ - && mkdir -p /home/${USERNAME}/.ssh \ - && mkdir -p /home/${USERNAME}/.local/share/opencode \ +RUN mkdir -p /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local/share/opencode \ && chmod 700 /home/${USERNAME}/.ssh \ - && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.config \ - && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.ssh \ - && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.local \ + && chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/.config \ + /home/${USERNAME}/.ssh \ + /home/${USERNAME}/.local \ && usermod -aG sudo ${USERNAME} \ && echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USERNAME} \ && chmod 0440 /etc/sudoers.d/${USERNAME} USER ${USERNAME} WORKDIR /home/${USERNAME}/workspaces/fdtd - -# ─── Stage 3: Runtime ───────────────────────────────────────────────────────── -# Minimal image with only the shared libraries the binary needs at runtime. -# HDF5, LAPACK, BLAS, and ngspice are all statically linked on Linux. -# Keep OpenMPI runtime libraries available for MPI-enabled image builds. -FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS runtime - -ENV DEBIAN_FRONTEND=noninteractive - -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update && apt-get install -y \ - libgfortran5 \ - libgomp1 \ - libcurl4t64 \ - zlib1g \ - libaec0 \ - libsz2 \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /tmp/semba-fdtd /usr/local/bin/semba-fdtd - -WORKDIR /work - -ENTRYPOINT ["semba-fdtd", "-i"] diff --git a/doc/docker.md b/doc/docker.md index 03c0d44e..82eebeb0 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -1,255 +1,112 @@ # Docker -This document explains how to use Docker to build, test, and run semba-fdtd without installing any dependencies on your machine. +Docker provides two environments for working with semba-fdtd. +Both mount the repository at `/home/developer/workspaces/fdtd`, so build artefacts and simulation output are written to the host workspace. -## Prerequisite: initialize submodules +## Prerequisite -Submodules must be initialized on the host before building the image, as the build depends on them: +Initialise submodules before building an image: ```bash git submodule update --init --recursive ``` -## Included files +## Environments -| File | Description | -|---|---| -| `Dockerfile` | Multi-stage build: `builder` (compilation + tests), `dev` (development container), and `runtime` (binary only) | -| `docker-compose.yml` | Services `solver` (run simulations), `dev` (development container), and `test` (build and test) | -| `.dockerignore` | Excludes unnecessary files from the build context | - ---- - -## Building the images - -```bash -docker compose build test # image for tests -docker compose build solver # runtime image for simulations -``` - -`build` only constructs and saves the image to disk — it does not start any container. You only need to re-run it when the code changes. - -### Development container tools - -The `dev` image target includes developer-only tools used from the dev container, including ParaView for inspecting generated VTK/XDMF/HDF5 outputs. ParaView is installed in the `dev-tools` stage only, so runtime and test images do not carry the extra GUI package dependencies. - -If `paraview` or `pvpython` is missing inside the dev container, rebuild the dev image: - -```bash -docker compose build dev -``` - -### Build arguments - -The build mode and optional features can be configured via `--build-arg`: - -| Argument | Values | Default | +| Service | Purpose | Included tools | |---|---|---| -| `BUILD_TYPE` | `Release`, `Debug` | `Release` | -| `ENABLE_MPI` | `ON`, `OFF` | `OFF` | -| `ENABLE_MTLN` | `ON`, `OFF` | `ON` | +| `quality` | Compile, test, run examples, and inspect generated output | GNU C/C++/Fortran compilers, CMake, Ninja, MPI, HDF5, Python, and ParaView | +| `dev` | Interactive development and the Dev Container | Everything in `quality`, plus Git, GitHub CLI, SSH client, Node/npm, OpenCode, Fortran tools, and debuggers | -```bash -# Debug build -docker compose build --build-arg BUILD_TYPE=Debug test - -# Combining arguments -docker compose build \ - --build-arg BUILD_TYPE=Debug \ - --build-arg ENABLE_MPI=ON \ - --build-arg ENABLE_MTLN=OFF \ - test -``` +`quality` intentionally excludes developer-only tools. +Both environments include ParaView. -**Release** (`-Ofast`): optimized for speed, no debug information. -**Debug** (`-g -O0 -fcheck=all -fbacktrace`): no optimization, with runtime checks and backtraces on error — useful for diagnosing crashes. +## Build Images -### Base image digest - -The `Dockerfile` pins the base image using a SHA256 digest instead of just the tag: - -```dockerfile -FROM ubuntu:22.04@sha256:eb29ed27... AS builder -``` - -`ubuntu:22.04` is a mutable tag — Canonical can update it at any time. The digest identifies an exact, immutable image, so builds are fully reproducible regardless of when or where they run. - -The downside is that OS security patches are not picked up automatically. To update the digest: +Build `quality` first: ```bash -docker pull ubuntu:22.04 -docker inspect ubuntu:22.04 --format='{{index .RepoDigests 0}}' +docker compose build quality ``` -Then replace both occurrences of the digest in the `Dockerfile` (builder and runtime stages). - ---- - -## Running the tests +Build `dev` when its additional developer tools are required: ```bash -docker compose run --rm test +docker compose build dev ``` -This runs in sequence: -1. `build/bin/fdtd_tests` — unit tests (GoogleTest) -2. `python3 -m pytest test/ --durations=20` — Python integration tests - -To run only part of the test suite: +The `dev` target is built from `quality`. +Docker therefore reuses the compiler and ParaView layers when building `dev`. -```bash -# Unit tests only -docker compose run --rm test build/bin/fdtd_tests +The source tree is never copied into either image. +Editing source files, running examples, and creating build directories do not invalidate image layers or require an image rebuild. -# pytest with a specific marker -docker compose run --rm test python3 -m pytest test/ -m mtln -docker compose run --rm test python3 -m pytest test/ -m hdf -``` +## Compile With Quality -To open an interactive shell inside the container: +Run CMake presets from the mounted workspace: ```bash -docker compose run --rm --entrypoint bash test +docker compose run --rm quality cmake --preset rls +docker compose run --rm quality cmake --build --preset rls ``` ---- - -## Debugging a simulation - -This uses `gdbserver` inside the container and connects VSCode to it via the C/C++ extension. - -### Prerequisites - -- VSCode extension: **C/C++** (`ms-vscode.cpptools`) -- `gdb` installed on the host: - ```bash - sudo apt install gdb - ``` - -### Step 1 — Build the debug image +For an MPI build: ```bash -docker compose build debug +docker compose run --rm quality cmake --preset rls-mpi +docker compose run --rm quality cmake --build --preset rls-mpi ``` -### Step 2 — Extract the binary for local symbol loading +Build directories such as `build-rls/` and `build-rls-mpi/` remain in the repository after the container exits. -VSCode's GDB client needs a local copy of the binary to load debug symbols. Extract it from the image once after each build: +## Run Binaries And Examples -```bash -docker create --name tmp-debug fdtd-debug -docker cp tmp-debug:/src/build/bin/semba-fdtd ./build/bin/semba-fdtd -docker rm tmp-debug -``` - -### Step 3 — Start gdbserver - -Place your input files in `simulations/` and run: +Use the binary produced in the mounted build directory: ```bash -docker compose run --rm -p 2345:2345 debug case.fdtd.json +docker compose run --rm quality build-rls/bin/semba-fdtd -i path/to/case.fdtd.json ``` -The container starts and waits, printing something like: +Any output generated by the solver is available immediately in the corresponding host directory. -``` -Process /src/build/bin/semba-fdtd created; pid = 7 -Listening on port 2345 -``` - -### Step 4 — Connect from VSCode - -`.vscode/launch.json` is tracked in the repository and already contains the **Docker: attach gdbserver** configuration. - -Open the **Run and Debug** panel (`Ctrl+Shift+D`), select **Docker: attach gdbserver**, and press `F5`. VSCode connects, the simulation starts, and breakpoints work normally. - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Docker: attach gdbserver", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/build/bin/semba-fdtd", - "miDebuggerServerAddress": "localhost:2345", - "miDebuggerPath": "gdb", - "MIMode": "gdb", - "cwd": "${workspaceFolder}/simulations", - "sourceFileMap": { - "/src": "${workspaceFolder}" - }, - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ] - } - ] -} -``` - -> The `sourceFileMap` maps `/src` (container path baked into debug info) to `${workspaceFolder}` on the host, so source files display correctly. - ---- - -## Running a simulation - -Place your input files in `simulations/` (created at the repo root) and run: +Open an interactive quality shell when repeatedly compiling or running examples: ```bash -docker compose run --rm solver case.fdtd.json +docker compose run --rm quality ``` -The `simulations/` directory is mounted at `/work` inside the container, which is the solver's working directory. - ---- - -## Managing images and containers - -### Where are images stored? - -Docker manages them internally (under `/var/lib/docker/` on Linux), not in a project folder. To inspect them: +ParaView is available in both environments: ```bash -docker images # list all saved images -docker image prune # remove unused images +docker compose run --rm quality paraview +docker compose run --rm dev paraview ``` -### Viewing active containers - -```bash -docker ps # currently running containers -docker ps -a # running + stopped containers -``` +## Run Tests -### Stopping and removing containers +Configure a build with tests, then run the required test commands: ```bash -docker stop # stops the container (does not remove it) -docker rm # removes it -docker rm -f # stop and remove in one step -docker container prune # remove all stopped containers +docker compose run --rm quality cmake --preset rls +docker compose run --rm quality cmake --build --preset rls +docker compose run --rm quality build-rls/bin/fdtd_tests ``` -The full ID is not required — the first 3–4 characters are enough: +Python integration tests require the project dependencies in a virtual environment: ```bash -docker stop a1b2 +docker compose run --rm quality python3 -m venv .venv +docker compose run --rm quality .venv/bin/python -m pip install -r requirements.txt +docker compose run --rm quality .venv/bin/python -m pytest test/ --durations=20 ``` -> With `--rm` (used in all `docker compose run` commands) the container is removed automatically when it finishes, so no manual cleanup is needed. - -### Does closing the terminal stop the container? +## Development Environment -- **Without `-d`** (normal mode, what we use): yes, closing the terminal stops the container because it is attached to it. -- **With `-d`** (detached mode): no, it keeps running in the background even after the terminal is closed. +Use `dev` for a full interactive shell: ```bash -# Run in the background -docker compose run -d --rm test +docker compose run --rm dev ``` -For tests it is better to omit `-d` so you can see the output in real time. +The Dev Container configuration also uses the `dev` service. +It mounts the host Git, SSH, GitHub CLI, and OpenCode configuration required by the development workflow. diff --git a/docker-compose.yml b/docker-compose.yml index a2db575f..365c0d27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,19 @@ services: - - # Run a simulation: docker compose run --rm solver path/to/case.fdtd.json - solver: + quality: build: context: . - target: runtime + target: quality volumes: - - ./simulations:/work - working_dir: /work + - type: bind + source: . + target: /home/developer/workspaces/fdtd + working_dir: /home/developer/workspaces/fdtd + tty: true dev: - image: fdtd-dev:latest build: context: . target: dev - environment: - HOME: /home/developer - XDG_CONFIG_HOME: /home/developer/.config - XDG_DATA_HOME: /home/developer/.local/share volumes: - type: bind source: . @@ -42,100 +38,5 @@ services: - type: bind source: ${HOME}/.local/share/opencode target: /home/developer/.local/share/opencode - - type: bind - source: ./build-dev - target: /home/developer/workspaces/fdtd/build-dev - - type: bind - source: ./build-dev-mpi - target: /home/developer/workspaces/fdtd/build-dev-mpi working_dir: /home/developer/workspaces/fdtd tty: true - # Debug a simulation with gdbserver (connect from VSCode with launch.json) - # Usage: docker compose run --rm -p 2345:2345 debug case.fdtd.json - debug: - build: - context: . - target: builder - args: - BUILD_TYPE: Debug - ENABLE_MTLN: "ON" - volumes: - - ./simulations:/work - ports: - - "2345:2345" - working_dir: /work - entrypoint: ["gdbserver", ":2345", "/src/build/bin/semba-fdtd", "-i"] - - # Build the project and run non-MPI tests - test: - build: - context: . - target: test-builder - args: - BUILD_TYPE: Release - ENABLE_MPI: "OFF" - ENABLE_MTLN: "ON" - environment: - SEMBA_FDTD_ENABLE_MPI: "OFF" - SEMBA_FDTD_ENABLE_MTLN: "ON" - SEMBA_FDTD_ENABLE_HDF: "ON" - SEMBA_EXE: /src/build/bin/semba-fdtd - working_dir: /src - command: > - sh -c "build/bin/fdtd_tests && python3 -m pytest test/ -m 'not mpi' --durations=20" - - # Build with MPI enabled and run MPI integration tests - test-mpi: - build: - context: . - target: test-builder - args: - BUILD_TYPE: Release - ENABLE_MPI: "ON" - ENABLE_MTLN: "ON" - environment: - SEMBA_FDTD_ENABLE_MPI: "ON" - SEMBA_FDTD_ENABLE_MTLN: "ON" - SEMBA_FDTD_ENABLE_HDF: "ON" - SEMBA_EXE: /src/build/bin/semba-fdtd - OMPI_ALLOW_RUN_AS_ROOT: "1" - OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: "1" - working_dir: /src - command: > - sh -c "build/bin/fdtd_tests && python3 -m pytest test/ -m mpi --durations=20" - - # Interactive shell in the non-MPI test image - shell: - build: - context: . - target: test-builder - args: - BUILD_TYPE: Debug - ENABLE_MPI: "OFF" - ENABLE_MTLN: "ON" - environment: - SEMBA_FDTD_ENABLE_MPI: "OFF" - SEMBA_FDTD_ENABLE_MTLN: "ON" - SEMBA_FDTD_ENABLE_HDF: "ON" - SEMBA_EXE: /src/build/bin/semba-fdtd - working_dir: /src - entrypoint: ["bash"] - - # Interactive shell in the MPI-enabled test image - shell-mpi: - build: - context: . - target: test-builder - args: - BUILD_TYPE: Debug - ENABLE_MPI: "ON" - ENABLE_MTLN: "ON" - environment: - SEMBA_FDTD_ENABLE_MPI: "ON" - SEMBA_FDTD_ENABLE_MTLN: "ON" - SEMBA_FDTD_ENABLE_HDF: "ON" - SEMBA_EXE: /src/build/bin/semba-fdtd - OMPI_ALLOW_RUN_AS_ROOT: "1" - OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: "1" - working_dir: /src - entrypoint: ["bash"] From f194b5eded9062af196abcc4462489ebe223803b Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Mon, 27 Jul 2026 12:53:40 +0000 Subject: [PATCH 08/10] FDTD | Fix | Import output allocation helpers --- .github/workflows/windows.yml | 5 ++++- src_mtln/dispersive.F90 | 3 +-- src_mtln/mtl.F90 | 2 +- src_mtln/mtl_bundle.F90 | 4 ++-- src_mtln/utils.F90 | 4 ++-- src_output/bulkProbeOutput.F90 | 1 + src_output/frequencySliceProbeOutput.F90 | 1 + src_output/movieProbeOutput.F90 | 2 ++ src_output/pointProbeOutput.F90 | 1 + src_output/volumicProbeUtils.F90 | 1 + src_output/wireProbeOutput.F90 | 1 + src_utils/directoryUtils.F90 | 4 ++-- test/mtln/test_math.F90 | 4 ++-- test/observation/test_observation_init.F90 | 9 +++++---- test/observation/test_observation_update.F90 | 9 +++++---- test/utils/fdetypes_tools.F90 | 1 + 16 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index dee927d4..dc4b4f37 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -25,6 +25,7 @@ jobs: mtln: ["ON", "OFF"] hdf: ["ON"] double-precision: ["ON", "OFF"] + new-output-module: ["ON"] fail-fast: false runs-on: ${{matrix.os}} @@ -65,13 +66,15 @@ jobs: -DSEMBA_FDTD_ENABLE_MPI=${{matrix.mpi}} \ -DSEMBA_FDTD_ENABLE_HDF=${{matrix.hdf}} \ -DSEMBA_FDTD_ENABLE_MTLN=${{matrix.mtln}} \ - -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} + -DSEMBA_FDTD_ENABLE_DOUBLE_PRECISION=${{matrix.double-precision}} \ + -DSEMBA_FDTD_ENABLE_OUTPUT_MODULE=${{matrix.new-output-module}} cmake --build build -j - name: Run unit tests run: build/bin/fdtd_tests.exe - name: Run python tests (except codemodel) + if: matrix.new-output-module == 'OFF' env: SEMBA_FDTD_ENABLE_MPI: ${{ matrix.mpi }} SEMBA_FDTD_ENABLE_MTLN: ${{ matrix.mtln }} diff --git a/src_mtln/dispersive.F90 b/src_mtln/dispersive.F90 index 9aa790d5..a60b659f 100644 --- a/src_mtln/dispersive.F90 +++ b/src_mtln/dispersive.F90 @@ -1,5 +1,5 @@ module dispersive_m - use utils_m + use mtln_utils_m use rational_approximation_m use FDETYPES_m, only: RKIND, RKIND_TIEMPO implicit none @@ -288,4 +288,3 @@ subroutine addTransferImpedanceInConductors(this, conductor_1, conductor_2, conn end module dispersive_m - diff --git a/src_mtln/mtl.F90 b/src_mtln/mtl.F90 index d5792e25..ab504f15 100644 --- a/src_mtln/mtl.F90 +++ b/src_mtln/mtl.F90 @@ -1,7 +1,7 @@ module mtl_m ! use NFDETypes - use utils_m + use mtln_utils_m use dispersive_m, dispersive_lumped_t => lumped_t use mtln_types_m, only: segment_t, multipolar_expansion_t use multipolar_expansion_m, only: getCellCapacitanceOnBox, getCellInductanceOnBox diff --git a/src_mtln/mtl_bundle.F90 b/src_mtln/mtl_bundle.F90 index cce0acb6..76435567 100644 --- a/src_mtln/mtl_bundle.F90 +++ b/src_mtln/mtl_bundle.F90 @@ -1,6 +1,6 @@ module mtl_bundle_m - use utils_m + use mtln_utils_m use probes_m use generators_m use dispersive_m @@ -504,4 +504,4 @@ subroutine Comm_MPI_Fields(this) end subroutine #endif -end module mtl_bundle_m \ No newline at end of file +end module mtl_bundle_m diff --git a/src_mtln/utils.F90 b/src_mtln/utils.F90 index b17c8185..b695ac21 100644 --- a/src_mtln/utils.F90 +++ b/src_mtln/utils.F90 @@ -1,4 +1,4 @@ -module utils_m +module mtln_utils_m use iso_fortran_env, only: real64 use FDETYPES_m, only: RKIND @@ -113,4 +113,4 @@ function element_wise_invert(n, x) result(y) end function -end module utils_m +end module mtln_utils_m diff --git a/src_output/bulkProbeOutput.F90 b/src_output/bulkProbeOutput.F90 index 9bede40b..0aaa66a5 100644 --- a/src_output/bulkProbeOutput.F90 +++ b/src_output/bulkProbeOutput.F90 @@ -1,6 +1,7 @@ module bulkProbeOutput_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use outputTypes_m use outputUtils_m implicit none diff --git a/src_output/frequencySliceProbeOutput.F90 b/src_output/frequencySliceProbeOutput.F90 index 784b2d7e..6e9149b5 100644 --- a/src_output/frequencySliceProbeOutput.F90 +++ b/src_output/frequencySliceProbeOutput.F90 @@ -1,6 +1,7 @@ module frequencySliceProbeOutput_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use report_m use outputTypes_m use outputUtils_m diff --git a/src_output/movieProbeOutput.F90 b/src_output/movieProbeOutput.F90 index 6ffdee26..b34f6615 100644 --- a/src_output/movieProbeOutput.F90 +++ b/src_output/movieProbeOutput.F90 @@ -1,10 +1,12 @@ module movieProbeOutput_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use report_m use outputTypes_m use outputUtils_m use volumicProbeUtils_m + use directoryUtils_m, only: add_extension, get_last_component, join_path, create_folder, create_file_with_path use xdmfAPI_m implicit none private diff --git a/src_output/pointProbeOutput.F90 b/src_output/pointProbeOutput.F90 index 5c3212c5..b0bba27b 100644 --- a/src_output/pointProbeOutput.F90 +++ b/src_output/pointProbeOutput.F90 @@ -1,6 +1,7 @@ module pointProbeOutput_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use outputTypes_m use domain_m use outputUtils_m diff --git a/src_output/volumicProbeUtils.F90 b/src_output/volumicProbeUtils.F90 index fc024861..5eade9e3 100644 --- a/src_output/volumicProbeUtils.F90 +++ b/src_output/volumicProbeUtils.F90 @@ -1,6 +1,7 @@ module volumicProbeUtils_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use outputTypes_m use outputUtils_m implicit none diff --git a/src_output/wireProbeOutput.F90 b/src_output/wireProbeOutput.F90 index f8ef97d9..f98e2d26 100644 --- a/src_output/wireProbeOutput.F90 +++ b/src_output/wireProbeOutput.F90 @@ -1,6 +1,7 @@ module wireProbeOutput_m use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use report_m use outputTypes_m use outputUtils_m diff --git a/src_utils/directoryUtils.F90 b/src_utils/directoryUtils.F90 index a09a165c..4585cc68 100644 --- a/src_utils/directoryUtils.F90 +++ b/src_utils/directoryUtils.F90 @@ -132,7 +132,7 @@ subroutine create_folder(path, ios) return end if -#ifdef __WIN32__ +#ifdef _WIN32 call execute_command_line("mkdir """//trim(path)//"""", exitstat=ios) #else call execute_command_line("mkdir -p "//trim(path), exitstat=ios) @@ -151,7 +151,7 @@ subroutine remove_folder(path, ios) return end if -#ifdef __WIN32__ +#ifdef _WIN32 call execute_command_line("rmdir /S /Q """//trim(path)//"""", exitstat=ios) #else call execute_command_line("rm -rf "//trim(path), exitstat=ios) diff --git a/test/mtln/test_math.F90 b/test/mtln/test_math.F90 index 94bbaeae..749966ee 100644 --- a/test/mtln/test_math.F90 +++ b/test/mtln/test_math.F90 @@ -1,5 +1,5 @@ integer function test_math_eigvals() bind(C) result(error_cnt) - use utils_m + use mtln_utils_m use mtln_testingTools_mod use iso_fortran_env, only: real64 @@ -63,4 +63,4 @@ integer function test_math_matmul_broadcast() bind(C) result(error_cnt) error_cnt = error_cnt +1 end if -end function \ No newline at end of file +end function diff --git a/test/observation/test_observation_init.F90 b/test/observation/test_observation_init.F90 index 517afd31..c2cae805 100644 --- a/test/observation/test_observation_init.F90 +++ b/test/observation/test_observation_init.F90 @@ -2,7 +2,7 @@ integer function test_init_time_movie_observation() bind(C) result(err) use FDETYPES_m use FDETYPES_TOOLS use Observa_m - use observation_testingTools + use observation_testingTools, only: create_base_sgg, get_temp_dir type(SGGFDTDINFO_t) :: sgg type(media_matrices_t) :: media @@ -22,7 +22,8 @@ integer function test_init_time_movie_observation() bind(C) result(err) sgg = create_base_sgg() call set_sgg_data(sgg) - media = create_media(sgg%Alloc) + media = create_geometry_media_from_sggAlloc(sgg%Alloc) + media%sggMtag = 0 tag_numbers = create_tag_list(sgg%Alloc) ThereAreObservation = .false. @@ -36,7 +37,7 @@ integer function test_init_time_movie_observation() bind(C) result(err) facesNF2FF = create_facesNF2FF(.false., .false., .false., .false., .false., .false.) control = create_control_flags(0, 0, 3, 10, trim(get_temp_dir())//'/entryRoot', "wireflavour",& - .false., .false., .false., .false., .false.,& + .false., .false., .false., .false., .false., .false.,& facesNF2FF) call InitObservation(sgg, media, tag_numbers, & @@ -127,4 +128,4 @@ function create_time_movie_observable(XI,YI,ZI,XE,YE,ZE) result(observable) observable%What = iMEC end function create_time_movie_observable -end function test_init_time_movie_observation \ No newline at end of file +end function test_init_time_movie_observation diff --git a/test/observation/test_observation_update.F90 b/test/observation/test_observation_update.F90 index 75e5c042..3ae15a12 100644 --- a/test/observation/test_observation_update.F90 +++ b/test/observation/test_observation_update.F90 @@ -2,7 +2,7 @@ integer function test_update_time_movie_observation() bind(C) result(err) use FDETYPES_m use FDETYPES_TOOLS use Observa_m - use observation_testingTools + use observation_testingTools, only: create_base_sgg, dummyFields_t, get_temp_dir type(SGGFDTDINFO_t) :: sgg type(media_matrices_t) :: media @@ -24,7 +24,8 @@ integer function test_update_time_movie_observation() bind(C) result(err) sgg = create_base_sgg() call set_sgg_data(sgg) - media = create_media(sgg%Alloc) + media = create_geometry_media_from_sggAlloc(sgg%Alloc) + media%sggMtag = 0 tag_numbers = create_tag_list(sgg%Alloc) ThereAreObservation = .false. @@ -38,7 +39,7 @@ integer function test_update_time_movie_observation() bind(C) result(err) facesNF2FF = create_facesNF2FF(.false., .false., .false., .false., .false., .false.) control = create_control_flags(0, 0, 3, 10, trim(get_temp_dir())//'/entryRoot', "wireflavour",& - .false., .false., .false., .false., .false.,& + .false., .false., .false., .false., .false., .false.,& facesNF2FF) call InitObservation(sgg, media, tag_numbers, & @@ -112,4 +113,4 @@ function create_time_movie_observable(XI,YI,ZI,XE,YE,ZE) result(observable) observable%What = iMEC end function create_time_movie_observable -end function test_update_time_movie_observation \ No newline at end of file +end function test_update_time_movie_observation diff --git a/test/utils/fdetypes_tools.F90 b/test/utils/fdetypes_tools.F90 index 489350fb..82d3cf43 100644 --- a/test/utils/fdetypes_tools.F90 +++ b/test/utils/fdetypes_tools.F90 @@ -1,6 +1,7 @@ module FDETYPES_TOOLS use FDETYPES_m use utils_m + use allocationUtils_m, only: alloc_and_init use NFDETypes_m implicit none private From 1ff65f0ab792d19fa5c3352b1d03578280a0dc4a Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Tue, 28 Jul 2026 10:35:25 +0000 Subject: [PATCH 09/10] FDTD | ENV | Unify build directory and add Intel container --- .devcontainer/devcontainer.json | 4 +- .vscode/launch.dev.json | 8 ++-- CLAUDE.md | 5 +-- CMakePresets.json | 16 ++++---- Dockerfile | 62 ++++++++++++++++++++++++++++++ doc/development.md | 24 ++++++++---- doc/docker.md | 57 ++++++++++++++++++++++----- docker-compose.yml | 19 +++++++++ docker/intel-quality-entrypoint.sh | 10 +++++ test/pyWrapper/utils.py | 22 ++--------- 10 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 docker/intel-quality-entrypoint.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 075c5643..883e1ac6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -35,10 +35,10 @@ "fortran-lang.linter-gfortran", "ms-vscode.cmake-tools", "ms-python.python", - "eamodio.gitlens" + "mhutchie.git-graph" ], "settings": { - "cmake.buildDirectory": "${workspaceFolder}/build-dev", + "cmake.buildDirectory": "${workspaceFolder}/build", "cmake.useCMakePresets": "always", "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "terminal.integrated.defaultProfile.linux": "bash" diff --git a/.vscode/launch.dev.json b/.vscode/launch.dev.json index 28d56134..b542eab7 100644 --- a/.vscode/launch.dev.json +++ b/.vscode/launch.dev.json @@ -5,7 +5,7 @@ "name": "Debug semba-fdtd (dbg)", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build-dbg/bin/semba-fdtd", + "program": "${workspaceFolder}/build/bin/semba-fdtd", "args": ["${input:inputFile}"], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -23,7 +23,7 @@ "name": "Debug semba-fdtd (dbg-mpi)", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build-dbg-mpi/bin/semba-fdtd", + "program": "${workspaceFolder}/build/bin/semba-fdtd", "args": ["${input:inputFile}"], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -40,7 +40,7 @@ "name": "Debug fdtd_tests (dbg)", "type": "cppdbg", "request": "launch", - "program": "${workspaceFolder}/build-dbg/bin/fdtd_tests", + "program": "${workspaceFolder}/build/bin/fdtd_tests", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -58,7 +58,7 @@ "name": "Attach to process", "type": "cppdbg", "request": "attach", - "program": "${workspaceFolder}/build-dbg/bin/semba-fdtd", + "program": "${workspaceFolder}/build/bin/semba-fdtd", "processId": "${command:pickProcess}", "MIMode": "gdb" } diff --git a/CLAUDE.md b/CLAUDE.md index 254fa4d5..136d88fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,13 +10,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **First time setup (required):** ```bash -git submodule init -git submodule update +git submodule update --init --recursive ``` **Configure and build:** ```bash -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --fresh --preset rls cmake --build build -j ``` diff --git a/CMakePresets.json b/CMakePresets.json index f61d693e..e8586aaa 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -4,7 +4,7 @@ { "name": "rls", "generator": "Ninja", - "binaryDir": "build-rls/", + "binaryDir": "build/", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "SEMBA_FDTD_ENABLE_MPI": "OFF" @@ -13,7 +13,7 @@ { "name": "dbg", "inherits": "rls", - "binaryDir": "build-dbg/", + "binaryDir": "build/", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } @@ -21,7 +21,7 @@ { "name": "rls-mpi", "inherits": "rls", - "binaryDir": "build-rls-mpi/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MPI": "ON" } @@ -29,7 +29,7 @@ { "name": "dbg-mpi", "inherits": "dbg", - "binaryDir": "build-dbg-mpi/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MPI": "ON" } @@ -37,7 +37,7 @@ { "name": "dbg-nomtln", "inherits": "dbg", - "binaryDir": "build-dbg-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } @@ -45,7 +45,7 @@ { "name": "rls-nomtln", "inherits": "rls", - "binaryDir": "build-rls-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } @@ -53,7 +53,7 @@ { "name": "intel-rls", "generator": "Ninja", - "binaryDir": "build-intel-rls/", + "binaryDir": "build/", "environment": { "I_MPI_ROOT": "/opt/intel/oneapi/mpi/latest", "FC": "mpiifx", @@ -70,7 +70,7 @@ { "name": "intel-rls-nomtln", "inherits": "intel-rls", - "binaryDir": "build-intel-rls-nomtln/", + "binaryDir": "build/", "cacheVariables": { "SEMBA_FDTD_ENABLE_MTLN": "OFF" } diff --git a/Dockerfile b/Dockerfile index 7cdb1c06..e36f70bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,11 @@ # Keep shared build dependencies first so both environments reuse this layer. FROM ubuntu:26.04@sha256:b7f48194d4d8b763a478a621cdc81c27be222ba2206ca3ca6bc42b49685f3d9e AS quality-base +# Prevent package installation from prompting during image builds. ENV DEBIAN_FRONTEND=noninteractive +# Cache apt metadata and packages between BuildKit builds without retaining them +# in the final image layer. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y \ @@ -23,14 +26,19 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && locale-gen en_US.UTF-8 \ && rm -rf /var/lib/apt/lists/* +# Use a UTF-8 locale consistently for compiler, test, and terminal output. ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 +# These can be overridden to match the host user and avoid bind-mount ownership +# mismatches when running through Docker Compose. ARG USERNAME=developer ARG USER_UID=1000 ARG USER_GID=1000 +# Reuse a pre-existing UID/GID where Ubuntu provides one, otherwise create the +# requested account. RUN set -eux; \ existing_group="$(getent group ${USER_GID} | cut -d: -f1)"; \ if [ -n "${existing_group}" ] && [ "${existing_group}" != "${USERNAME}" ] && ! getent group ${USERNAME} >/dev/null; then \ @@ -61,6 +69,7 @@ FROM quality AS dev USER root +# Development-only tools: version control, debugging, and the OpenCode CLI. RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y \ @@ -74,12 +83,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ sudo \ && rm -rf /var/lib/apt/lists/* +# Preserve package-manager caches across BuildKit builds. RUN --mount=type=cache,target=/root/.npm \ npm install -g opencode-ai RUN --mount=type=cache,target=/root/.cache/pip \ python3 -m pip install --break-system-packages fortls fprettify +# Prepare mount points and grant passwordless sudo for interactive development. RUN mkdir -p /home/${USERNAME}/.config \ /home/${USERNAME}/.ssh \ /home/${USERNAME}/.local/share/opencode \ @@ -93,3 +104,54 @@ RUN mkdir -p /home/${USERNAME}/.config \ USER ${USERNAME} WORKDIR /home/${USERNAME}/workspaces/fdtd + +# Intel oneAPI 2025.0 ships the compilers, Intel MPI, and matching runtime +# libraries as a tested toolkit rather than relying on Intel's APT repository. +FROM intel/oneapi-hpckit:2025.0.0-0-devel-ubuntu24.04 AS intel-quality + +USER root + +ENV DEBIAN_FRONTEND=noninteractive + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y \ + locales \ + cmake \ + make \ + ninja-build \ + && locale-gen en_US.UTF-8 \ + && rm -rf /var/lib/apt/lists/* + +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=1000 + +RUN set -eux; \ + existing_group="$(getent group ${USER_GID} | cut -d: -f1)"; \ + if [ -n "${existing_group}" ] && [ "${existing_group}" != "${USERNAME}" ] && ! getent group ${USERNAME} >/dev/null; then \ + groupmod -n ${USERNAME} ${existing_group}; \ + elif [ -z "${existing_group}" ]; then \ + groupadd --gid ${USER_GID} ${USERNAME}; \ + fi; \ + existing_user="$(getent passwd ${USER_UID} | cut -d: -f1)"; \ + if [ -n "${existing_user}" ] && [ "${existing_user}" != "${USERNAME}" ]; then \ + usermod -l ${USERNAME} -d /home/${USERNAME} -m ${existing_user}; \ + elif [ -z "${existing_user}" ]; then \ + useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USERNAME}; \ + fi; \ + mkdir -p /home/${USERNAME}/workspaces/fdtd; \ + chown -R ${USER_UID}:${USER_GID} /home/${USERNAME}/workspaces + +COPY docker/intel-quality-entrypoint.sh /usr/local/bin/intel-quality-entrypoint +RUN chmod 0755 /usr/local/bin/intel-quality-entrypoint + +USER ${USERNAME} +WORKDIR /home/${USERNAME}/workspaces/fdtd + +ENTRYPOINT ["/usr/local/bin/intel-quality-entrypoint"] +CMD ["bash", "-l"] diff --git a/doc/development.md b/doc/development.md index a4dd8aa4..1457b820 100644 --- a/doc/development.md +++ b/doc/development.md @@ -8,7 +8,13 @@ In windows, you need to install [intel oneapi runtime libraries](https://www.int ## GNU/Linux Compilation -It is important to point out the repository has dependencies which are available as submodules. It is necessary to run `git submodule init` and `git submodule update` from the root folder before running any `cmake` or `build` commands. +The repository has dependencies available as submodules. Before running CMake, initialise them from the repository root: + +```shell +git submodule update --init --recursive +``` + +All local configurations use `build/`. Run CMake with `--fresh` whenever changing compiler, build type, or feature options so the previous cache is discarded. If you use intel oneapi compiler, make sure to run @@ -93,8 +99,13 @@ navigate to the `/fdtd/` folder that has been created, this folder will be refer ### Prerequisites This compilation process will use the already available precompiled libraries included with the project, thus it's not required to build them manually. -This repository has dependencies that are available as submodules. It is necessary to run `git submodule init` and `git submodule update` from the root folder before running any `cmake` or `build` commands. -In the .gitmodules file, the submodules use the SSH remote URL by default. If not using a SSH-key in the computer where the following process will be performed, the remote addresses for each submodule must be individually changed to their HTTPS alternative. +This repository has dependencies available as submodules. Initialise them from the root folder before running CMake: + +```shell +git submodule update --init --recursive +``` + +The default submodule URLs use HTTPS. This software requires [Windows BaseKit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit.html) and [Windows HPCKit](https://www.intel.com/content/www/us/en/developer/tools/oneapi/hpc-toolkit-download.html). Install these packages with all their features selected. @@ -113,7 +124,7 @@ This will load the OneAPI environment for x64. Navigate to the fdtd root folder, choose between "Debug"/"Release" for `-DCMAKE_BUILD_TYPE`, and "ON"/"OFF" for `-DSEMBA_FDTD_ENABLE_MPI`, for example, a Release version with MPI Support would be: ```shell -cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Release -DSEMBA_FDTD_ENABLE_MPI=ON +cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Release -DSEMBA_FDTD_ENABLE_MPI=ON --fresh ``` Then, @@ -260,8 +271,7 @@ cd This project has submodule dependencies remember to initiate an update the ```bash -git submodule init -git submodule update +git submodule update --init --recursive ``` #### Step 2: Install Python Requirements @@ -392,4 +402,4 @@ Run the following command as super user: ``` echo 0| sudo tee /proc/sys/kernel/yama/ptrace_scope ``` -([source](https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB)) \ No newline at end of file +([source](https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB)) diff --git a/doc/docker.md b/doc/docker.md index 82eebeb0..cd943c33 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -1,7 +1,9 @@ # Docker -Docker provides two environments for working with semba-fdtd. -Both mount the repository at `/home/developer/workspaces/fdtd`, so build artefacts and simulation output are written to the host workspace. +Docker provides three environments for working with semba-fdtd. +All environments mount the repository at `/home/developer/workspaces/fdtd`, so build artefacts and simulation output are written to the host workspace. +All presets use the same `build/` directory. +Run CMake with `--fresh` whenever switching preset so the previous compiler and configuration cache is discarded. ## Prerequisite @@ -16,10 +18,12 @@ git submodule update --init --recursive | Service | Purpose | Included tools | |---|---|---| | `quality` | Compile, test, run examples, and inspect generated output | GNU C/C++/Fortran compilers, CMake, Ninja, MPI, HDF5, Python, and ParaView | +| `intel-quality` | Validate Intel oneAPI compiler and Intel MPI builds | Intel oneAPI HPCKit, Intel MPI, CMake, Ninja, and the Intel HDF5 runtime | | `dev` | Interactive development and the Dev Container | Everything in `quality`, plus Git, GitHub CLI, SSH client, Node/npm, OpenCode, Fortran tools, and debuggers | `quality` intentionally excludes developer-only tools. -Both environments include ParaView. +`quality` and `dev` include ParaView. +`intel-quality` is for Intel-specific compilation checks; it is not the Python test environment or the Dev Container. ## Build Images @@ -35,6 +39,12 @@ Build `dev` when its additional developer tools are required: docker compose build dev ``` +Build the Intel oneAPI validation environment when checking Intel-specific configurations: + +```bash +docker compose build intel-quality +``` + The `dev` target is built from `quality`. Docker therefore reuses the compiler and ParaView layers when building `dev`. @@ -46,25 +56,54 @@ Editing source files, running examples, and creating build directories do not in Run CMake presets from the mounted workspace: ```bash -docker compose run --rm quality cmake --preset rls +docker compose run --rm quality cmake --fresh --preset rls docker compose run --rm quality cmake --build --preset rls ``` For an MPI build: ```bash -docker compose run --rm quality cmake --preset rls-mpi +docker compose run --rm quality cmake --fresh --preset rls-mpi docker compose run --rm quality cmake --build --preset rls-mpi ``` -Build directories such as `build-rls/` and `build-rls-mpi/` remain in the repository after the container exits. +The active build remains in `build/` after the container exits. + +## Compile With Intel oneAPI + +`intel-quality` initialises Intel oneAPI and Intel MPI automatically. +It also selects the bundled Intel HDF5 runtime, so no manual environment setup is required. + +Use the Intel Release preset to validate the MPI, MTLN, and double-precision configuration: + +```bash +docker compose run --rm intel-quality cmake --fresh --preset intel-rls +docker compose run --rm intel-quality cmake --build --preset intel-rls +docker compose run --rm intel-quality build/bin/fdtd_tests +``` + +For the Intel configuration without MTLN: + +```bash +docker compose run --rm intel-quality cmake --fresh --preset intel-rls-nomtln +docker compose run --rm intel-quality cmake --build --preset intel-rls-nomtln +docker compose run --rm intel-quality build/bin/fdtd_tests +``` + +Open a shell for repeated Intel build checks: + +```bash +docker compose run --rm intel-quality +``` + +A terminal inside the container should appear. ## Run Binaries And Examples Use the binary produced in the mounted build directory: ```bash -docker compose run --rm quality build-rls/bin/semba-fdtd -i path/to/case.fdtd.json +docker compose run --rm quality build/bin/semba-fdtd -i path/to/case.fdtd.json ``` Any output generated by the solver is available immediately in the corresponding host directory. @@ -87,9 +126,9 @@ docker compose run --rm dev paraview Configure a build with tests, then run the required test commands: ```bash -docker compose run --rm quality cmake --preset rls +docker compose run --rm quality cmake --fresh --preset rls docker compose run --rm quality cmake --build --preset rls -docker compose run --rm quality build-rls/bin/fdtd_tests +docker compose run --rm quality build/bin/fdtd_tests ``` Python integration tests require the project dependencies in a virtual environment: diff --git a/docker-compose.yml b/docker-compose.yml index 365c0d27..5e523211 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,31 @@ services: + # Lightweight image for compiling and testing the solver. quality: build: context: . target: quality volumes: + # Work directly against the checked-out source tree. - type: bind source: . target: /home/developer/workspaces/fdtd working_dir: /home/developer/workspaces/fdtd tty: true + # Intel oneAPI compiler and Intel MPI environment for manual quality builds. + intel-quality: + build: + context: . + target: intel-quality + volumes: + - type: bind + source: . + target: /home/developer/workspaces/fdtd + working_dir: /home/developer/workspaces/fdtd + stdin_open: true + tty: true + + # Interactive development image with source-control and OpenCode tooling. dev: build: context: . @@ -18,6 +34,7 @@ services: - type: bind source: . target: /home/developer/workspaces/fdtd + # Use the host Git identity and SSH keys for authenticated Git operations. - type: bind source: ${HOME}/.gitconfig target: /home/developer/.gitconfig @@ -28,6 +45,7 @@ services: - type: bind source: ${HOME}/.config/gh target: /home/developer/.config/gh + # Share local OpenCode configuration and agent resources with the container. - type: bind source: ${HOME}/.config/opencode target: /home/developer/.config/opencode @@ -35,6 +53,7 @@ services: source: ${HOME}/.agents target: /home/developer/.agents read_only: true + # Keep OpenCode's local data available between disposable containers. - type: bind source: ${HOME}/.local/share/opencode target: /home/developer/.local/share/opencode diff --git a/docker/intel-quality-entrypoint.sh b/docker/intel-quality-entrypoint.sh new file mode 100644 index 00000000..83e03aeb --- /dev/null +++ b/docker/intel-quality-entrypoint.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e + +# Initialise compiler and Intel MPI paths for every interactive container. +source /opt/intel/oneapi/setvars.sh --force >/dev/null + +export HDF5_ROOT="${HDF5_ROOT:-/home/developer/workspaces/fdtd/precompiled_libraries/linux-intel/hdf5}" +export LD_LIBRARY_PATH="${HDF5_ROOT}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +exec "$@" diff --git a/test/pyWrapper/utils.py b/test/pyWrapper/utils.py index d454745a..2a9fc216 100644 --- a/test/pyWrapper/utils.py +++ b/test/pyWrapper/utils.py @@ -35,26 +35,12 @@ def _default_semba_exe(): exe_name = 'semba-fdtd.exe' if platform == "win32" else 'semba-fdtd' - build_dirs = ['build'] + return os.path.abspath(os.getenv( + 'SEMBA_EXE', os.path.join(os.getcwd(), 'build', 'bin', exe_name))) - if os.getenv("SEMBA_FDTD_ENABLE_MPI") == "ON": - build_dirs.extend(['build-rls-mpi', 'build-dbg-mpi']) - else: - build_dirs.extend(['build-rls', 'build-dbg']) - for build_dir in build_dirs: - candidate = os.path.join(os.getcwd(), build_dir, 'bin', exe_name) - if os.path.isfile(candidate): - return candidate - - return os.path.join(os.getcwd(), 'build', 'bin', exe_name) - - -# Use of absolute path to avoid conflicts when changing directory. -if platform == "linux": - SEMBA_EXE = os.path.join(os.getcwd(), 'build', 'bin', 'semba-fdtd') -elif platform == "win32": - SEMBA_EXE = os.path.join(os.getcwd(), 'build', 'bin', 'semba-fdtd.exe') +# Use an absolute path to avoid conflicts when changing directory. +SEMBA_EXE = _default_semba_exe() NGSPICE_DLL = os.path.join( os.getcwd(), 'precompiled_libraries', 'windows-intel', 'ngspice', 'ngspice.dll') From 09509eeb090e3c09e73cbb91b8a769dc80ffc4bc Mon Sep 17 00:00:00 2001 From: adrianarce-elemwave Date: Tue, 28 Jul 2026 14:18:23 +0000 Subject: [PATCH 10/10] FDTD | ENV | Include debug mpi docs. Minor adjustments for mpi debug tools --- .devcontainer/devcontainer.json | 1 + .gitignore | 4 +- .vscode/extensions.dev.json | 36 +++++ .vscode/launch.dev.json | 228 ++++++++++++++++++++++++-- .vscode/settings.dev.json | 25 +++ .vscode/tasks.dev.json | 133 +++++++++++++++ .vscode/tasks.json | 133 +++++++++++++++ doc/development.md | 277 ++++++++++++++++++++++++++++---- doc/docker.md | 11 ++ doc/fdtdjson.md | 20 +++ doc/mtln.md | 11 +- scripts/debug-mpi-gdbserver.sh | 239 +++++++++++++++++++++++++++ 12 files changed, 1068 insertions(+), 50 deletions(-) create mode 100644 .vscode/extensions.dev.json create mode 100644 .vscode/settings.dev.json create mode 100644 .vscode/tasks.dev.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/debug-mpi-gdbserver.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 883e1ac6..012b6c98 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -34,6 +34,7 @@ "extensions": [ "fortran-lang.linter-gfortran", "ms-vscode.cmake-tools", + "ms-vscode.cpptools", "ms-python.python", "mhutchie.git-graph" ], diff --git a/.gitignore b/.gitignore index 2086ba67..12a28b40 100755 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ Testing/ /SEMBA_FDTD_temp.log .vscode/launch.json .vscode/settings.json -.vscode/tasks.json */*.bak ./*.bak .vscode/ltex.dictionary.en-US.txt @@ -76,4 +75,5 @@ git_info.txt .venv/build2/ build2/ -.venv/ \ No newline at end of file +.venv/ +specs/changes/* \ No newline at end of file diff --git a/.vscode/extensions.dev.json b/.vscode/extensions.dev.json new file mode 100644 index 00000000..a24ec6e9 --- /dev/null +++ b/.vscode/extensions.dev.json @@ -0,0 +1,36 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + // Fortran language support and linting + "fortran-lang.linter-gfortran", + + // CMake support + "ms-vscode.cmake-tools", + "twxs.cmake", + + // C++ tools (used alongside Fortran in this project) + "ms-vscode.cpptools", + "ms-vscode.cpp-devtools", + + // Testing + "matepek.vscode-catch2-test-adapter", + "fredericbonnet.cmake-test-adapter", + "ms-python.python", + + // Git + "mhutchie.git-graph", + "github.vscode-pull-request-github", + + // GitHub Copilot + "github.copilot", + "github.copilot-chat", + "ms-toolsai.jupyter" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} \ No newline at end of file diff --git a/.vscode/launch.dev.json b/.vscode/launch.dev.json index b542eab7..21455a9e 100644 --- a/.vscode/launch.dev.json +++ b/.vscode/launch.dev.json @@ -6,9 +6,9 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/build/bin/semba-fdtd", - "args": ["${input:inputFile}"], + "args": ["-i", "${config:semba-fdtd.debug.inputFile}"], "stopAtEntry": false, - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", "MIMode": "gdb", "setupCommands": [ { @@ -17,18 +17,100 @@ "ignoreFailures": true } ], - "preLaunchTask": "CMake: build Debug no-MPI" + "miDebuggerPath": "/usr/bin/gdb" }, { - "name": "Debug semba-fdtd (dbg-mpi)", + "name": "MPI: debug solver rank 0 (2 ranks)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/build/bin/semba-fdtd", - "args": ["${input:inputFile}"], - "stopAtEntry": false, - "cwd": "${workspaceFolder}", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} --foreground-debug-rank 0 2 ${workspaceFolder}/build/bin/semba-fdtd -i ${config:semba-fdtd.debug.inputFile}", + "serverStarted": "Listening on port 20000", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "order": 2 + }, "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "MPI all ranks: rank 0", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/semba-fdtd", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} --foreground-all 2 ${workspaceFolder}/build/bin/semba-fdtd -i ${config:semba-fdtd.debug.inputFile}", + "serverStarted": "Listening on port 20000", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "hidden": true + }, + "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, + { + "description": "Enable pretty printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "MPI all ranks: rank 1", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/semba-fdtd", + "args": [], + "stopAtEntry": true, + "cwd": "${workspaceFolder}/${config:semba-fdtd.debug.inputCwd}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20001", + "debugServerPath": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "debugServerArgs": "--wait-for-port 20001", + "serverStarted": "MPI gdbserver ready on port 20001", + "filterStdout": true, + "filterStderr": true, + "serverLaunchTimeout": 30000, + "presentation": { + "hidden": true + }, + "setupCommands": [ + { + "description": "Use local shared libraries", + "text": "set sysroot /", + "ignoreFailures": true + }, { "description": "Enable pretty printing for gdb", "text": "-enable-pretty-printing", @@ -52,7 +134,103 @@ "ignoreFailures": true } ], - "preLaunchTask": "CMake: build Debug no-MPI" + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests filter (dbg)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/fdtd_tests", + "args": ["--gtest_filter=${input:gtestFilter}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests (dbg-mpi)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug fdtd_tests filter (dbg-mpi)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/fdtd_tests", + "args": ["--gtest_filter=${input:gtestFilter}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "Debug MPI fdtd_tests rank 0", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20000", + "presentation": { + "hidden": true + }, + "postDebugTask": "Debug: stop MPI gdbservers" + }, + { + "name": "Debug MPI fdtd_tests rank 1", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/bin/fdtd_tests", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb", + "miDebuggerServerAddress": "localhost:20001", + "presentation": { + "hidden": true + } + }, + { + "name": "Debug pytest node (dbg)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["${input:pytestNode}", "-s"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { + "SEMBA_EXE": "${workspaceFolder}/build/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "OFF", + "SEMBA_FDTD_ENABLE_MTLN": "ON", + "SEMBA_FDTD_ENABLE_HDF": "ON" + } + }, + { + "name": "Debug pytest node (dbg-mpi)", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": ["${input:pytestNode}", "-s"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { + "SEMBA_EXE": "${workspaceFolder}/build/bin/semba-fdtd", + "SEMBA_FDTD_ENABLE_MPI": "ON", + "SEMBA_FDTD_ENABLE_MTLN": "ON", + "SEMBA_FDTD_ENABLE_HDF": "ON" + } }, { "name": "Attach to process", @@ -60,15 +238,41 @@ "request": "attach", "program": "${workspaceFolder}/build/bin/semba-fdtd", "processId": "${command:pickProcess}", - "MIMode": "gdb" + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/gdb" + } + ], + "compounds": [ + { + "name": "MPI: debug all ranks (2 ranks)", + "configurations": ["MPI all ranks: rank 0", "MPI all ranks: rank 1"], + "stopAll": true, + "presentation": { + "order": 1 + } + }, + { + "name": "MPI: debug fdtd_tests (2 ranks)", + "configurations": ["Debug MPI fdtd_tests rank 0", "Debug MPI fdtd_tests rank 1"], + "preLaunchTask": "Debug: prepare MPI fdtd_tests", + "stopAll": true, + "presentation": { + "order": 3 + } } ], "inputs": [ { - "id": "inputFile", + "id": "gtestFilter", + "type": "promptString", + "description": "GoogleTest filter", + "default": "*" + }, + { + "id": "pytestNode", "type": "promptString", - "description": "Path to .fdtd.json input file", - "default": "testData/input_examples/sphere.fdtd.json" + "description": "Pytest node or path", + "default": "test/pyWrapper/test_pyWrapper.py::test_fdtd_with_string_args" } ] } diff --git a/.vscode/settings.dev.json b/.vscode/settings.dev.json new file mode 100644 index 00000000..e5b27324 --- /dev/null +++ b/.vscode/settings.dev.json @@ -0,0 +1,25 @@ +{ + "semba-fdtd.debug.inputFile": "nodal-source-with-movie.fdtd.json", + "semba-fdtd.debug.inputCwd": "Testing", + "semba-fdtd.debug.mpiGtestFilter": "conformal.geometry_coord_position", + "fortran.fortls.path": "/usr/local/bin/fortls", + "fortran.fortls.notifyInit": true, + "fortran.fortls.disableAutoupdate": true, + "fortran.provide.hover": "Both", + "fortran.formatting.formatter": "fprettify", + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": [ + "entity.name.function.fortran", + "entity.name.function.procedure.fortran", + "entity.name.function.subroutine.fortran" + ], + "settings": { + "foreground": "#DCDCAA", + "fontStyle": "bold" + } + } + ] + } +} diff --git a/.vscode/tasks.dev.json b/.vscode/tasks.dev.json new file mode 100644 index 00000000..14181551 --- /dev/null +++ b/.vscode/tasks.dev.json @@ -0,0 +1,133 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: configure Release no-MPI", + "type": "shell", + "command": "cmake --fresh --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: build Release no-MPI", + "type": "shell", + "command": "cmake --build --preset rls", + "dependsOn": ["CMake: configure Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug no-MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MPI", + "type": "shell", + "command": "cmake --build --preset dbg", + "dependsOn": ["CMake: configure Debug no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug MPI", + "type": "shell", + "command": "cmake --build --preset dbg-mpi", + "dependsOn": ["CMake: configure Debug MPI"], + "problemMatcher": [] + }, + { + "label": "Debug: prepare MPI fdtd_tests", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": [ + "2", + "${workspaceFolder}/build/bin/fdtd_tests", + "--gtest_filter=${config:semba-fdtd.debug.mpiGtestFilter}" + ], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Debug: stop MPI gdbservers", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": ["--stop"], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "panel": "dedicated" + } + }, + { + "label": "CMake: configure Debug no-MTLN", + "type": "shell", + "command": "cmake --fresh --preset dbg-nomtln", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MTLN", + "type": "shell", + "command": "cmake --build --preset dbg-nomtln", + "dependsOn": ["CMake: configure Debug no-MTLN"], + "problemMatcher": "$gcc" + }, + { + "label": "Lint: Fortitude (advisory)", + "type": "shell", + "command": "fortitude check --exit-zero", + "problemMatcher": [] + }, + { + "label": "CMake: configure Release MPI", + "type": "shell", + "command": "cmake --fresh --preset rls-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Release MPI", + "type": "shell", + "command": "cmake --build --preset rls-mpi", + "dependsOn": ["CMake: configure Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: unit Release no-MPI", + "type": "shell", + "command": "build/bin/fdtd_tests", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release no-MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=OFF SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/bin/semba-fdtd .venv/bin/python -m pytest test/ -m 'not mpi' --durations=20", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=ON SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/bin/semba-fdtd .venv/bin/python -m pytest test/ -m mpi --durations=20", + "dependsOn": ["CMake: build Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: all Release no-MPI", + "dependsOrder": "sequence", + "dependsOn": [ + "Test: unit Release no-MPI", + "Test: pytest Release no-MPI" + ], + "problemMatcher": [] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..14181551 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,133 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: configure Release no-MPI", + "type": "shell", + "command": "cmake --fresh --preset rls", + "problemMatcher": [] + }, + { + "label": "CMake: build Release no-MPI", + "type": "shell", + "command": "cmake --build --preset rls", + "dependsOn": ["CMake: configure Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug no-MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MPI", + "type": "shell", + "command": "cmake --build --preset dbg", + "dependsOn": ["CMake: configure Debug no-MPI"], + "problemMatcher": [] + }, + { + "label": "CMake: configure Debug MPI", + "type": "shell", + "command": "cmake --fresh --preset dbg-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug MPI", + "type": "shell", + "command": "cmake --build --preset dbg-mpi", + "dependsOn": ["CMake: configure Debug MPI"], + "problemMatcher": [] + }, + { + "label": "Debug: prepare MPI fdtd_tests", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": [ + "2", + "${workspaceFolder}/build/bin/fdtd_tests", + "--gtest_filter=${config:semba-fdtd.debug.mpiGtestFilter}" + ], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Debug: stop MPI gdbservers", + "type": "process", + "command": "${workspaceFolder}/scripts/debug-mpi-gdbserver.sh", + "args": ["--stop"], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "panel": "dedicated" + } + }, + { + "label": "CMake: configure Debug no-MTLN", + "type": "shell", + "command": "cmake --fresh --preset dbg-nomtln", + "problemMatcher": [] + }, + { + "label": "CMake: build Debug no-MTLN", + "type": "shell", + "command": "cmake --build --preset dbg-nomtln", + "dependsOn": ["CMake: configure Debug no-MTLN"], + "problemMatcher": "$gcc" + }, + { + "label": "Lint: Fortitude (advisory)", + "type": "shell", + "command": "fortitude check --exit-zero", + "problemMatcher": [] + }, + { + "label": "CMake: configure Release MPI", + "type": "shell", + "command": "cmake --fresh --preset rls-mpi", + "problemMatcher": [] + }, + { + "label": "CMake: build Release MPI", + "type": "shell", + "command": "cmake --build --preset rls-mpi", + "dependsOn": ["CMake: configure Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: unit Release no-MPI", + "type": "shell", + "command": "build/bin/fdtd_tests", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release no-MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=OFF SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/bin/semba-fdtd .venv/bin/python -m pytest test/ -m 'not mpi' --durations=20", + "dependsOn": ["CMake: build Release no-MPI"], + "problemMatcher": [] + }, + { + "label": "Test: pytest Release MPI", + "type": "shell", + "command": "SEMBA_FDTD_ENABLE_MPI=ON SEMBA_FDTD_ENABLE_MTLN=ON SEMBA_FDTD_ENABLE_HDF=ON SEMBA_EXE=build/bin/semba-fdtd .venv/bin/python -m pytest test/ -m mpi --durations=20", + "dependsOn": ["CMake: build Release MPI"], + "problemMatcher": [] + }, + { + "label": "Test: all Release no-MPI", + "dependsOrder": "sequence", + "dependsOn": [ + "Test: unit Release no-MPI", + "Test: pytest Release no-MPI" + ], + "problemMatcher": [] + } + ] +} diff --git a/doc/development.md b/doc/development.md index 1457b820..a99f9e72 100644 --- a/doc/development.md +++ b/doc/development.md @@ -1,5 +1,22 @@ # Compilation and debugging +## Contents + +- [Prebuilt binary releases](#running-from-prebuilt-binary-releases) +- [GNU/Linux compilation](#gnulinux-compilation) + - [Compilation options](#compilation-options) + - [HDF5 libraries](#hdf5-libraries) + - [MTLN and ngspice](#mtln-and-ngspice) + - [MPI](#mpi) +- [Windows (intelLLVM) compilation](#windows-intelllvm-compilation) + - [Prerequisites](#prerequisites) + - [Compilation process](#compilation-process) + - [Visual Studio debugging](#debugging-with-visual-studio) +- [WSL2 and Visual Studio Code setup](#wsl2--visual-studio-code--gfortran-setup-guide) +- [Debugging the project](#debugging-the-project) + - [MPI debugging](#debugging-with-mpi) + - [Troubleshooting](#troubleshooting) + ## Running from prebuilt binary releases Prebuilt binares are available at [releases](https://github.com/OpenSEMBA/fdtd/releases). @@ -351,55 +368,245 @@ An example of launch.json filke is given. This will use a file as argument when Now you are ready to work with the project. -### Debugging with MPI +### Debugging with MPI + +#### Overview + +GDB controls one process per debug session. +To debug an MPI job, each MPI rank is therefore started under its own +`gdbserver`, and VS Code creates one `cppdbg` session for each rank. + +The checked-in configuration supports a two-rank solver job: + +```text +VS Code: MPI: debug all ranks (2 ranks) + |-- GDB session: rank 0 -> localhost:20000 -> gdbserver -> MPI rank 0 + `-- GDB session: rank 1 -> localhost:20001 -> gdbserver -> MPI rank 1 +``` + +Both sessions are shown separately in the VS Code **Call Stack** panel. +Breakpoints are sent to both sessions, although rank-specific control flow can +mean that only one rank reaches a particular breakpoint. -gdb is a serial debugger, but can be attached to one of the parallel processes after they have started running. +#### Configuration files +| File | Responsibility | +|---|---| +| `.vscode/launch.dev.json` | Version-controlled template for debug configurations. | +| `.vscode/launch.json` | Active local configuration; ignored by Git. | +| `.vscode/settings.json` | Local input file, working directory, and test filter values. | +| `.vscode/tasks.json` | Build tasks and the MPI-safe `fdtd_tests` preparation task. | +| `scripts/debug-mpi-gdbserver.sh` | Starts MPI ranks under `gdbserver`, waits for ports, and cleans stale jobs. | -1. Modify the file launch.json to attach to a running process after launching the debugger: +Copy the version-controlled files template when initially configuring the +workspace, or when the template changes: + +```shell +cp .vscode/launch.dev.json .vscode/launch.json +``` + +Add the following project-specific values to the local +`.vscode/settings.json` file: ```json { - "version": "0.2.0", - "configurations": [ - { - "name": "(gdb) Attach", - "type": "cppdbg", - "request": "attach", - "processId": "${command:pickProcess}", - "program": "${workspaceFolder}/build/bin/semba-fdtd", - "MIMode": "gdb", - "miDebuggerPath": "/usr/bin/gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } - ] - } - ] + "semba-fdtd.debug.inputFile": "pw-in-box.fdtd.json", + "semba-fdtd.debug.inputCwd": "testData/cases/planewave", + "semba-fdtd.debug.mpiGtestFilter": "conformal.geometry_coord_position" } ``` -2. Use *mpirun* to execute semba-fdtd paralellized in 'np' processes: -``` -mpirun -np 2 build/bin/semba-fdtd -i input_file.fdtd.json -args +`inputFile` is relative to `inputCwd`. +Set `inputCwd` to the directory containing the JSON input and all files that +the JSON references with relative paths, such as excitation files. + +#### Build requirements + +Debug launches do not configure or rebuild the project automatically. +Build an MPI-enabled Debug executable before starting VS Code debugging: + +```shell +cmake --fresh --preset dbg-mpi +cmake --build --preset dbg-mpi -j ``` -3. Once mpirun is running, launch the debuuger. A selection box will ask which process to attach to. Type *semba-fdtd* and all mpirun processes running semba will display. Selecto which process the debugger should attach to +All CMake presets currently share `build/`. +Running a Release or non-MPI configure replaces the previous build +configuration, so rerun the commands above before MPI debugging when needed. + +#### Starting all ranks + +1. Open the VS Code **Run and Debug** view. +2. Select `MPI: debug all ranks (2 ranks)`. +3. Press F5. +4. Wait for both `MPI all ranks: rank 0` and + `MPI all ranks: rank 1` to appear in **Call Stack**. +5. Continue each session once after the initial entry stop. + +The two ranks must both be allowed to continue. +If one remains stopped before `MPI_Init` or another collective operation, the +other rank can appear blocked while it waits for that rank. + +`MPI: debug solver rank 0 (2 ranks)` is a simpler alternative. +It runs a two-rank MPI job but attaches GDB only to rank 0; +rank 1 runs normally. + +#### Startup sequence + +The all-rank configuration is a VS Code compound containing two hidden launch +configurations. +No `preLaunchTask` or problem matcher is used for the solver. +Instead, the C/C++ extension directly owns the helper processes through +`debugServerPath` and waits for readiness through `serverStarted`. + +The startup sequence is: + +1. The rank 0 launch configuration invokes + `scripts/debug-mpi-gdbserver.sh` with `--foreground-all 2`. +2. The script validates `--workdir`, changes to that directory, and executes + one `mpirun -np 2` job. +3. Each MPI process calculates its debugger port as + `20000 + OMPI_COMM_WORLD_RANK` and then executes `gdbserver`. +4. The rank 0 adapter waits for `Listening on port 20000` and connects its GDB. +5. The rank 1 launch configuration invokes the same script with + `--wait-for-port 20001` instead of starting a second MPI job. +6. The waiter checks `/proc/net/tcp` and `/proc/net/tcp6` without opening a + debugger connection. +7. When port 20001 is listening, the rank 1 adapter connects its own GDB. +8. The compound's `stopAll` option stops both sessions when either session is + terminated. + +It is important that rank 1 only waits for its port. +Starting `mpirun` from both hidden configurations would create two unrelated +MPI jobs rather than two debugger views of the same job. + +#### Port and rank mapping + +| MPI rank | `gdbserver` address | VS Code session | +|---:|---|---| +| 0 | `localhost:20000` | `MPI all ranks: rank 0` | +| 1 | `localhost:20001` | `MPI all ranks: rank 1` | + +The shell script supports more ranks, but the checked-in compound explicitly +defines two GDB sessions. +Supporting additional ranks requires another hidden launch configuration and +port waiter for each extra rank. + +#### Working directory + +The target process is launched by `gdbserver`, not directly by `cppdbg`. +Consequently, the `cwd` property alone does not reliably set the inferior's +working directory. + +The launch configuration passes the directory explicitly: + +```text +--workdir ${workspaceFolder}/${config:semba-fdtd.debug.inputCwd} +``` + +The script verifies that the directory exists and is writable, then changes to +it before starting `mpirun`. +This is required for relative JSON resources, output files, and solver control +files such as `running`, `pause`, `relaunch`, and `forcestop`. + +#### Script modes + +The helper script has the following modes: + +| Mode | Purpose | +|---|---| +| `--foreground-all ...` | Run every MPI rank under a separate `gdbserver`. | +| `--foreground-debug-rank ...` | Debug one rank and run the remaining ranks normally. | +| `--wait-for-port ` | Wait for another launch configuration's `gdbserver`. | +| `--debug-rank ...` | Detached preparation mode used by task-based workflows. | +| `--stop` | Stop a detached MPI debug job recorded by the script. | + +The `--foreground-*` modes are preferred for solver debugging because +`OpenDebugAD7` owns their lifetime directly. +This avoids races in which a background task exits before GDB connects. + +#### Debugging MPI unit tests + +The full `fdtd_tests` suite should normally be debugged as one process, even +when linked against an MPI-enabled build. +Several tests write fixed file names and are not safe to execute concurrently +on every rank. + +The `MPI: debug fdtd_tests (2 ranks)` compound is intended only for an +MPI-safe filtered test. +Set `semba-fdtd.debug.mpiGtestFilter` in `.vscode/settings.json` before using +that compound. #### Troubleshooting -1. After selecting the process the debugger should attach to, a new terminal opens with the message "Superuser access is required to attach to a process" +**GDB connection timeout** -Run the following command as super user: +Confirm that the selected configuration is +`MPI: debug all ranks (2 ranks)` and reload the VS Code window after changing +`launch.json`. +The solver configuration must use `debugServerPath`, `serverStarted`, and the +foreground script modes; it must not depend on a background `preLaunchTask`. + +Check for stale MPI or `gdbserver` processes before retrying: + +```shell +pgrep -af 'gdbserver|prterun|mpirun' +``` + +**Cannot create `running` or another relative file** + +Verify `semba-fdtd.debug.inputCwd` and confirm that the directory is writable. +The debug output prints `MPI working directory: ...` before `mpirun` starts. + +**A breakpoint is not reached** + +Confirm that the correct rank executes that code path and that the breakpoint +was installed before the one-time initialization code ran. +Also confirm that the active executable is an MPI-enabled Debug build. +A valid source breakpoint cannot force execution through a false runtime +condition. + +**Both sessions connect but the program does not advance** + +Select each rank in **Call Stack** and continue it. +One stopped rank can hold the other rank inside an MPI collective operation. + +**Warnings about unavailable system-library debug information** + +Messages about missing separate debug information for MPI or system libraries +are non-fatal when debugging project sources. +Install the corresponding system debug packages only when stepping inside those +libraries is required. + +#### Manual attach fallback + +The native compound is preferred, but GDB can also attach manually to an +already running process. +Start the MPI job in a terminal: + +```shell +mpirun -np 2 build/bin/semba-fdtd -i input_file.fdtd.json ``` -echo 0| sudo tee /proc/sys/kernel/yama/ptrace_scope + +Then use the `Attach to process` configuration and select one `semba-fdtd` +process. +This method provides one attached rank per debug session and does not perform +the automatic port coordination described above. + +If Linux blocks manual attachment because of `ptrace_scope`, temporarily relax +the restriction only on a trusted development machine: + +```shell +sudo sysctl kernel.yama.ptrace_scope=0 ``` -([source](https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB)) + +Restore the normal restriction after debugging: + +```shell +sudo sysctl kernel.yama.ptrace_scope=1 +``` + +See the [MIEngine troubleshooting guide][miengine-troubleshooting] +for more information. + +[miengine-troubleshooting]: https://github.com/Microsoft/MIEngine/wiki/Troubleshoot-attaching-to-processes-using-GDB diff --git a/doc/docker.md b/doc/docker.md index cd943c33..2d48e519 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -5,6 +5,17 @@ All environments mount the repository at `/home/developer/workspaces/fdtd`, so b All presets use the same `build/` directory. Run CMake with `--fresh` whenever switching preset so the previous compiler and configuration cache is discarded. +## Contents + +- [Prerequisite](#prerequisite) +- [Environments](#environments) +- [Build images](#build-images) +- [Compile with quality](#compile-with-quality) +- [Compile with Intel oneAPI](#compile-with-intel-oneapi) +- [Run binaries and examples](#run-binaries-and-examples) +- [Run tests](#run-tests) +- [Development environment](#development-environment) + ## Prerequisite Initialise submodules before building an image: diff --git a/doc/fdtdjson.md b/doc/fdtdjson.md index dec86010..e8d7e8f1 100644 --- a/doc/fdtdjson.md +++ b/doc/fdtdjson.md @@ -5,6 +5,26 @@ Being in JSON, it can be easily navigated with most text editors, such as Visual There are also multiple tools to read and write them. This document assumes that you are familiar with the basic JSON notation, a brief explanation on this notation can be found [here](https://www.w3schools.com/js/js_json_syntax.asp). +## Contents + +- [Examples](#examples) +- [FDTD-JSON objects description](#fdtd-json-objects-description) + - [`general`, `background`, and `boundary`](#general) + - [`mesh`](#mesh) + - [`materials`](#materials) + - [`materialAssociations`](#materialassociations) + - [`probes`](#probes) + - [`sources`](#sources) +- [Material types](#bulk-materials) + - [`lumped` models](#lumped) + - [Wire and multiwire materials](#wire) + - [Terminals and connectors](#terminal) +- [Probe types](#probe-types) +- [Probe domains](#domain) +- [Source types](#planewave) + +## Examples + The following are examples of valid inputs: 1. An empty space illuminated by a plane wave: [planewave.fdtd.json](testData/input_examples/planewave.fdtd.json). The field at a point close to the center is recorded. diff --git a/doc/mtln.md b/doc/mtln.md index 57c71ee7..a1b2f879 100644 --- a/doc/mtln.md +++ b/doc/mtln.md @@ -5,6 +5,16 @@ bibliography: # The `mtln` solver module This module allows to solve networks of multiconductor transmission line bundles in the time domain. + +## Contents + +- [Multiconductor transmission lines](#multiconductor-transmission-lines) +- [Networks](#networks) +- [Bundles](#bundles) +- [Features](#features) + - [Coupling to NgSpice](#coupling-to-ngspice) + - [Dispersive elements](#dispersive-elements) + In the context of transmission line theory, a working definition of each of the terms above is: * A **multiconductor tranmission line** is tranmission line composed of more than one conductor (and the reference conductor), i.e a tranmssion line composed of 3 or more conductors, where one of them is taken as the reference. @@ -137,4 +147,3 @@ Dispersive elements are those whose properties depend on frequency. The solver w - diff --git a/scripts/debug-mpi-gdbserver.sh b/scripts/debug-mpi-gdbserver.sh new file mode 100755 index 00000000..1ef3a38f --- /dev/null +++ b/scripts/debug-mpi-gdbserver.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +set -euo pipefail + +state_dir="${TMPDIR:-/tmp}/semba-fdtd-mpi-gdbserver-${UID}" +pid_file="${state_dir}/pid" +log_file="${state_dir}/output.log" + +print_log() { + if [[ -f "$log_file" ]]; then + while IFS= read -r line; do + printf '%s\n' "$line" >&2 + done < "$log_file" + fi +} + +stop_job() { + local pid cmdline + + if [[ ! -f "$pid_file" ]]; then + return + fi + + IFS= read -r pid < "$pid_file" + if [[ "$pid" =~ ^[1-9][0-9]*$ ]] && kill -0 "$pid" 2>/dev/null; then + cmdline="$(tr '\0' ' ' < "/proc/${pid}/cmdline" 2>/dev/null || true)" + if [[ "$cmdline" != *mpirun* && "$cmdline" != *mpiexec* && "$cmdline" != *prterun* ]]; then + printf 'Refusing to stop unexpected process %s: %s\n' "$pid" "$cmdline" >&2 + return 1 + fi + + kill "$pid" 2>/dev/null || true + for _ in {1..30}; do + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + sleep 0.1 + done + if kill -0 "$pid" 2>/dev/null; then + kill -KILL "$pid" 2>/dev/null || true + fi + fi + + rm -f "$pid_file" +} + +port_is_listening() { + local port_hex table slot local_address remote_address connection_state remainder + + printf -v port_hex '%04X' "$1" + for table in /proc/net/tcp /proc/net/tcp6; do + [[ -r "$table" ]] || continue + while read -r slot local_address remote_address connection_state remainder; do + if [[ "${local_address##*:}" == "$port_hex" && "$connection_state" == "0A" ]]; then + return 0 + fi + done < "$table" + done + return 1 +} + +mkdir -p "$state_dir" + +if [[ "${1:-}" == "--stop" ]]; then + stop_job + exit 0 +fi + +if [[ "${1:-}" == "--workdir" ]]; then + workdir="${2:-}" + if [[ ! -d "$workdir" ]]; then + printf 'MPI debug working directory does not exist: %s\n' "$workdir" >&2 + exit 2 + fi + if [[ ! -w "$workdir" ]]; then + printf 'MPI debug working directory is not writable: %s\n' "$workdir" >&2 + exit 2 + fi + cd "$workdir" + shift 2 +fi + +if [[ "${1:-}" == "--wait-for-port" ]]; then + port="${2:-}" + if ! [[ "$port" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then + printf 'Invalid port: %s\n' "$port" >&2 + exit 2 + fi + + for _ in {1..300}; do + if port_is_listening "$port"; then + printf 'MPI gdbserver ready on port %s\n' "$port" + trap 'exit 0' INT TERM + while true; do + sleep 3600 + done + fi + sleep 0.1 + done + + printf 'Timed out waiting for MPI gdbserver on port %s.\n' "$port" >&2 + exit 1 +fi + +debug_rank="" +foreground=false +foreground_all=false +if [[ "${1:-}" == "--foreground-all" ]]; then + foreground_all=true + shift +elif [[ "${1:-}" == "--foreground-debug-rank" ]]; then + foreground=true + if [[ "$#" -lt 3 ]]; then + printf 'Missing rank after --foreground-debug-rank.\n' >&2 + exit 2 + fi + debug_rank="$2" + shift 2 +elif [[ "${1:-}" == "--debug-rank" ]]; then + if [[ "$#" -lt 3 ]]; then + printf 'Missing rank after --debug-rank.\n' >&2 + exit 2 + fi + debug_rank="$2" + shift 2 +fi + +if [ "$#" -lt 3 ]; then + printf 'Usage: %s [arguments ...]\n' "$0" >&2 + printf ' %s --workdir \n' "$0" >&2 + printf ' %s --debug-rank [arguments ...]\n' "$0" >&2 + printf ' %s --foreground-all [arguments ...]\n' "$0" >&2 + printf ' %s --foreground-debug-rank [arguments ...]\n' "$0" >&2 + printf ' %s --wait-for-port \n' "$0" >&2 + printf ' %s --stop\n' "$0" >&2 + exit 2 +fi + +ranks="$1" +program="$2" +shift 2 + +if ! [[ "$ranks" =~ ^[1-9][0-9]*$ ]]; then + printf 'The number of MPI ranks must be a positive integer: %s\n' "$ranks" >&2 + exit 2 +fi + +if [[ -n "$debug_rank" ]] && { + ! [[ "$debug_rank" =~ ^[0-9]+$ ]] || ((debug_rank >= ranks)); +}; then + printf 'The debug rank must be between 0 and %s: %s\n' "$((ranks - 1))" "$debug_rank" >&2 + exit 2 +fi + +if [[ ! -x "$program" ]]; then + printf 'MPI debug executable is missing or not executable: %s\n' "$program" >&2 + exit 2 +fi + +stop_job + +if [[ "$foreground_all" == true ]]; then + printf '%s\n' "$$" > "$pid_file" + printf 'MPI working directory: %s\n' "$PWD" + printf 'Starting %s MPI gdbserver ranks on ports 20000-%s\n' \ + "$ranks" "$((20000 + ranks - 1))" + exec mpirun --tag-output -np "$ranks" bash -c ' + port=$((20000 + OMPI_COMM_WORLD_RANK)) + exec gdbserver --no-disable-randomization --once ":${port}" "$@" + ' gdbserver-rank "$program" "$@" +fi + +if [[ "$foreground" == true ]]; then + printf '%s\n' "$$" > "$pid_file" + printf 'MPI working directory: %s\n' "$PWD" + printf 'Starting %s MPI ranks with rank %s under gdbserver on port 20000\n' \ + "$ranks" "$debug_rank" + exec mpirun --tag-output -np "$ranks" bash -c ' + debug_rank=$1 + shift + if [[ "$OMPI_COMM_WORLD_RANK" == "$debug_rank" ]]; then + exec gdbserver --no-disable-randomization --once :20000 "$@" + fi + exec "$@" + ' mpi-debug-rank "$debug_rank" "$program" "$@" +fi + +: > "$log_file" + +if [[ -n "$debug_rank" ]]; then + printf 'Starting %s MPI ranks with rank %s under gdbserver on port 20000\n' \ + "$ranks" "$debug_rank" + expected_servers=1 + nohup mpirun --tag-output -np "$ranks" bash -c ' + debug_rank=$1 + shift + if [[ "$OMPI_COMM_WORLD_RANK" == "$debug_rank" ]]; then + exec gdbserver --no-disable-randomization --once :20000 "$@" + fi + exec "$@" + ' mpi-debug-rank "$debug_rank" "$program" "$@" > "$log_file" 2>&1 < /dev/null & +else + printf 'Starting %s MPI gdbserver ranks on ports 20000-%s\n' \ + "$ranks" "$((20000 + ranks - 1))" + expected_servers="$ranks" + nohup mpirun --tag-output -np "$ranks" bash -c ' + port=$((20000 + OMPI_COMM_WORLD_RANK)) + exec gdbserver --no-disable-randomization --once ":${port}" "$@" + ' gdbserver-rank "$program" "$@" > "$log_file" 2>&1 < /dev/null & +fi +mpi_pid=$! +printf '%s\n' "$mpi_pid" > "$pid_file" + +for _ in {1..150}; do + ready=0 + while IFS= read -r line; do + if [[ "$line" == *"Listening on port "* ]]; then + ((ready += 1)) + fi + done < "$log_file" + + if ((ready >= expected_servers)); then + printf 'MPI gdbserver is ready.\n' + exit 0 + fi + + if ! kill -0 "$mpi_pid" 2>/dev/null; then + printf 'MPI gdbserver exited before all ranks were ready.\n' >&2 + print_log + rm -f "$pid_file" + exit 1 + fi + + sleep 0.1 +done + +printf 'Timed out waiting for all MPI gdbserver ranks.\n' >&2 +print_log +stop_job +exit 1