diff --git a/.bumpversion.toml b/.bumpversion.toml deleted file mode 100644 index a0b9821..0000000 --- a/.bumpversion.toml +++ /dev/null @@ -1,10 +0,0 @@ -[tool.bumpversion] -current_version = "0.41.1.dev1" -parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.dev(?P\\d+))?" -serialize = ["{major}.{minor}.{patch}.dev{dev}"] - -[[tool.bumpversion.files]] -filename = "src/atlas4py/_version.py" - -[[tool.bumpversion.files]] -filename = "pyproject.toml" diff --git a/.gitignore b/.gitignore index f0d3dfa..d90f810 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ __pycache__/ *$py.class # C extensions -*.cmake *.so # Distribution / packaging diff --git a/examples/Atlas4Py-Test.ipynb b/examples/Atlas4Py-Test.ipynb index 3017e6e..2ef9bdf 100644 --- a/examples/Atlas4Py-Test.ipynb +++ b/examples/Atlas4Py-Test.ipynb @@ -25,7 +25,7 @@ "metadata": {}, "outputs": [], "source": [ - "import _atlas4py" + "import atlas4py" ] }, { @@ -37,22 +37,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "_atlas4py.Grid({'domain': {'type': 'rectangular', 'units': 'degrees', 'xmax': 1.0, 'xmin': -1.0, 'ymax': 1.0, 'ymin': -1.0}, 'projection': {'type': 'lonlat'}, 'type': 'structured', 'xspace': {'N': 20, 'end': 1.0, 'endpoint': True, 'start': -1.0, 'type': 'linear'}, 'yspace': {'N': 21, 'end': 1.0, 'endpoint': True, 'start': -1.0, 'type': 'linear'}})\n", - "<_atlas4py.functionspace.CellColumns object at 0x7f32bfd1efb0>\n" + "_atlas4py.Grid({'type': 'structured', 'xspace': {'type': 'linear', 'start': -1.0, 'end': 1.0, 'N': 20, 'endpoint': True}, 'yspace': {'type': 'linear', 'start': -1.0, 'end': 1.0, 'N': 21, 'endpoint': True}, 'domain': {'type': 'rectangular', 'xmin': -1.0, 'xmax': 1.0, 'ymin': -1.0, 'ymax': 1.0, 'units': 'degrees'}, 'projection': {'type': 'lonlat'}})\n", + "\n" ] } ], "source": [ - "#grid = _atlas4py.StructuredGrid(\"L32x32\")\n", - "#grid = _atlas4py.StructuredGrid(x_spacings=[_atlas4py.LinearSpacing(-1, 1, 21)]*15 + [_atlas4py.LinearSpacing(-1, 1, 15)]*6,\n", - "# y_spacing=_atlas4py.LinearSpacing(-1, 1, 21))\n", - "grid = _atlas4py.StructuredGrid(x_spacing=_atlas4py.LinearSpacing(-1, 1, 20),\n", - " y_spacing=_atlas4py.LinearSpacing(-1, 1, 21))\n", + "#grid = atlas4py.StructuredGrid(\"L32x32\")\n", + "#grid = atlas4py.StructuredGrid(x_spacings=[atlas4py.LinearSpacing(-1, 1, 21)]*15 + [atlas4py.LinearSpacing(-1, 1, 15)]*6,\n", + "# y_spacing=atlas4py.LinearSpacing(-1, 1, 21))\n", + "grid = atlas4py.StructuredGrid(x_spacing=atlas4py.LinearSpacing(-1, 1, 20),\n", + " y_spacing=atlas4py.LinearSpacing(-1, 1, 21))\n", "print(grid)\n", - "mesh = _atlas4py.StructuredMeshGenerator().generate(grid)\n", - "_atlas4py.build_edges(mesh)\n", - "_atlas4py.build_node_to_edge_connectivity(mesh)\n", - "fs = _atlas4py.functionspace.CellColumns(mesh)\n", + "mesh = atlas4py.StructuredMeshGenerator().generate(grid)\n", + "atlas4py.build_edges(mesh)\n", + "atlas4py.build_node_to_edge_connectivity(mesh)\n", + "fs = atlas4py.functionspace.CellColumns(mesh)\n", "print(fs)" ] }, @@ -79,8 +79,8 @@ "metadata": {}, "outputs": [], "source": [ - "out_view = np.array(out_f, copy=False)\n", - "in_view = np.array(in_f, copy=False)\n", + "out_view = np.asarray(out_f)\n", + "in_view = np.asarray(in_f)\n", "out_view[:] = in_view[:] = np.zeros_like(in_view)" ] }, @@ -90,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "lonlat_view = np.array(mesh.nodes.lonlat, copy=False)\n", + "lonlat_view = np.asarray(mesh.nodes.lonlat)\n", "lonlat0 = lonlat_view[0]\n", "lonlat1 = lonlat_view[-1]" ] @@ -133,7 +133,7 @@ "source": [ "!rm out.msh\n", "import computation\n", - "with _atlas4py.Gmsh(\"out.msh\") as out:\n", + "with atlas4py.Gmsh(\"out.msh\") as out:\n", " out.write(mesh)\n", " for i in range(100):\n", " in_f.metadata[\"step\"] = i\n", @@ -156,12 +156,12 @@ "metadata": {}, "outputs": [], "source": [ - "grid = _atlas4py.StructuredGrid(x_spacing=_atlas4py.LinearSpacing(-1, 1, 5),\n", - " y_spacing=_atlas4py.LinearSpacing(-1, 1, 5))\n", - "mesh = _atlas4py.StructuredMeshGenerator().generate(grid)\n", - "_atlas4py.build_edges(mesh)\n", - "_atlas4py.build_node_to_edge_connectivity(mesh)\n", - "fs = _atlas4py.functionspace.CellColumns(mesh, halo=2)" + "grid = atlas4py.StructuredGrid(x_spacing=atlas4py.LinearSpacing(-1, 1, 5),\n", + " y_spacing=atlas4py.LinearSpacing(-1, 1, 5))\n", + "mesh = atlas4py.StructuredMeshGenerator().generate(grid)\n", + "atlas4py.build_edges(mesh)\n", + "atlas4py.build_node_to_edge_connectivity(mesh)\n", + "fs = atlas4py.functionspace.CellColumns(mesh, halo=2)" ] }, { @@ -265,7 +265,7 @@ } ], "source": [ - "lonlat_view = np.array(mesh.nodes.lonlat, copy=False)\n", + "lonlat_view = np.asarray(mesh.nodes.lonlat)\n", "lonlat_view" ] }, @@ -283,45 +283,45 @@ "block: 0\n", "40 x 2\n", "[[5, 0],\n", - " [5, 6],\n", - " [6, 1],\n", " [0, 1],\n", - " [6, 7],\n", - " [7, 2],\n", + " [6, 1],\n", + " [5, 6],\n", " [1, 2],\n", - " [7, 8],\n", - " [8, 3],\n", + " [7, 2],\n", + " [6, 7],\n", " [2, 3],\n", - " [8, 9],\n", - " [9, 4],\n", + " [8, 3],\n", + " [7, 8],\n", " [3, 4],\n", + " [9, 4],\n", + " [8, 9],\n", " [10, 5],\n", - " [10, 11],\n", " [11, 6],\n", - " [11, 12],\n", + " [10, 11],\n", " [12, 7],\n", - " [12, 13],\n", + " [11, 12],\n", " [13, 8],\n", - " [13, 14],\n", + " [12, 13],\n", " [14, 9],\n", + " [13, 14],\n", " [15, 10],\n", - " [15, 16],\n", " [16, 11],\n", - " [16, 17],\n", + " [15, 16],\n", " [17, 12],\n", - " [17, 18],\n", + " [16, 17],\n", " [18, 13],\n", - " [18, 19],\n", + " [17, 18],\n", " [19, 14],\n", + " [18, 19],\n", " [20, 15],\n", - " [20, 21],\n", " [21, 16],\n", - " [21, 22],\n", + " [20, 21],\n", " [22, 17],\n", - " [22, 23],\n", + " [21, 22],\n", " [23, 18],\n", - " [23, 24],\n", - " [24, 19]]\n", + " [22, 23],\n", + " [24, 19],\n", + " [23, 24]]\n", "--- \n" ] } @@ -358,22 +358,22 @@ "\n", "block: 0\n", "16 x 4\n", - "[[0, 5, 6, 1],\n", - " [1, 6, 7, 2],\n", - " [2, 7, 8, 3],\n", - " [3, 8, 9, 4],\n", - " [5, 10, 11, 6],\n", - " [6, 11, 12, 7],\n", - " [7, 12, 13, 8],\n", - " [8, 13, 14, 9],\n", - " [10, 15, 16, 11],\n", - " [11, 16, 17, 12],\n", - " [12, 17, 18, 13],\n", - " [13, 18, 19, 14],\n", - " [15, 20, 21, 16],\n", - " [16, 21, 22, 17],\n", - " [17, 22, 23, 18],\n", - " [18, 23, 24, 19]]\n", + "[[5, 0, 1, 6],\n", + " [6, 1, 2, 7],\n", + " [7, 2, 3, 8],\n", + " [8, 3, 4, 9],\n", + " [10, 5, 6, 11],\n", + " [11, 6, 7, 12],\n", + " [12, 7, 8, 13],\n", + " [13, 8, 9, 14],\n", + " [15, 10, 11, 16],\n", + " [16, 11, 12, 17],\n", + " [17, 12, 13, 18],\n", + " [18, 13, 14, 19],\n", + " [20, 15, 16, 21],\n", + " [21, 16, 17, 22],\n", + " [22, 17, 18, 23],\n", + " [23, 18, 19, 24]]\n", "--- \n", "\n", "block: 1\n", @@ -452,31 +452,31 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[0, 3],\n", - " [2, 3, 6],\n", - " [5, 6, 9],\n", - " [8, 9, 12],\n", - " [11, 12],\n", - " [13, 1, 0],\n", - " [15, 1, 4, 2],\n", - " [17, 4, 7, 5],\n", - " [19, 7, 10, 8],\n", - " [21, 10, 11],\n", - " [22, 14, 13],\n", - " [24, 14, 16, 15],\n", - " [26, 16, 18, 17],\n", - " [28, 18, 20, 19],\n", - " [30, 20, 21],\n", - " [31, 23, 22],\n", - " [33, 23, 25, 24],\n", - " [35, 25, 27, 26],\n", - " [37, 27, 29, 28],\n", - " [39, 29, 30],\n", - " [32, 31],\n", - " [32, 34, 33],\n", - " [34, 36, 35],\n", - " [36, 38, 37],\n", - " [38, 39]]\n" + "[[0, 1],\n", + " [2, 1, 4],\n", + " [5, 4, 7],\n", + " [8, 7, 10],\n", + " [11, 10],\n", + " [13, 3, 0],\n", + " [14, 3, 6, 2],\n", + " [16, 6, 9, 5],\n", + " [18, 9, 12, 8],\n", + " [20, 12, 11],\n", + " [22, 15, 13],\n", + " [23, 15, 17, 14],\n", + " [25, 17, 19, 16],\n", + " [27, 19, 21, 18],\n", + " [29, 21, 20],\n", + " [31, 24, 22],\n", + " [32, 24, 26, 23],\n", + " [34, 26, 28, 25],\n", + " [36, 28, 30, 27],\n", + " [38, 30, 29],\n", + " [33, 31],\n", + " [33, 35, 32],\n", + " [35, 37, 34],\n", + " [37, 39, 36],\n", + " [39, 38]]\n" ] } ], @@ -494,19 +494,19 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "_atlas4py.Metadata({'bool': True, 'float': 3.12, 'global': False, 'int': 3, 'levels': 1, 'name': 'my_field', 'step': 98, 'str': 'x', 'variables': 0})\n", + "_atlas4py.Metadata({'name': 'my_field', 'levels': 1, 'variables': 0, 'global': False})\n", "True\n", "3\n", "3.12\n", "x\n", - "_atlas4py.Metadata({'bool': True, 'float': 3.12, 'global': False, 'int': 3, 'levels': 1, 'name': 'my_field', 'step': 98, 'str': 'x', 'variables': 0})\n" + "_atlas4py.Metadata({'name': 'my_field', 'levels': 1, 'variables': 0, 'global': False, 'bool': True, 'int': 3, 'float': 3.12, 'str': 'x'})\n" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 85bab26..bdceca7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ description = 'Python bindings for Atlas: a ECMWF library for parallel data-structures' name = 'atlas4py' -version = '0.41.1.dev1' +version = '0.41.1.dev2' # ...dev : ...dev license = {text = "Apache License 2.0"} readme = {file = 'README.md', content-type = 'text/markdown'} authors = [{email = 'willem.deconinck@ecmwf.int'}, {name = 'Willem Deconinck'}] @@ -41,6 +41,7 @@ test = ['pytest', 'pytest-cache'] [tool.scikit-build] minimum-version = '0.5' +build-dir="build/{wheel_tag}" cmake.minimum-version = '3.25' cmake.verbose = true cmake.source-dir = "src/atlas4py" @@ -48,8 +49,7 @@ cmake.build-type = "Release" cmake.args = [ "-DATLAS4PY_ECBUILD_VERSION=3.9.1", "-DATLAS4PY_ECKIT_VERSION=1.28.6", - "-DATLAS4PY_ATLAS_VERSION=0.41.1", - "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=build", + "-DATLAS4PY_ATLAS_VERSION=0.41.1" ] wheel.expand-macos-universal-tags = true wheel.install-dir = "atlas4py" @@ -75,3 +75,12 @@ exclude = ''' | dist )/ ''' + +[tool.bumpversion] +# To update atlas4py dev version: +# pip install bump-my-version +# bump-my-version bump dev +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.dev(?P\\d+))?" +serialize = ["{major}.{minor}.{patch}.dev{dev}"] +[[tool.bumpversion.files]] +filename = "pyproject.toml" diff --git a/requirements-dev.txt b/requirements-dev.txt index 8381ec5..43cad8c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ black>=21.12b0 -bump2version>=1.0 +bump-my-version>=1.2.6 numpy>=1.17 pytest>=6.1 tox>=4.0 diff --git a/src/atlas4py/CMakeLists.txt b/src/atlas4py/CMakeLists.txt index 1f61c0c..e692305 100644 --- a/src/atlas4py/CMakeLists.txt +++ b/src/atlas4py/CMakeLists.txt @@ -1,64 +1,84 @@ cmake_minimum_required(VERSION 3.25.0) -project(atlas4py LANGUAGES CXX) +if (NOT SKBUILD) + set (PROJECT_ROOT_DIR "../..") + cmake_path(ABSOLUTE_PATH PROJECT_ROOT_DIR BASE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} NORMALIZE OUTPUT_VARIABLE PROJECT_ROOT_DIR) + message(FATAL_ERROR "\ + + This CMake file is meant to be executed using 'scikit-build'. Running + it directly will almost certainly not produce the desired result. If + you are a user trying to install this package, please use the command + below, which will install all necessary build dependencies, compile + the package in an isolated environment, and then install it. + ===================================================================== + $ pip install ${PROJECT_ROOT_DIR} + ===================================================================== + For development purposes, it is usually much more efficient to + install the build dependencies in your environment once and use the + following command that avoids a costly creation of a new virtual + environment at every compilation: + ===================================================================== + $ pip install pybind11 scikit-build-core + $ pip install --no-build-isolation -ve ${PROJECT_ROOT_DIR} + ===================================================================== + You may optionally add -Ceditable.rebuild=true to auto-rebuild when + the package is imported. Otherwise, you need to re-run the above + after editing C++ files.") +endif() + +set( PROJECT_NAME ${SKBUILD_PROJECT_NAME} ) +set( PROJECT_VERSION ${SKBUILD_PROJECT_VERSION} ) +set( PROJECT_VERSION_FULL ${SKBUILD_PROJECT_VERSION_FULL} ) +string( REPLACE "${PROJECT_VERSION}.dev" "" PROJECT_VERSION_DEV "${PROJECT_VERSION_FULL}" ) + +message( STATUS "[${PROJECT_NAME}] (${PROJECT_VERSION_FULL})" ) +project(${PROJECT_NAME} LANGUAGES CXX VERSION ${PROJECT_VERSION}) + +# Policies cmake_policy(SET CMP0074 NEW) +if (POLICY CMP0148) + cmake_policy(SET CMP0148 NEW) +endif() +if (POLICY CMP0168) + cmake_policy(SET CMP0168 NEW) +endif() + set(CMAKE_CXX_STANDARD 17) -include(FetchContent) +include(cmake/atlas4py_add_atlas.cmake) +atlas4py_add_atlas() -find_package(atlas QUIET PATHS $ENV{ATLAS_INSTALL_DIR}) -if( atlas_FOUND ) - message( "Found atlas: ${atlas_DIR} (found version \"${atlas_VERSION}\")" ) -endif() +### Find pybind11 -if(NOT atlas_FOUND) - find_package(ecbuild) - if(NOT ecbuild_FOUND) - FetchContent_Declare( - ecbuild - GIT_REPOSITORY https://github.com/ecmwf/ecbuild.git - GIT_TAG ${ATLAS4PY_ECBUILD_VERSION} - ) - FetchContent_MakeAvailable(ecbuild) - endif() - find_package(eckit) - if( eckit_FOUND ) - message( "Found eckit: ${eckit_DIR} (found version \"${eckit_VERSION}\")" ) - endif() - - if(NOT eckit_FOUND) - FetchContent_Declare( - eckit - GIT_REPOSITORY https://github.com/ecmwf/eckit.git - GIT_TAG ${ATLAS4PY_ECKIT_VERSION} - ) - FetchContent_MakeAvailable(eckit) - set(_atlas4py_built_eckit ON) - endif() - - FetchContent_Declare( - atlas - GIT_REPOSITORY https://github.com/ecmwf/atlas.git - GIT_TAG ${ATLAS4PY_ATLAS_VERSION} - ) - set( ENABLE_GRIDTOOLS_STORAGE OFF CACHE BOOL "" FORCE ) - FetchContent_MakeAvailable(atlas) +message( STATUS "${PROJECT_NAME}: find_package(pybind11 CONFIG)..." ) +find_package(pybind11 CONFIG) +if (NOT pybind11_FOUND) + message( FATAL_ERROR "pybind11 not found. Please install pybind11 or use pip to install this package." ) endif() -find_package(pybind11) +### RPATH handling +include(GNUInstallDirs) # defines CMAKE_INSTALL_LIBDIR if(APPLE) set(rpath_origin_install_libdir "@loader_path/${CMAKE_INSTALL_LIBDIR}") else() set(rpath_origin_install_libdir "$ORIGIN/${CMAKE_INSTALL_LIBDIR}") endif() +set( CMAKE_INSTALL_RPATH "${rpath_origin_install_libdir}" ) # A semicolon-separated list specifying the RPATH to use in installed targets +set( CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE ) # add the automatic parts to RPATH which point to dirs outside build tree +set( CMAKE_SKIP_BUILD_RPATH FALSE ) # use RPATHs for the build tree +set( CMAKE_BUILD_WITH_INSTALL_RPATH TRUE ) # build with *relative* rpaths by default + +### Python bindings module atlas4py pybind11_add_module(_atlas4py _atlas4py.cpp) target_link_libraries(_atlas4py PUBLIC atlas) -install(TARGETS _atlas4py DESTINATION .) -set_target_properties(_atlas4py PROPERTIES INSTALL_RPATH "${rpath_origin_install_libdir}") +target_compile_definitions(_atlas4py PRIVATE ATLAS4PY_VERSION_STRING=${PROJECT_VERSION_FULL}) + +### Installation +install(TARGETS _atlas4py DESTINATION .) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ DESTINATION . FILES_MATCHING PATTERN "*.py" diff --git a/src/atlas4py/__init__.py b/src/atlas4py/__init__.py index 3296ead..843d21a 100644 --- a/src/atlas4py/__init__.py +++ b/src/atlas4py/__init__.py @@ -5,8 +5,8 @@ """ import atexit -from ._version import __version__ from ._atlas4py import * +from ._atlas4py import __version__ _atlas4py._initialise() atexit.register(lambda: _atlas4py._finalise()) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 96ba4a2..2183bbf 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -55,34 +55,78 @@ void atlasInitialise() { atlas::initialise(argc, argv); } +py::object toPyObject( eckit::Configuration const& v ); +py::object toPyObject( eckit::Configuration const& v, std::string& key ); -py::object toPyObject( eckit::Value const& v ) { - if ( v.isBool() ) - return py::bool_( v.as() ); - else if ( v.isNumber() ) - return py::int_( v.as() ); - else if ( v.isDouble() ) - return py::float_( v.as() ); - else if ( v.isMap() ) { - py::dict ret; - auto const& map = v.as(); - for ( auto const& [k, v] : map ) { - ret[k.as().c_str()] = toPyObject( v ); - } - return ret; +py::object toPyObject(bool v) { + return py::bool_(v); +} +py::object toPyObject(long v) { + return py::int_(v); +} +py::object toPyObject(double v) { + return py::float_(v); +} +py::object toPyObject(std::string const& v) { + return py::str(v); +} +template +py::object toPyObject( std::vector const& v ) { + py::list ret; + for ( auto const& val : v ) { + ret.append( toPyObject( val ) ); } - else if ( v.isList() ) { - py::list ret; - auto const& list = v.as(); - for ( auto const& v : list ) - ret.append( toPyObject( v ) ); - return ret; + return ret; +} + +py::object toPyObject( eckit::Configuration const& v, std::string const& key ) { + if ( v.isSubConfiguration ( key ) ) { + return toPyObject( v.getSubConfiguration( key ) ); } - else if ( v.isString() ) - return py::str( v.as() ); - else - throw std::out_of_range( "type of value unsupported (" + v.typeName() + ")" ); + else if (v.isBoolean( key )) { + return toPyObject( v.getBool( key ) ); + } + else if (v.isIntegral( key )) { + return toPyObject( v.getLong( key ) ); + } + else if (v.isFloatingPoint( key )) { + return toPyObject( v.getDouble( key ) ); + } + else if (v.isString( key )) { + return toPyObject( v.getString( key ) ); + } + else if (v.isSubConfigurationList( key )) { + std::vector subconfigs = v.getSubConfigurations( key ); + return toPyObject( subconfigs ); + } + else if (v.isIntegralList( key )) { + std::vector values = v.getLongVector( key ); + return toPyObject( values ); + } + else if (v.isFloatingPointList( key )) { + std::vector values = v.getDoubleVector( key ); + return toPyObject( values ); + } + else if (v.isStringList( key )) { + std::vector values = v.getStringVector( key ); + return toPyObject( values ); + } + else if (v.isBooleanList( key )) { + throw std::out_of_range( "boolean lists not supported for key " + key ); + } + else { + throw std::out_of_range( "type of value unsupported for key " + key ); + } +} + +py::object toPyObject( eckit::Configuration const& v ) { + py::dict ret; + for ( auto const& key : v.keys()) { + ret[ key.c_str() ] = toPyObject( v, key ); + } + return ret; } + std::string atlasToPybind( array::DataType const& dt ) { switch ( dt.kind() ) { case array::DataType::KIND_INT32: @@ -116,10 +160,13 @@ array::DataType pybindToAtlas( py::dtype const& dtype ) { } // namespace +#define STRINGIFY(s) STRINGIFY_HELPER(s) +#define STRINGIFY_HELPER(s) #s + PYBIND11_MODULE( _atlas4py, m ) { m.def("_initialise", atlasInitialise) .def("_finalise", atlas::finalise); - m.attr("version") = atlas::Library::instance().version(); + m.attr("__version__") = STRINGIFY(ATLAS4PY_VERSION_STRING); py::class_( m, "PointLonLat" ) .def( py::init( []( double lon, double lat ) { @@ -144,38 +191,38 @@ PYBIND11_MODULE( _atlas4py, m ) { } ); py::class_( m, "Projection" ).def( "__repr__", []( Projection const& p ) { - return "_atlas4py.Projection("_s + py::str( toPyObject( p.spec().get() ) ) + ")"_s; + return "_atlas4py.Projection("_s + py::str( toPyObject( p.spec() ) ) + ")"_s; } ); py::class_( m, "Domain" ) .def_property_readonly( "type", &Domain::type ) - .def_property_readonly( "global", &Domain::global ) + .def_property_readonly( "is_global", &Domain::global ) // global is a python keyword, so we can't use it as a property name .def_property_readonly( "units", &Domain::units ) .def( "__repr__", []( Domain const& d ) { - return "_atlas4py.Domain("_s + ( d ? py::str( toPyObject( d.spec().get() ) ) : "" ) + ")"_s; + return "_atlas4py.Domain("_s + ( d ? py::str( toPyObject( d.spec() ) ) : "" ) + ")"_s; } ); py::class_( m, "RectangularDomain" ) .def( py::init( []( std::tuple xInterval, std::tuple yInterval ) { auto [xFrom, xTo] = xInterval; - auto [yFrom, yTo] = xInterval; + auto [yFrom, yTo] = yInterval; return RectangularDomain( { xFrom, xTo }, { yFrom, yTo } ); } ), "x_interval"_a, "y_interval"_a ); py::class_( m, "Grid" ) - .def( py::init() ) + .def( py::init(), "name"_a ) .def_property_readonly( "name", &Grid::name ) .def_property_readonly( "uid", &Grid::uid ) .def_property_readonly( "size", &Grid::size ) .def_property_readonly( "projection", &Grid::projection ) .def_property_readonly( "domain", &Grid::domain ) .def( "__repr__", - []( Grid const& g ) { return "_atlas4py.Grid("_s + py::str( toPyObject( g.spec().get() ) ) + ")"_s; } ); + []( Grid const& g ) { return "_atlas4py.Grid("_s + py::str( toPyObject( g.spec() ) ) + ")"_s; } ); py::class_( m, "Spacing" ) .def( "__len__", &grid::Spacing::size ) .def( "__getitem__", &grid::Spacing::operator[]) .def( "__repr__", []( grid::Spacing const& spacing ) { - return "_atlas4py.Spacing("_s + py::str( toPyObject( spacing.spec().get() ) ) + ")"_s; + return "_atlas4py.Spacing("_s + py::str( toPyObject( spacing.spec() ) ) + ")"_s; } ); py::class_( m, "LinearSpacing" ) .def( py::init( []( double start, double stop, long N, bool endpoint ) { @@ -186,10 +233,14 @@ PYBIND11_MODULE( _atlas4py, m ) { .def( py::init( []( long N ) { return grid::GaussianSpacing{ N }; } ), "N"_a ); py::class_( m, "StructuredGrid" ) + .def( py::init( []( const Grid& grid, Domain const& d ) { + return StructuredGrid{ grid, d }; + } ), + "grid"_a, "domain"_a = Domain() ) .def( py::init( []( std::string const& s, Domain const& d ) { return StructuredGrid{ s, d }; } ), - "gridname"_a, "domain"_a = Domain() ) + "name"_a, "domain"_a = Domain() ) .def( py::init( []( grid::LinearSpacing xSpacing, grid::Spacing ySpacing ) { return StructuredGrid{ xSpacing, ySpacing }; } ), @@ -201,6 +252,9 @@ PYBIND11_MODULE( _atlas4py, m ) { return StructuredGrid{ xSpacings, ySpacing, Projection(), d }; } ), "x_spacings"_a, "y_spacing"_a, "domain"_a = Domain() ) + .def( "__enter__", []( StructuredGrid& self ) { return self; } ) + .def( "__exit__", []( StructuredGrid& self, py::object exc_type, py::object exc_val, py::object exc_tb ) { self.reset( nullptr ); } ) + .def( "__bool__", []( StructuredGrid const& self ) { return self.valid(); } ) .def_property_readonly( "valid", &StructuredGrid::valid ) .def_property_readonly( "ny", &StructuredGrid::ny ) .def_property_readonly( "nx", py::overload_cast<>( &StructuredGrid::nx, py::const_ ) ) @@ -241,10 +295,10 @@ PYBIND11_MODULE( _atlas4py, m ) { // not be done (see comment in Config::get). We cannot // avoid this right now because otherwise we cannot query // the type of the underlying data. - return toPyObject( config.get().element( key ) ); + return toPyObject( config, key ); } ) .def( "__repr__", []( util::Config const& config ) { - return "_atlas4py.Config("_s + py::str( toPyObject( config.get() ) ) + ")"_s; + return "_atlas4py.Config("_s + py::str( toPyObject( config ) ) + ")"_s; } ); py::class_( m, "StructuredMeshGenerator" ) @@ -413,10 +467,10 @@ PYBIND11_MODULE( _atlas4py, m ) { // not be done (see comment in Config::get). We cannot // avoid this right now because otherwise we cannot query // the type of the underlying data. - return toPyObject( metadata.get().element( key ) ); + return toPyObject( metadata, key ); } ) .def( "__repr__", []( util::Metadata const& metadata ) { - return "_atlas4py.Metadata("_s + py::str( toPyObject( metadata.get() ) ) + ")"_s; + return "_atlas4py.Metadata("_s + py::str( toPyObject( metadata ) ) + ")"_s; } ); py::class_( m, "Field", py::buffer_protocol() ) @@ -425,6 +479,7 @@ PYBIND11_MODULE( _atlas4py, m ) { .def_property_readonly( "shape", py::overload_cast<>( &Field::shape, py::const_ ) ) .def_property_readonly( "size", &Field::size ) .def_property_readonly( "rank", &Field::rank ) + .def_property_readonly( "dtype", []( Field& f ) { return atlasToPybind( f.datatype() ); } ) .def_property_readonly( "datatype", []( Field& f ) { return atlasToPybind( f.datatype() ); } ) .def_property( "metadata", py::overload_cast<>( &Field::metadata, py::const_ ), py::overload_cast<>( &Field::metadata ) ) @@ -458,13 +513,9 @@ PYBIND11_MODULE( _atlas4py, m ) { py::class_( m, "Gmsh" ) .def( py::init( []( std::string const& path ) { return output::Gmsh{ path }; } ), "path"_a ) .def( "__enter__", []( output::Gmsh& gmsh ) { return gmsh; } ) - .def( "__exit__", []( output::Gmsh& gmsh, py::object exc_type, py::object exc_val, - py::object exc_tb ) { gmsh.reset( nullptr ); } ) - .def( - "write", []( output::Gmsh& gmsh, Mesh const& mesh ) { gmsh.write( mesh ); }, "mesh"_a ) - .def( - "write", []( output::Gmsh& gmsh, Field const& field ) { gmsh.write( field ); }, "field"_a ) - .def( - "write", []( output::Gmsh& gmsh, Field const& field, FunctionSpace const& fs ) { gmsh.write( field, fs ); }, - "field"_a, "functionspace"_a ); + .def( "__exit__", []( output::Gmsh& self, py::object exc_type, py::object exc_val, py::object exc_tb ) { self.reset( nullptr ); } ) + .def( "write", []( output::Gmsh& gmsh, Mesh const& mesh ) { gmsh.write( mesh ); }, "mesh"_a ) + .def( "write", []( output::Gmsh& gmsh, Field const& field ) { gmsh.write( field ); }, "field"_a ) + .def( "write", []( output::Gmsh& gmsh, Field const& field, FunctionSpace const& fs ) { gmsh.write( field, fs ); }, + "field"_a, "functionspace"_a ); } diff --git a/src/atlas4py/_version.py b/src/atlas4py/_version.py deleted file mode 100644 index 90ba6b6..0000000 --- a/src/atlas4py/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.41.1.dev1" diff --git a/src/atlas4py/cmake/atlas4py_add_atlas.cmake b/src/atlas4py/cmake/atlas4py_add_atlas.cmake new file mode 100644 index 0000000..89d7486 --- /dev/null +++ b/src/atlas4py/cmake/atlas4py_add_atlas.cmake @@ -0,0 +1,71 @@ +# This file defines the macro atlas4py_add_atlas, which is responsible for ensuring that the atlas library +# is available for building the Python bindings. +# It does so by first trying to find an existing installation of atlas using CMake's find_package. +# If it cannot find it, it uses FetchContent to download and build atlas and its dependencies (eckit and ecbuild) from source. +# The macro is called in the main CMakeLists.txt file of the project. + +macro(atlas4py_add_atlas) + if (NOT build_atlas) + if (DEFINED ENV{ATLAS_INSTALL_DIR}) + set( atlas_ROOT ${ENV{ATLAS_INSTALL_DIR}} ) + endif() + find_package(atlas CONFIG QUIET) + if( atlas_FOUND ) + message( STATUS "Found atlas: ${atlas_DIR} (found version \"${atlas_VERSION}\")" ) + message( STATUS "Found eckit: ${eckit_DIR} (found version \"${eckit_VERSION}\")" ) + message( STATUS "If instead you want ensure to build atlas from source, configure with -Dbuild_atlas=ON") + if (NOT atlas_VERSION VERSION_EQUAL ATLAS4PY_ATLAS_VERSION) + message( WARNING "Found atlas version \"${atlas_VERSION}\", but configured version is \"${ATLAS4PY_ATLAS_VERSION}\"" ) + endif() + if (NOT eckit_VERSION VERSION_EQUAL ATLAS4PY_ECKIT_VERSION) + message( WARNING "Found eckit version \"${eckit_VERSION}\", but configured version is \"${ATLAS4PY_ECKIT_VERSION}\"" ) + endif() + set( ignore_variable ${ATLAS4PY_ECBUILD_VERSION} ) # To avoid "unused variable" warning + else() + set(build_atlas TRUE CACHE INTERNAL "build atlas") + endif() + endif() + if(build_atlas) + include(FetchContent) + + ### Download dependencies + set ( ecbuild_ROOT ${CMAKE_BINARY_DIR}/_deps/ecbuild ) + message( STATUS "Downloading ecbuild version \"${ATLAS4PY_ECBUILD_VERSION}\" to ${ecbuild_ROOT}" ) + message( STATUS "Downloading and building eckit version \"${ATLAS4PY_ECKIT_VERSION}\"" ) + message( STATUS "Downloading and building atlas version \"${ATLAS4PY_ATLAS_VERSION}\"" ) + FetchContent_Populate( + ecbuild + GIT_REPOSITORY https://github.com/ecmwf/ecbuild.git + GIT_TAG ${ATLAS4PY_ECBUILD_VERSION} + SOURCE_DIR ${ecbuild_ROOT} + QUIET + ) + FetchContent_Declare( + eckit + GIT_REPOSITORY https://github.com/ecmwf/eckit.git + GIT_TAG ${ATLAS4PY_ECKIT_VERSION} + ) + FetchContent_Declare( + atlas + GIT_REPOSITORY https://github.com/ecmwf/atlas.git + GIT_TAG ${ATLAS4PY_ATLAS_VERSION} + ) + + # Disable unused features for faster compilation + set(ECKIT_ENABLE_TESTS OFF) + set(ECKIT_ENABLE_DOCS OFF) + set(ECKIT_ENABLE_PKGCONFIG OFF) + set(ECKIT_ENABLE_ECKIT_GEO OFF) + set(ECKIT_ENABLE_ECKIT_SQL OFF) + set(ECKIT_ENABLE_ECKIT_CMD OFF) + + set(ATLAS_ENABLE_TESTS OFF) + set(ATLAS_ENABLE_DOCS OFF) + set(ATLAS_ENABLE_PKGCONFIG OFF) + set(ECKIT_ENABLE_ECKIT_GEO OFF) + set(ECKIT_ENABLE_ECKIT_SQL OFF) + set(ATLAS_ENABLE_ECKIT_CMD OFF) + + FetchContent_MakeAvailable(eckit atlas) + endif() +endmacro() diff --git a/tests/conftest.py b/tests/conftest.py index d167007..98b2d37 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,4 +3,3 @@ # Ignore hidden folders and disabled tests collect_ignore_glob = [".*", "_disabled*"] - diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 790de18..423bb48 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -3,6 +3,7 @@ import atlas4py + # -- Fixtures -- @pytest.fixture def structured_grid(): @@ -42,10 +43,78 @@ def test_version(): def test_grid_generation(structured_grid): assert structured_grid.domain.type == "rectangular" assert structured_grid.regular == True + assert structured_grid.reduced == False + assert structured_grid.periodic == False + assert structured_grid.ny == 21 + assert structured_grid.nx == [20 for _ in range(21)] + assert structured_grid.size == 420 + ll0_0 = structured_grid.lonlat(0, 0) + ll19_20 = structured_grid.lonlat(19, 20) + assert [ll0_0.lon, ll0_0.lat] == pytest.approx([-1, -1]) + assert [ll19_20.lon, ll19_20.lat] == pytest.approx([1, 1]) + assert structured_grid.domain.is_global == False + + +def test_grid_generation_regional(): + grid = atlas4py.StructuredGrid( + x_spacings=[atlas4py.LinearSpacing(-1, 1, 20) for _ in range(21)], + y_spacing=atlas4py.LinearSpacing(-1, 1, 21), + ) + assert grid.regular == True + assert grid.reduced == False + assert grid.periodic == False + assert grid.size == 420 + assert grid.domain.is_global == False + + +def test_grid_generation_O32(): + grid = atlas4py.Grid("O32") + with atlas4py.StructuredGrid(grid) as sg: + assert sg + assert sg.regular == False + assert sg.reduced == True + assert sg.periodic == True + assert grid.size == 5248 + assert grid.domain.is_global == True + + +def test_grid_generation_F32(): + grid = atlas4py.Grid("F32") + with atlas4py.StructuredGrid(grid) as sg: + assert sg + assert sg.regular == True + assert sg.reduced == False + assert sg.periodic == True + assert grid.size == 8192 + assert grid.domain.is_global == True def test_mesh_generation(structured_mesh): assert structured_mesh.grid.domain.type == "rectangular" + assert structured_mesh.cells.size == 380 + assert structured_mesh.nodes.size == 420 + + +def test_mesh_connectivity(structured_mesh): + mesh = structured_mesh + assert mesh.cells.node_connectivity.rows == 380 + assert mesh.cells.node_connectivity.cols(0) == 4 + assert mesh.cells.node_connectivity[0, 0] == 20 + assert mesh.cells.node_connectivity[0, 1] == 0 + assert mesh.cells.node_connectivity[0, 2] == 1 + assert mesh.cells.node_connectivity[0, 3] == 21 + assert mesh.cells.node_connectivity[0, 0, 0] == 20 + assert mesh.cells.node_connectivity[0, 0, 1] == 0 + assert mesh.cells.node_connectivity[0, 0, 2] == 1 + assert mesh.cells.node_connectivity[0, 0, 3] == 21 + + block = mesh.cells.node_connectivity.block(0) + assert block.rows == 380 + assert block.cols == 4 + assert block[0, 0] == 20 + assert block[0, 1] == 0 + assert block[0, 2] == 1 + assert block[0, 3] == 21 def test_function_space_generation(structured_function_space): @@ -54,9 +123,29 @@ def test_function_space_generation(structured_function_space): def test_field_generation(structured_in_and_out_fields): in_f, out_f = structured_in_and_out_fields + assert in_f.rank == 2 + assert out_f.rank == 2 + assert in_f.shape == [380, 1] + assert out_f.shape == [380, 1] + assert np.dtype(in_f.dtype) == np.float64 + out_view = np.asarray(out_f) in_view = np.asarray(in_f) in_view[...] = np.full_like(in_view, 3.1415) out_view[...] = in_view + 1 assert np.allclose(in_view + 1, out_view) + + +def test_gmsh_output(structured_mesh): + import os + + output_file = "test_output.msh" + with atlas4py.Gmsh(path=output_file) as gmsh: + gmsh.write(structured_mesh) + + # Check that the file was created and has content + assert os.path.exists(output_file) + + # Clean up the output file + os.remove(output_file)