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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions locale/templates/grasslibs.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:00+0000\n"
"POT-Creation-Date: 2026-04-04 16:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -6649,7 +6649,7 @@ msgstr[1] ""
#: ../lib/vector/Vlib/build_sfa.c:665
#, c-format
msgid "One vertex registered"
msgid_plural "%d vertices registered"
msgid_plural "%<PRId64> vertices registered"
msgstr[0] ""
msgstr[1] ""

Expand Down
18 changes: 9 additions & 9 deletions locale/templates/grassmods.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:00+0000\n"
"POT-Creation-Date: 2026-04-04 16:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -4097,7 +4097,6 @@ msgstr ""
msgid "Get text color from cell color value"
msgstr ""

#. GTC Count of window rows
#. GTC Count of raster rows
#. GTC Count of window rows
#: ../display/d.rast.num/main.c:211 ../raster/r.thin/io.c:110
Expand Down Expand Up @@ -6654,7 +6653,7 @@ msgid "create project"
msgstr ""

#: ../general/g.proj/main.c:70
msgid "Prints or modifies GRASS projection information files (in various co-ordinate system descriptions)."
msgid "Prints or modifies GRASS projection information files (in various coordinate system descriptions)."
msgstr ""

#: ../general/g.proj/main.c:72
Expand Down Expand Up @@ -6738,7 +6737,7 @@ msgid "Datum"
msgstr ""

#: ../general/g.proj/main.c:186
msgid "Datum (overrides any datum specified in input co-ordinate system)"
msgid "Datum (overrides any datum specified in input coordinate system)"
msgstr ""

#: ../general/g.proj/main.c:188
Expand All @@ -6755,7 +6754,7 @@ msgid "\"0\" for unspecified or \"-1\" to list and exit"
msgstr ""

#: ../general/g.proj/main.c:205
msgid "Force override of datum transformation information in input co-ordinate system"
msgid "Force override of datum transformation information in input coordinate system"
msgstr ""

#: ../general/g.proj/main.c:210 ../raster/r.null/main.c:75
Expand Down Expand Up @@ -15097,6 +15096,7 @@ msgstr ""
#: ../locale/scriptstrings/t.vect.list_to_translate.c:8
#: ../locale/scriptstrings/t.vect.list_to_translate.c:14
#: ../locale/scriptstrings/t.vect.list_to_translate.c:16
#: ../locale/scriptstrings/t.vect.list_to_translate.c:18
#: ../locale/scriptstrings/t.vect.univar_to_translate.c:9
#: ../locale/scriptstrings/t.vect.univar_to_translate.c:12
#: ../raster/r.kappa/main.c:107 ../raster/r.kappa/main.c:112
Expand Down Expand Up @@ -15129,7 +15129,7 @@ msgstr ""
#: ../locale/scriptstrings/t.rast3d.list_to_translate.c:13
#: ../locale/scriptstrings/t.rast3d.univar_to_translate.c:11
#: ../locale/scriptstrings/t.vect.db.select_to_translate.c:8
#: ../locale/scriptstrings/t.vect.list_to_translate.c:13
#: ../locale/scriptstrings/t.vect.list_to_translate.c:15
#: ../locale/scriptstrings/t.vect.univar_to_translate.c:8
msgid "Field separator character between the output columns"
msgstr ""
Expand Down Expand Up @@ -15636,14 +15636,15 @@ msgid "The granule to be used for listing. The granule must be specified as stri
msgstr ""

#: ../locale/scriptstrings/t.rast.list_to_translate.c:14
#: ../locale/scriptstrings/t.vect.list_to_translate.c:13
msgid "plain;Plain text output;line;Comma separated list of map names;json;JSON (JavaScript Object Notation);yaml;YAML (YAML Ain't Markup Language);csv;CSV (Comma Separated Values);"
msgstr ""

#: ../locale/scriptstrings/t.rast.list_to_translate.c:18
#: ../locale/scriptstrings/t.rast.univar_to_translate.c:16
#: ../locale/scriptstrings/t.rast3d.list_to_translate.c:15
#: ../locale/scriptstrings/t.rast3d.univar_to_translate.c:14
#: ../locale/scriptstrings/t.vect.list_to_translate.c:15
#: ../locale/scriptstrings/t.vect.list_to_translate.c:17
#: ../locale/scriptstrings/t.vect.univar_to_translate.c:11
msgid "Suppress printing of column names"
msgstr ""
Expand Down Expand Up @@ -29882,7 +29883,6 @@ msgstr ""
msgid "Input raster must be of type CELL."
msgstr ""

#. GTC Count of window columns
#. GTC Count of raster columns
#. GTC Count of window columns
#: ../raster/r.thin/io.c:112 ../raster/r.thin/io.c:188
Expand Down Expand Up @@ -42146,7 +42146,7 @@ msgid "centrality measures"
msgstr ""

#: ../vector/v.net.centrality/main.c:100
msgid "Computes degree, centrality, betweeness, closeness and eigenvector centrality measures in the network."
msgid "Computes degree, centrality, betweenness, closeness and eigenvector centrality measures in the network."
msgstr ""

#: ../vector/v.net.centrality/main.c:148
Expand Down
2 changes: 1 addition & 1 deletion locale/templates/grasswxpy.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:00+0000\n"
"POT-Creation-Date: 2026-04-04 16:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down
62 changes: 54 additions & 8 deletions python/grass/script/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ def __del__(self):

class array(np.memmap):
# pylint: disable-next=signature-differs; W0222
def __new__(cls, mapname=None, null=None, dtype=np.double, env=None):
def __new__(cls, mapname=None, null=None, dtype=None, env=None):
"""Define new numpy array

:param cls:
:param dtype: data type (default: numpy.double)
:param dtype: data type (based on map type, fallbacks to numpy.double)
:param env: environment
"""
reg = gcore.region(env=env)
Expand All @@ -144,12 +144,31 @@ def __new__(cls, mapname=None, null=None, dtype=np.double, env=None):

tempfile = _tempfile(env)
if mapname:
if not dtype:
try:
map_type = gcore.parse_command(
"r.info", map=mapname, format="json", env=env
)["datatype"]
if map_type == "CELL":
dtype = np.int32
elif map_type == "FCELL":
dtype = np.float32
elif map_type == "DCELL":
dtype = np.float64
except CalledModuleError:
dtype = np.double
kind = np.dtype(dtype).kind
size = np.dtype(dtype).itemsize

if kind == "f":
flags = "f"
elif kind in "biu":
if size == 8:
message = (
"64-bit integers are not supported by GRASS raster maps. "
"Use dtype=numpy.int32 or a smaller integer type."
)
raise ValueError(message)
flags = "i"
else:
raise ValueError(_("Invalid kind <%s>") % kind)
Expand All @@ -170,7 +189,11 @@ def __new__(cls, mapname=None, null=None, dtype=np.double, env=None):
)

self = np.memmap.__new__(
cls, filename=tempfile.filename, dtype=dtype, mode="r+", shape=shape
cls,
filename=tempfile.filename,
dtype=dtype or np.double,
mode="r+",
shape=shape,
)

self.tempfile = tempfile
Expand Down Expand Up @@ -201,6 +224,14 @@ def write(self, mapname, title=None, null=None, overwrite=None, quiet=None):
raise ValueError(_("Invalid FP size <%d>") % size)
size = None
elif kind in "biu":
if size == 8:
message = (
"64-bit integers are not supported by GRASS raster maps. "
"Cast to a supported type before writing, e.g., "
"array.astype(numpy.int32)"
)
raise ValueError(message)

if size not in {1, 2, 4}:
raise ValueError(_("Invalid integer size <%d>") % size)
flags = None
Expand Down Expand Up @@ -242,21 +273,32 @@ def write(self, mapname, title=None, null=None, overwrite=None, quiet=None):

class array3d(np.memmap):
# pylint: disable-next=signature-differs; W0222
def __new__(cls, mapname=None, null=None, dtype=np.double, env=None):
def __new__(cls, mapname=None, null=None, dtype=None, env=None):
"""Define new 3d numpy array

:param cls:
:param dtype: data type (default: numpy.double)
:param dtype: data type (based on map type, fallbacks to numpy.double)
:param env: environment
"""
reg = gcore.region(True)
reg = gcore.region(True, env=env)
r = reg["rows3"]
c = reg["cols3"]
d = reg["depths"]
shape = (d, r, c)

tempfile = _tempfile()
tempfile = _tempfile(env=env)
if mapname:
if not dtype:
try:
map_type = gcore.parse_command(
"r3.info", map=mapname, format="json", env=env
)["datatype"]
if map_type == "FCELL":
dtype = np.float32
elif map_type == "DCELL":
dtype = np.float64
except CalledModuleError:
dtype = np.double
kind = np.dtype(dtype).kind
size = np.dtype(dtype).itemsize

Expand All @@ -283,7 +325,11 @@ def __new__(cls, mapname=None, null=None, dtype=np.double, env=None):
)

self = np.memmap.__new__(
cls, filename=tempfile.filename, dtype=dtype, mode="r+", shape=shape
cls,
filename=tempfile.filename,
dtype=dtype or np.double,
mode="r+",
shape=shape,
)

self.tempfile = tempfile
Expand Down
144 changes: 144 additions & 0 deletions python/grass/script/tests/grass_script_array_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Tests for grass.script.array"""

import os

import pytest
import numpy as np

import grass.script as gs
from grass.script import array as garray
from grass.tools import Tools


@pytest.fixture
def session_3x4(tmp_path):
"""Set up a GRASS session with a 3x4 region and rasters of different types."""
project = tmp_path / "test_project"
gs.create_project(project)
with (
gs.setup.init(project, env=os.environ.copy()) as session,
Tools(session=session) as tools,
):
tools.g_region(rows=3, cols=4)
tools.r_mapcalc(expression="int_map = int(row() + col())")
tools.r_mapcalc(expression="float_map = float(row() + col())")
tools.r_mapcalc(expression="double_map = double(row() + col())")
yield session


@pytest.fixture
def session_3d(tmp_path):
"""Set up a GRASS session with a 3D region and 3D rasters."""
project = tmp_path / "test_project_3d"
gs.create_project(project)
with (
gs.setup.init(project, env=os.environ.copy()) as session,
Tools(session=session) as tools,
):
tools.g_region(n=2, s=0, e=3, w=0, res3=1, b=0, t=2)
tools.r3_mapcalc(expression="float3d = float(row() + col() + depth())")
tools.r3_mapcalc(expression="double3d = double(row() + col() + depth())")
yield session


class TestArrayDtypeAutoDetection:
"""Test automatic dtype detection when reading raster maps."""

def test_auto_detect_cell(self, session_3x4):
"""Reading a CELL map without dtype should give int32."""
arr = garray.array(mapname="int_map", env=session_3x4.env)
assert arr.dtype == np.int32

def test_auto_detect_fcell(self, session_3x4):
"""Reading an FCELL map without dtype should give float32."""
arr = garray.array(mapname="float_map", env=session_3x4.env)
assert arr.dtype == np.float32

def test_auto_detect_dcell(self, session_3x4):
"""Reading a DCELL map without dtype should give float64."""
arr = garray.array(mapname="double_map", env=session_3x4.env)
assert arr.dtype == np.float64

def test_explicit_dtype_overrides_detection(self, session_3x4):
"""Explicit dtype should override auto-detection."""
arr = garray.array(mapname="int_map", dtype=np.float64, env=session_3x4.env)
assert arr.dtype == np.float64

def test_no_mapname_defaults_to_double(self, session_3x4):
"""Empty array without mapname should default to float64."""
arr = garray.array(env=session_3x4.env)
assert arr.dtype == np.float64


class TestArrayWriteReadRoundTrip:
"""Test that dtype is preserved through write and read-back."""

def test_roundtrip_int(self, session_3x4):
"""Write an int32 array, read it back, check dtype and values."""
arr = garray.array(dtype=np.int32, env=session_3x4.env)
arr[:] = np.arange(12, dtype=np.int32).reshape(3, 4)
arr.write(mapname="roundtrip_int", overwrite=True)

arr2 = garray.array(mapname="roundtrip_int", env=session_3x4.env)
assert arr2.dtype == np.int32
np.testing.assert_array_equal(arr, arr2)

def test_roundtrip_float(self, session_3x4):
"""Write a float32 array, read it back, check dtype and values."""
arr = garray.array(dtype=np.float32, env=session_3x4.env)
arr[:] = np.arange(12, dtype=np.float32).reshape(3, 4) * 0.5
arr.write(mapname="roundtrip_float", overwrite=True)

arr2 = garray.array(mapname="roundtrip_float", env=session_3x4.env)
assert arr2.dtype == np.float32
np.testing.assert_array_almost_equal(arr, arr2)

def test_roundtrip_double(self, session_3x4):
"""Write a float64 array, read it back, check dtype and values."""
arr = garray.array(dtype=np.float64, env=session_3x4.env)
arr[:] = np.arange(12, dtype=np.float64).reshape(3, 4) * 0.1
arr.write(mapname="roundtrip_double", overwrite=True)

arr2 = garray.array(mapname="roundtrip_double", env=session_3x4.env)
assert arr2.dtype == np.float64
np.testing.assert_array_equal(arr, arr2)


class TestArrayInt64Rejected:
"""Test that 64-bit integers are rejected with actionable error messages."""

def test_read_with_int64_dtype_raises(self, session_3x4):
"""Passing dtype=int64 with a mapname should raise ValueError."""
with pytest.raises(ValueError, match="64-bit integers are not supported"):
garray.array(mapname="int_map", dtype=np.int64, env=session_3x4.env)

def test_write_int64_array_raises(self, session_3x4):
"""Writing an int64 array should raise ValueError with cast hint."""
arr = garray.array(dtype=np.int64, env=session_3x4.env)
arr[:] = np.arange(12, dtype=np.int64).reshape(3, 4)
with pytest.raises(ValueError, match=r"array\.astype"):
arr.write(mapname="should_fail", overwrite=True)


class TestArray3dDtypeAutoDetection:
"""Test automatic dtype detection for 3D raster arrays."""

def test_auto_detect_fcell_3d(self, session_3d):
"""Reading an FCELL 3D map without dtype should give float32."""
arr = garray.array3d(mapname="float3d", env=session_3d.env)
assert arr.dtype == np.float32

def test_auto_detect_dcell_3d(self, session_3d):
"""Reading a DCELL 3D map without dtype should give float64."""
arr = garray.array3d(mapname="double3d", env=session_3d.env)
assert arr.dtype == np.float64

def test_explicit_dtype_overrides_3d(self, session_3d):
"""Explicit dtype should override auto-detection for 3D."""
arr = garray.array3d(mapname="double3d", dtype=np.float32, env=session_3d.env)
assert arr.dtype == np.float32

def test_no_mapname_defaults_to_double_3d(self, session_3d):
"""Empty 3D array without mapname should default to float64."""
arr = garray.array3d(env=session_3d.env)
assert arr.dtype == np.float64