Skip to content
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
311 changes: 311 additions & 0 deletions raster/r.mapcalc/testsuite/test_r_mapcalc_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
#!/usr/bin/env python3

############################################################################
#
# MODULE: test_r_mapcalc_functions.py
# AUTHOR: Saurabh Singh
# PURPOSE: Test math functions, conditionals, and operators in r.mapcalc
# COPYRIGHT: (C) 2026 by Saurabh Singh and the GRASS Development Team
#
# This program is free software under the GNU General Public
# License (>=v2). Read the file COPYING that comes with GRASS
# for details.
#
#############################################################################

from grass.gunittest.case import TestCase
from grass.gunittest.main import test


class TestMathFunctions(TestCase):
"""Test mathematical functions in r.mapcalc"""

output = "test_output"
to_remove = []

@classmethod
def setUpClass(cls):
"""Create test environment"""
cls.use_temp_region()
cls.runModule("g.region", n=3, s=0, e=3, w=0, res=1)

@classmethod
def tearDownClass(cls):
"""Clean up"""
cls.del_temp_region()
if cls.to_remove:
cls.runModule("g.remove", flags="f", type="raster", name=cls.to_remove)

def test_sqrt(self):
"""Test sqrt() function"""
expression = f"{self.output} = sqrt(16)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 4, "range": 0}, precision=1e-6
)

def test_exp(self):
"""Test exp() function"""
expression = f"{self.output} = exp(0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_log(self):
"""Test log() function"""
expression = f"{self.output} = log(2.718281828)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_sin(self):
"""Test sin() function"""
expression = f"{self.output} = sin(0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 0, "range": 0}, precision=1e-6
)

def test_cos(self):
"""Test cos() function"""
expression = f"{self.output} = cos(0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_abs(self):
"""Test abs() function"""
expression = f"{self.output} = abs(-5)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 5, "range": 0}, precision=1e-6
)


class TestConditionals(TestCase):
"""Test conditional statements in r.mapcalc"""

input_map = "test_input"
output = "test_output"
to_remove = []

@classmethod
def setUpClass(cls):
"""Create test data"""
cls.use_temp_region()
cls.runModule("g.region", n=3, s=0, e=3, w=0, res=1)
cls.runModule("r.mapcalc", expression=f"{cls.input_map} = row() * col()")
cls.to_remove.append(cls.input_map)

@classmethod
def tearDownClass(cls):
"""Clean up"""
cls.del_temp_region()
if cls.to_remove:
cls.runModule("g.remove", flags="f", type="raster", name=cls.to_remove)

def test_if_simple(self):
"""Test simple if() statement"""
expression = f"{self.output} = if({self.input_map} > 4, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(self.output, {"min": 0, "max": 1}, precision=1e-6)

def test_if_nested(self):
"""Test nested if() statements"""
expression = f"{self.output} = if({self.input_map} < 3, 1, if({self.input_map} < 6, 2, 3))"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(self.output, {"min": 1, "max": 3}, precision=1e-6)


class TestLogicalOperators(TestCase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test the &&& and ||| operators, they are more tricky. Create suitable input rasters for that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Read the r.mapcalc documentation, the test as you have it now is not actually testing the special behavior of the &&& and ||| operators.

"""Test logical operators in r.mapcalc"""

input_a = "test_input_a"
input_b = "test_input_b"
output = "test_output"
to_remove = []

@classmethod
def setUpClass(cls):
"""Create test environment with input rasters"""
cls.use_temp_region()
cls.runModule("g.region", n=3, s=0, e=3, w=0, res=1)
cls.runModule("r.mapcalc", expression=f"{cls.input_a} = if(row() > 1, 1, 0)")
cls.runModule("r.mapcalc", expression=f"{cls.input_b} = if(col() > 1, 1, 0)")
cls.to_remove.extend([cls.input_a, cls.input_b])

@classmethod
def tearDownClass(cls):
"""Clean up"""
cls.del_temp_region()
if cls.to_remove:
cls.runModule("g.remove", flags="f", type="raster", name=cls.to_remove)

def test_and_operator(self):
"""Test && (AND) operator"""
expression = f"{self.output} = if(1 && 1, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_or_operator(self):
"""Test || (OR) operator"""
expression = f"{self.output} = if(0 || 1, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_not_operator(self):
"""Test ! (NOT) operator"""
expression = f"{self.output} = if(!0, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_and_and_and_operator_special_behavior(self):
"""Test &&& operator handles NULL differently than &&

Regular && returns NULL if any operand is NULL.
Special &&& returns 0 if one operand is 0, even if the other is NULL.
"""
# Create NULL and Zero maps
null_map = "test_null_and"
zero_map = "test_zero"
self.runModule("r.mapcalc", expression=f"{null_map} = null()")
self.runModule("r.mapcalc", expression=f"{zero_map} = 0")
self.to_remove.extend([null_map, zero_map])

# Test &&& (Special)
# null &&& 0 should be 0
expression = f"{self.output} = {null_map} &&& {zero_map}"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 0, "range": 0}, precision=1e-6
)

def test_or_or_or_operator_special_behavior(self):
"""Test ||| operator handles NULL differently than ||

Regular || returns NULL if any operand is NULL.
Special ||| returns 1 if one operand is 1, even if the other is NULL.
"""
# Create NULL and One maps
null_map = "test_null_or"
one_map = "test_one"
self.runModule("r.mapcalc", expression=f"{null_map} = null()")
self.runModule("r.mapcalc", expression=f"{one_map} = 1")
self.to_remove.extend([null_map, one_map])

# Test ||| (Special)
# null ||| 1 should be 1
expression = f"{self.output} = {null_map} ||| {one_map}"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)


class TestComparisonOperators(TestCase):
"""Test comparison operators in r.mapcalc"""

output = "test_output"
to_remove = []

@classmethod
def setUpClass(cls):
"""Create test environment"""
cls.use_temp_region()
cls.runModule("g.region", n=3, s=0, e=3, w=0, res=1)

@classmethod
def tearDownClass(cls):
"""Clean up"""
cls.del_temp_region()
if cls.to_remove:
cls.runModule("g.remove", flags="f", type="raster", name=cls.to_remove)

def test_greater_than(self):
"""Test > operator"""
expression = f"{self.output} = if(5 > 3, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_less_than(self):
"""Test < operator"""
expression = f"{self.output} = if(3 < 5, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_equal(self):
"""Test == operator"""
expression = f"{self.output} = if(5 == 5, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_not_equal(self):
"""Test != operator"""
expression = f"{self.output} = if(5 != 3, 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)


class TestNullHandling(TestCase):
"""Test null value handling in r.mapcalc"""

null_map = "test_null"
output = "test_output"
to_remove = []

@classmethod
def setUpClass(cls):
"""Create test data with nulls"""
cls.use_temp_region()
cls.runModule("g.region", n=3, s=0, e=3, w=0, res=1)
# Create map with null values
cls.runModule("r.mapcalc", expression=f"{cls.null_map} = null()")
cls.to_remove.append(cls.null_map)

@classmethod
def tearDownClass(cls):
"""Clean up"""
cls.del_temp_region()
if cls.to_remove:
cls.runModule("g.remove", flags="f", type="raster", name=cls.to_remove)

def test_isnull(self):
"""Test isnull() function"""
expression = f"{self.output} = if(isnull({self.null_map}), 1, 0)"
self.assertModule("r.mapcalc", expression=expression, overwrite=True)
self.to_remove.append(self.output)
self.assertRasterFitsUnivar(
self.output, {"mean": 1, "range": 0}, precision=1e-6
)

def test_null_creation(self):
"""Test null() creates a null raster"""
self.assertRasterFitsUnivar(self.null_map, {"n": 0}, precision=1e-6)


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