HeightMap: add threshold operations that return new HeightMaps (closes #197)
Add three methods that create NEW HeightMap objects: - threshold(range): preserve original values where in range, 0.0 elsewhere - threshold_binary(range, value=1.0): set uniform value where in range - inverse(): return (1.0 - value) for each cell These operations are immutable - they preserve the original HeightMap. Useful for masking operations with Grid.apply_threshold/apply_ranges. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b98b2be012
commit
d92d5f0274
3 changed files with 463 additions and 0 deletions
|
|
@ -119,6 +119,32 @@ PyMethodDef PyHeightMap::methods[] = {
|
||||||
MCRF_RETURNS("int: Number of cells with values in range")
|
MCRF_RETURNS("int: Number of cells with values in range")
|
||||||
MCRF_RAISES("ValueError", "min > max")
|
MCRF_RAISES("ValueError", "min > max")
|
||||||
)},
|
)},
|
||||||
|
// Threshold operations (#197) - return NEW HeightMaps
|
||||||
|
{"threshold", (PyCFunction)PyHeightMap::threshold, METH_VARARGS,
|
||||||
|
MCRF_METHOD(HeightMap, threshold,
|
||||||
|
MCRF_SIG("(range: tuple[float, float])", "HeightMap"),
|
||||||
|
MCRF_DESC("Return NEW HeightMap with original values where in range, 0.0 elsewhere."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("range", "Value range as (min, max) tuple or list, inclusive")
|
||||||
|
MCRF_RETURNS("HeightMap: New HeightMap (original is unchanged)")
|
||||||
|
MCRF_RAISES("ValueError", "min > max")
|
||||||
|
)},
|
||||||
|
{"threshold_binary", (PyCFunction)PyHeightMap::threshold_binary, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(HeightMap, threshold_binary,
|
||||||
|
MCRF_SIG("(range: tuple[float, float], value: float = 1.0)", "HeightMap"),
|
||||||
|
MCRF_DESC("Return NEW HeightMap with uniform value where in range, 0.0 elsewhere."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("range", "Value range as (min, max) tuple or list, inclusive")
|
||||||
|
MCRF_ARG("value", "Value to set for cells in range (default 1.0)")
|
||||||
|
MCRF_RETURNS("HeightMap: New HeightMap (original is unchanged)")
|
||||||
|
MCRF_RAISES("ValueError", "min > max")
|
||||||
|
)},
|
||||||
|
{"inverse", (PyCFunction)PyHeightMap::inverse, METH_NOARGS,
|
||||||
|
MCRF_METHOD(HeightMap, inverse,
|
||||||
|
MCRF_SIG("()", "HeightMap"),
|
||||||
|
MCRF_DESC("Return NEW HeightMap with (1.0 - value) for each cell."),
|
||||||
|
MCRF_RETURNS("HeightMap: New inverted HeightMap (original is unchanged)")
|
||||||
|
)},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -554,3 +580,181 @@ PyObject* PyHeightMap::subscript(PyHeightMapObject* self, PyObject* key)
|
||||||
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||||
return PyFloat_FromDouble(value);
|
return PyFloat_FromDouble(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Threshold operations (#197) - return NEW HeightMaps
|
||||||
|
|
||||||
|
// Helper: Parse range from tuple or list
|
||||||
|
static bool ParseRange(PyObject* range_obj, float* min_val, float* max_val)
|
||||||
|
{
|
||||||
|
if (PyTuple_Check(range_obj) && PyTuple_Size(range_obj) == 2) {
|
||||||
|
PyObject* min_obj = PyTuple_GetItem(range_obj, 0);
|
||||||
|
PyObject* max_obj = PyTuple_GetItem(range_obj, 1);
|
||||||
|
if (PyFloat_Check(min_obj)) *min_val = (float)PyFloat_AsDouble(min_obj);
|
||||||
|
else if (PyLong_Check(min_obj)) *min_val = (float)PyLong_AsLong(min_obj);
|
||||||
|
else { PyErr_SetString(PyExc_TypeError, "range values must be numeric"); return false; }
|
||||||
|
if (PyFloat_Check(max_obj)) *max_val = (float)PyFloat_AsDouble(max_obj);
|
||||||
|
else if (PyLong_Check(max_obj)) *max_val = (float)PyLong_AsLong(max_obj);
|
||||||
|
else { PyErr_SetString(PyExc_TypeError, "range values must be numeric"); return false; }
|
||||||
|
} else if (PyList_Check(range_obj) && PyList_Size(range_obj) == 2) {
|
||||||
|
PyObject* min_obj = PyList_GetItem(range_obj, 0);
|
||||||
|
PyObject* max_obj = PyList_GetItem(range_obj, 1);
|
||||||
|
if (PyFloat_Check(min_obj)) *min_val = (float)PyFloat_AsDouble(min_obj);
|
||||||
|
else if (PyLong_Check(min_obj)) *min_val = (float)PyLong_AsLong(min_obj);
|
||||||
|
else { PyErr_SetString(PyExc_TypeError, "range values must be numeric"); return false; }
|
||||||
|
if (PyFloat_Check(max_obj)) *max_val = (float)PyFloat_AsDouble(max_obj);
|
||||||
|
else if (PyLong_Check(max_obj)) *max_val = (float)PyLong_AsLong(max_obj);
|
||||||
|
else { PyErr_SetString(PyExc_TypeError, "range values must be numeric"); return false; }
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "range must be a tuple or list of (min, max)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*min_val > *max_val) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "range min must be less than or equal to max");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !PyErr_Occurred();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Create a new HeightMap object with same dimensions
|
||||||
|
static PyHeightMapObject* CreateNewHeightMap(int width, int height)
|
||||||
|
{
|
||||||
|
// Get the HeightMap type from the module
|
||||||
|
PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap");
|
||||||
|
if (!heightmap_type) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create size tuple
|
||||||
|
PyObject* size_tuple = Py_BuildValue("(ii)", width, height);
|
||||||
|
if (!size_tuple) {
|
||||||
|
Py_DECREF(heightmap_type);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create args tuple containing the size tuple
|
||||||
|
PyObject* args = PyTuple_Pack(1, size_tuple);
|
||||||
|
Py_DECREF(size_tuple);
|
||||||
|
if (!args) {
|
||||||
|
Py_DECREF(heightmap_type);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new object
|
||||||
|
PyHeightMapObject* new_hmap = (PyHeightMapObject*)PyObject_Call(heightmap_type, args, nullptr);
|
||||||
|
Py_DECREF(args);
|
||||||
|
Py_DECREF(heightmap_type);
|
||||||
|
|
||||||
|
if (!new_hmap) {
|
||||||
|
return nullptr; // Python error already set
|
||||||
|
}
|
||||||
|
|
||||||
|
return new_hmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method: threshold(range) -> HeightMap
|
||||||
|
PyObject* PyHeightMap::threshold(PyHeightMapObject* self, PyObject* args)
|
||||||
|
{
|
||||||
|
PyObject* range_obj = nullptr;
|
||||||
|
if (!PyArg_ParseTuple(args, "O", &range_obj)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
float min_val, max_val;
|
||||||
|
if (!ParseRange(range_obj, &min_val, &max_val)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new HeightMap with same dimensions
|
||||||
|
PyHeightMapObject* result = CreateNewHeightMap(self->heightmap->w, self->heightmap->h);
|
||||||
|
if (!result) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy values that are in range, leave others as 0.0
|
||||||
|
for (int y = 0; y < self->heightmap->h; y++) {
|
||||||
|
for (int x = 0; x < self->heightmap->w; x++) {
|
||||||
|
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||||
|
if (value >= min_val && value <= max_val) {
|
||||||
|
TCOD_heightmap_set_value(result->heightmap, x, y, value);
|
||||||
|
}
|
||||||
|
// else: already 0.0 from initialization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method: threshold_binary(range, value=1.0) -> HeightMap
|
||||||
|
PyObject* PyHeightMap::threshold_binary(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* keywords[] = {"range", "value", nullptr};
|
||||||
|
PyObject* range_obj = nullptr;
|
||||||
|
float set_value = 1.0f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(keywords),
|
||||||
|
&range_obj, &set_value)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
float min_val, max_val;
|
||||||
|
if (!ParseRange(range_obj, &min_val, &max_val)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new HeightMap with same dimensions
|
||||||
|
PyHeightMapObject* result = CreateNewHeightMap(self->heightmap->w, self->heightmap->h);
|
||||||
|
if (!result) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uniform value where in range, leave others as 0.0
|
||||||
|
for (int y = 0; y < self->heightmap->h; y++) {
|
||||||
|
for (int x = 0; x < self->heightmap->w; x++) {
|
||||||
|
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||||
|
if (value >= min_val && value <= max_val) {
|
||||||
|
TCOD_heightmap_set_value(result->heightmap, x, y, set_value);
|
||||||
|
}
|
||||||
|
// else: already 0.0 from initialization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method: inverse() -> HeightMap
|
||||||
|
PyObject* PyHeightMap::inverse(PyHeightMapObject* self, PyObject* Py_UNUSED(args))
|
||||||
|
{
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new HeightMap with same dimensions
|
||||||
|
PyHeightMapObject* result = CreateNewHeightMap(self->heightmap->w, self->heightmap->h);
|
||||||
|
if (!result) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set (1.0 - value) for each cell
|
||||||
|
for (int y = 0; y < self->heightmap->h; y++) {
|
||||||
|
for (int x = 0; x < self->heightmap->w; x++) {
|
||||||
|
float value = TCOD_heightmap_get_value(self->heightmap, x, y);
|
||||||
|
TCOD_heightmap_set_value(result->heightmap, x, y, 1.0f - value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ public:
|
||||||
static PyObject* min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args));
|
static PyObject* min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args));
|
||||||
static PyObject* count_in_range(PyHeightMapObject* self, PyObject* args);
|
static PyObject* count_in_range(PyHeightMapObject* self, PyObject* args);
|
||||||
|
|
||||||
|
// Threshold operations (#197) - return NEW HeightMaps
|
||||||
|
static PyObject* threshold(PyHeightMapObject* self, PyObject* args);
|
||||||
|
static PyObject* threshold_binary(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* inverse(PyHeightMapObject* self, PyObject* Py_UNUSED(args));
|
||||||
|
|
||||||
// Subscript support for hmap[x, y] syntax
|
// Subscript support for hmap[x, y] syntax
|
||||||
static PyObject* subscript(PyHeightMapObject* self, PyObject* key);
|
static PyObject* subscript(PyHeightMapObject* self, PyObject* key);
|
||||||
|
|
||||||
|
|
|
||||||
254
tests/unit/test_heightmap_threshold.py
Normal file
254
tests/unit/test_heightmap_threshold.py
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Unit tests for mcrfpy.HeightMap threshold operations (#197)
|
||||||
|
|
||||||
|
Tests the HeightMap threshold methods: threshold, threshold_binary, inverse
|
||||||
|
These methods return NEW HeightMap objects, preserving the original.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_basic():
|
||||||
|
"""threshold() returns new HeightMap with values in range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold((0.4, 0.6))
|
||||||
|
|
||||||
|
# Result should have values (all 0.5 are in range)
|
||||||
|
assert abs(result[5, 5] - 0.5) < 0.001, f"Expected 0.5, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_basic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_preserves_original():
|
||||||
|
"""threshold() does not modify original"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
original_value = hmap[5, 5]
|
||||||
|
|
||||||
|
_ = hmap.threshold((0.0, 0.3)) # Range excludes 0.5
|
||||||
|
|
||||||
|
# Original should be unchanged
|
||||||
|
assert abs(hmap[5, 5] - original_value) < 0.001, "Original was modified!"
|
||||||
|
print("PASS: test_threshold_preserves_original")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_returns_new():
|
||||||
|
"""threshold() returns a different object"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold((0.0, 1.0))
|
||||||
|
|
||||||
|
assert result is not hmap, "threshold should return a new HeightMap"
|
||||||
|
print("PASS: test_threshold_returns_new")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_out_of_range():
|
||||||
|
"""threshold() sets values outside range to 0.0"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold((0.6, 1.0)) # Excludes 0.5
|
||||||
|
|
||||||
|
# All values should be 0.0 since 0.5 is not in [0.6, 1.0]
|
||||||
|
assert abs(result[5, 5]) < 0.001, f"Expected 0.0, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_out_of_range")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_preserves_values():
|
||||||
|
"""threshold() preserves original values (not just 1.0)"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10))
|
||||||
|
|
||||||
|
# Set different values manually using scalar ops
|
||||||
|
hmap.fill(0.0)
|
||||||
|
|
||||||
|
# We can't set individual values, so let's test with uniform map
|
||||||
|
# and verify the value is preserved, not converted to 1.0
|
||||||
|
hmap2 = mcrfpy.HeightMap((10, 10), fill=0.75)
|
||||||
|
result = hmap2.threshold((0.5, 1.0))
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 0.75) < 0.001, f"Expected 0.75, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_preserves_values")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_invalid_range():
|
||||||
|
"""threshold() raises ValueError for invalid range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hmap.threshold((1.0, 0.0)) # min > max
|
||||||
|
print("FAIL: test_threshold_invalid_range - should have raised ValueError")
|
||||||
|
sys.exit(1)
|
||||||
|
except ValueError as e:
|
||||||
|
assert "min" in str(e).lower()
|
||||||
|
|
||||||
|
print("PASS: test_threshold_invalid_range")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_accepts_list():
|
||||||
|
"""threshold() accepts list as range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold([0.4, 0.6]) # List instead of tuple
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 0.5) < 0.001
|
||||||
|
print("PASS: test_threshold_accepts_list")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_binary_basic():
|
||||||
|
"""threshold_binary() sets uniform value in range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold_binary((0.4, 0.6))
|
||||||
|
|
||||||
|
# Default value is 1.0
|
||||||
|
assert abs(result[5, 5] - 1.0) < 0.001, f"Expected 1.0, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_binary_basic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_binary_custom_value():
|
||||||
|
"""threshold_binary() uses custom value"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold_binary((0.4, 0.6), value=0.8)
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 0.8) < 0.001, f"Expected 0.8, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_binary_custom_value")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_binary_out_of_range():
|
||||||
|
"""threshold_binary() sets 0.0 for values outside range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.threshold_binary((0.6, 1.0))
|
||||||
|
|
||||||
|
# 0.5 is not in [0.6, 1.0], so result should be 0.0
|
||||||
|
assert abs(result[5, 5]) < 0.001, f"Expected 0.0, got {result[5, 5]}"
|
||||||
|
print("PASS: test_threshold_binary_out_of_range")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_binary_preserves_original():
|
||||||
|
"""threshold_binary() does not modify original"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
_ = hmap.threshold_binary((0.0, 1.0), value=0.0)
|
||||||
|
|
||||||
|
assert abs(hmap[5, 5] - 0.5) < 0.001, "Original was modified!"
|
||||||
|
print("PASS: test_threshold_binary_preserves_original")
|
||||||
|
|
||||||
|
|
||||||
|
def test_threshold_binary_invalid_range():
|
||||||
|
"""threshold_binary() raises ValueError for invalid range"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
hmap.threshold_binary((1.0, 0.0)) # min > max
|
||||||
|
print("FAIL: test_threshold_binary_invalid_range - should have raised ValueError")
|
||||||
|
sys.exit(1)
|
||||||
|
except ValueError as e:
|
||||||
|
assert "min" in str(e).lower()
|
||||||
|
|
||||||
|
print("PASS: test_threshold_binary_invalid_range")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_basic():
|
||||||
|
"""inverse() returns (1.0 - value) for each cell"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.3)
|
||||||
|
result = hmap.inverse()
|
||||||
|
|
||||||
|
expected = 1.0 - 0.3
|
||||||
|
assert abs(result[5, 5] - expected) < 0.001, f"Expected {expected}, got {result[5, 5]}"
|
||||||
|
print("PASS: test_inverse_basic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_preserves_original():
|
||||||
|
"""inverse() does not modify original"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.3)
|
||||||
|
_ = hmap.inverse()
|
||||||
|
|
||||||
|
assert abs(hmap[5, 5] - 0.3) < 0.001, "Original was modified!"
|
||||||
|
print("PASS: test_inverse_preserves_original")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_returns_new():
|
||||||
|
"""inverse() returns a different object"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.inverse()
|
||||||
|
|
||||||
|
assert result is not hmap, "inverse should return a new HeightMap"
|
||||||
|
print("PASS: test_inverse_returns_new")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_zero():
|
||||||
|
"""inverse() of 0.0 is 1.0"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.0)
|
||||||
|
result = hmap.inverse()
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 1.0) < 0.001, f"Expected 1.0, got {result[5, 5]}"
|
||||||
|
print("PASS: test_inverse_zero")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_one():
|
||||||
|
"""inverse() of 1.0 is 0.0"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=1.0)
|
||||||
|
result = hmap.inverse()
|
||||||
|
|
||||||
|
assert abs(result[5, 5]) < 0.001, f"Expected 0.0, got {result[5, 5]}"
|
||||||
|
print("PASS: test_inverse_one")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inverse_half():
|
||||||
|
"""inverse() of 0.5 is 0.5"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
||||||
|
result = hmap.inverse()
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 0.5) < 0.001, f"Expected 0.5, got {result[5, 5]}"
|
||||||
|
print("PASS: test_inverse_half")
|
||||||
|
|
||||||
|
|
||||||
|
def test_double_inverse():
|
||||||
|
"""double inverse() returns to original value"""
|
||||||
|
hmap = mcrfpy.HeightMap((10, 10), fill=0.7)
|
||||||
|
result = hmap.inverse().inverse()
|
||||||
|
|
||||||
|
assert abs(result[5, 5] - 0.7) < 0.001, f"Expected 0.7, got {result[5, 5]}"
|
||||||
|
print("PASS: test_double_inverse")
|
||||||
|
|
||||||
|
|
||||||
|
def test_size_preserved():
|
||||||
|
"""threshold operations preserve HeightMap size"""
|
||||||
|
hmap = mcrfpy.HeightMap((15, 20), fill=0.5)
|
||||||
|
|
||||||
|
result1 = hmap.threshold((0.0, 1.0))
|
||||||
|
result2 = hmap.threshold_binary((0.0, 1.0))
|
||||||
|
result3 = hmap.inverse()
|
||||||
|
|
||||||
|
assert result1.size == (15, 20), f"threshold size mismatch: {result1.size}"
|
||||||
|
assert result2.size == (15, 20), f"threshold_binary size mismatch: {result2.size}"
|
||||||
|
assert result3.size == (15, 20), f"inverse size mismatch: {result3.size}"
|
||||||
|
print("PASS: test_size_preserved")
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all tests"""
|
||||||
|
print("Running HeightMap threshold operation tests (#197)...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
test_threshold_basic()
|
||||||
|
test_threshold_preserves_original()
|
||||||
|
test_threshold_returns_new()
|
||||||
|
test_threshold_out_of_range()
|
||||||
|
test_threshold_preserves_values()
|
||||||
|
test_threshold_invalid_range()
|
||||||
|
test_threshold_accepts_list()
|
||||||
|
test_threshold_binary_basic()
|
||||||
|
test_threshold_binary_custom_value()
|
||||||
|
test_threshold_binary_out_of_range()
|
||||||
|
test_threshold_binary_preserves_original()
|
||||||
|
test_threshold_binary_invalid_range()
|
||||||
|
test_inverse_basic()
|
||||||
|
test_inverse_preserves_original()
|
||||||
|
test_inverse_returns_new()
|
||||||
|
test_inverse_zero()
|
||||||
|
test_inverse_one()
|
||||||
|
test_inverse_half()
|
||||||
|
test_double_inverse()
|
||||||
|
test_size_preserved()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("All HeightMap threshold operation tests PASSED!")
|
||||||
|
|
||||||
|
|
||||||
|
# Run tests directly
|
||||||
|
run_all_tests()
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue