Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i.ifft: Add Test Suite #5264

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions imagery/i.ifft/testsuite/test_i_ifft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import numpy as np
from grass.script import array
from grass.gunittest.case import TestCase
from grass.gunittest.main import test


class TestIIFFT(TestCase):
"""Regression tests for the i.ifft GRASS GIS module."""

real_input = "real_input_raster"
imag_input = "imag_input_raster"
output_raster = "output_raster"

@classmethod
def setUpClass(cls):
"""Set up an input raster and configure test environment."""
cls.use_temp_region()
cls.runModule("g.region", n=10, s=0, e=10, w=0, rows=10, cols=10)
cls.temp_rasters = []
cls.runModule(
"r.mapcalc", expression=f"{cls.real_input} = col()", overwrite=True
)
cls.runModule("r.mapcalc", expression=f"{cls.imag_input} = 0", overwrite=True)
cls.temp_rasters.append([cls.real_input, cls.imag_input])

@classmethod
def tearDownClass(cls):
"""Clean up generated data and reset the region."""
for raster in cls.temp_rasters + [cls.output_raster]:
cls.runModule("g.remove", type="raster", name=raster, flags="f")

cls.del_temp_region()

def test_linearity_property(self):
"""Test linearity of the IFFT by combining two rasters."""
self.runModule("r.mapcalc", expression="real_input2 = row()", overwrite=True)
self.runModule("r.mapcalc", expression="imag_input2 = 0", overwrite=True)
self.runModule(
"r.mapcalc",
expression="real_input_sum = real_input_raster + real_input2",
overwrite=True,
)
self.runModule(
"r.mapcalc",
expression="imag_input_sum = imag_input_raster + imag_input2",
overwrite=True,
)
self.temp_rasters.append(
["real_input2", "imag_input2", "real_input_sum", "imag_input_sum"]
)

self.assertModule(
"i.ifft",
real=self.real_input,
imaginary=self.imag_input,
output=self.output_raster,
overwrite=True,
)
self.assertRasterExists(self.output_raster)

self.assertModule(
"i.ifft",
real="real_input2",
imaginary="imag_input2",
output="output_raster2",
overwrite=True,
)
Comment on lines +52 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these 2 i.ifft runs testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two i.ifft runs generate two separate rasters by performing the inverse Fourier transform on different input sets. The outputs are then combined into a third raster (output_raster_sum) using the summed inputs (real_input_sum, imag_input_sum). Finally, the results of output_raster_sum are compared against reference statistics, which were derived using the same method. This test validates the linearity property of the inverse Fourier transform by ensuring that IFFT(A) + IFFT(B) = IFFT(A + B).

self.assertRasterExists("output_raster2")
self.temp_rasters.append("output_raster2")

self.assertModule(
"i.ifft",
real="real_input_sum",
imaginary="imag_input_sum",
output="output_raster_sum",
overwrite=True,
)
self.assertRasterExists("output_raster_sum")
self.temp_rasters.append("output_raster_sum")

reference_stats = {
"min": -5,
"max": 110,
"mean": 1.2,
"stddev": 11.138222,
}

self.assertRasterFitsUnivar(
"output_raster_sum", reference_stats, precision=1e-6
)

def test_scaling_property(self):
"""Test the scaling property of the IFFT: verify that ifft(2.5 * x) == 2.5 * ifft(x)."""
self.assertModule(
"i.ifft",
real=self.real_input,
imaginary=self.imag_input,
output=self.output_raster,
overwrite=True,
)

self.assertRasterExists(self.output_raster)

reference_stats = {
"min": -12.5,
"max": 137.5,
"mean": 1.5,
"stddev": 14.173037,
}

expected_unscaled_stats = {
key: value / 2.5 for key, value in reference_stats.items()
}

self.assertRasterFitsUnivar(
self.output_raster, expected_unscaled_stats, precision=1e-6
)

def test_coefficient_symmetry(self):
"""Test if IFFT preserves expected symmetry in real-valued output."""

self.runModule(
"r.mapcalc",
expression="real_sym = if(col() > 5, 10 - col(), col())",
overwrite=True,
)
self.runModule("r.mapcalc", expression="imag_zero = 0", overwrite=True)
self.temp_rasters.append("real_sym")
self.temp_rasters.append("imag_zero")

self.assertModule(
"i.ifft",
real="real_sym",
imaginary="imag_zero",
output="output_sym",
overwrite=True,
)
self.assertRasterExists("output_sym")
self.temp_rasters.append("output_sym")

self.runModule(
"r.mapcalc", expression="shifted = output_sym[5, 5]", overwrite=True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this test more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks whether i.ifft preserves symmetry in the real-valued output. The input (real_sym) is designed to be symmetric around column 5, and imag_zero ensures there is no imaginary component. I wanted to compare the flipped raster with the raster generated by i.ifft and initially, I attempted to use r.flip to create the shifted raster, but it did not work as expected. So instead, I used shifted = output_sym[5, 5] to obtain a reference value and computed sym_diff = abs(output_sym - shifted) to find deviations from symmetry. Then this is compared against reference statistics which validates that the IFFT output maintains the expected symmetry.

)
self.temp_rasters.append("shifted")

self.runModule(
"r.mapcalc",
expression="sym_diff = abs(output_sym - shifted)",
overwrite=True,
)
self.temp_rasters.append("sym_diff")

reference_stats = {
"min": 0,
"max": 25,
"mean": 1.357770,
"stddev": 5.102593,
}

self.assertRasterFitsUnivar("sym_diff", reference_stats, precision=1e-6)

def test_mask_functionality(self):
"""Test if masking functionality works properly."""

self.runModule(
"r.mapcalc",
expression=f"masked_input = if(row() > 5, {self.real_input}, 0)",
overwrite=True,
)
self.temp_rasters.append("masked_input")
self.assertModule(
"i.ifft",
real="masked_input",
imaginary=self.imag_input,
output=self.output_raster,
overwrite=True,
)

reference_stats = {
"min": -4.236067,
"max": 27.5,
"mean": 0.6,
"stddev": 3.254228,
}

self.assertRasterFitsUnivar(self.output_raster, reference_stats, precision=1e-6)

def test_ifft_reconstruction(self):
"""Test if IFFT reconstructs the original raster."""
self.runModule(
"r.mapcalc",
expression="original = sin(row() * 0.1) + cos(col() * 0.1)",
overwrite=True,
)
self.temp_rasters.append("original")

self.runModule(
"i.fft",
input="original",
real="fft_real",
imaginary="fft_imag",
overwrite=True,
)
self.assertRasterExists("fft_real")
self.assertRasterExists("fft_imag")
self.temp_rasters.append("fft_real")
self.temp_rasters.append("fft_imag")

self.assertModule(
"i.ifft",
real="fft_real",
imaginary="fft_imag",
output="ifft_reconstructed",
overwrite=True,
)
self.assertRasterExists("ifft_reconstructed")
self.temp_rasters.append("ifft_reconstructed")

original_values = array.array("original")
reconstructed_values = array.array("ifft_reconstructed")

self.assertTrue(
np.allclose(original_values, reconstructed_values, atol=1e-6),
"Reconstructed raster does not match the original",
)


if __name__ == "__main__":
test()
Loading