diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 7b5f533..e9ad5d3 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -7,7 +7,7 @@ inputs: required: true install-build-backend: description: > - Whether to pre-install pybind11, scikit-build-core and numpy into the + Whether to pre-install nanobind, scikit-build-core into the host environment. Required for `--no-build-isolation` builds; can be skipped when pip provisions an isolated build environment. required: false @@ -63,4 +63,4 @@ runs: - name: Install Python build backend (for --no-build-isolation builds) if: inputs.install-build-backend == 'true' shell: bash - run: python -m pip install pybind11 scikit-build-core numpy + run: python -m pip install nanobind scikit-build-core diff --git a/.github/actions/verify-atlas4py/action.yml b/.github/actions/verify-atlas4py/action.yml index 642bcf8..04c2224 100644 --- a/.github/actions/verify-atlas4py/action.yml +++ b/.github/actions/verify-atlas4py/action.yml @@ -23,5 +23,5 @@ runs: - name: Run pytest suite shell: bash run: | - python -m pip install pytest + python -m pip install pytest numpy pytest -v tests diff --git a/pyproject.toml b/pyproject.toml index eb555b7..cdd6a82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,17 +2,15 @@ build-backend = 'scikit_build_core.build' requires = [ 'scikit-build-core>=0.10.7', - 'pybind11>=2.11.1', + 'nanobind>=2.11.0', ] [project] -dependencies = [ - 'numpy>=1.23' -] +dependencies = [] description = 'Python bindings for Atlas: a ECMWF library for parallel data-structures' name = 'atlas4py' -version = '0.41.1.dev3' # ...dev : ...dev +version = '0.41.1.dev4' # ...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'}] @@ -35,7 +33,7 @@ classifiers = [ repository = 'https://github.com/GridTools/atlas4py' [project.optional-dependencies] -test = ['pytest'] +test = ['pytest','numpy>=1.23'] [tool.scikit-build] minimum-version = '0.5' diff --git a/requirements-dev.txt b/requirements-dev.txt index e94e170..0bc235b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ black>=21.12b0 bump-my-version>=1.2.6 -numpy>=1.17 +numpy>=1.23 pytest>=6.1 tox>=4.0 build>=1.2.2 diff --git a/src/atlas4py/CMakeLists.txt b/src/atlas4py/CMakeLists.txt index 6f0457e..90c1963 100644 --- a/src/atlas4py/CMakeLists.txt +++ b/src/atlas4py/CMakeLists.txt @@ -19,7 +19,7 @@ if (NOT SKBUILD) following command that avoids a costly creation of a new virtual environment at every compilation: ===================================================================== - $ pip install pybind11 scikit-build-core + $ pip install nanobind scikit-build-core $ pip install --no-build-isolation -ve ${PROJECT_ROOT_DIR} ===================================================================== You may optionally add -Ceditable.rebuild=true to auto-rebuild when @@ -49,12 +49,19 @@ set(CMAKE_CXX_STANDARD 17) include(cmake/atlas4py_add_atlas.cmake) atlas4py_add_atlas() -### Find pybind11 +### Find nanobind -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." ) +message( STATUS "${PROJECT_NAME}: find_package(Python 3.9 REQUIRED COMPONENTS Interpreter Development.Module)" ) +find_package(Python 3.9 REQUIRED COMPONENTS Interpreter Development.Module) + +execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT) + +message( STATUS "${PROJECT_NAME}: find_package(nanobind CONFIG)..." ) +find_package(nanobind CONFIG) +if (NOT nanobind_FOUND) + message( FATAL_ERROR "nanobind not found. Please install nanobind or use pip to install this package." ) endif() ### RPATH handling @@ -74,7 +81,7 @@ endif() ### Python bindings module atlas4py -pybind11_add_module(_atlas4py _atlas4py.cpp) +nanobind_add_module(_atlas4py _atlas4py.cpp) target_link_libraries(_atlas4py PUBLIC atlas) target_compile_definitions(_atlas4py PRIVATE ATLAS4PY_VERSION_STRING=${PROJECT_VERSION_FULL}) diff --git a/src/atlas4py/_atlas4py.cpp b/src/atlas4py/_atlas4py.cpp index 2183bbf..3cdf243 100644 --- a/src/atlas4py/_atlas4py.cpp +++ b/src/atlas4py/_atlas4py.cpp @@ -1,8 +1,12 @@ #include -#include -#include -#include +#include +#include +#include // Required for std::string support +#include +#include +#include // required for std::optional arguments +#include // required for std::optional arguments #include "atlas/functionspace.h" #include "atlas/grid.h" @@ -18,14 +22,30 @@ #include "atlas/output/Gmsh.h" #include "atlas/library.h" +#if __has_include("pluto/pointer_info.h") +#include "pluto/pointer_info.h" +#define ATLAS4PY_HAS_PLUTO +#else +#warning "pluto/pointer_info.h not found, it is only available starting from atlas version 0.42.0. Assuming all arrays are host accessible and not pinned, device, or managed" +namespace pluto { + inline bool is_pinned(const void*) { return false; } + inline bool is_host(const void*) { return true; } + inline bool is_device(const void*) { return false; } + inline bool is_managed(const void*) { return false; } + inline bool is_host_accessible(const void*) { return true; } + inline bool is_device_accessible(const void*) { return false; } +} +#endif + #include "eckit/value/Value.h" #include "eckit/config/Configuration.h" -namespace py = ::pybind11; +namespace nb = ::nanobind; + using namespace atlas; -using namespace pybind11::literals; +using namespace nb::literals; -namespace pybind11 { +namespace nanobind { namespace detail { template <> struct type_caster @@ -34,7 +54,81 @@ template <> struct type_caster : public type_caster> {}; } // namespace detail -} // namespace pybind11 +} // namespace nanobind + + +namespace atlas4py::dtype { +std::string to_python_name( DataType const& dt ) { + switch ( dt.kind() ) { + case DataType::KIND_INT32: + return "int32"; + case DataType::KIND_INT64: + return "int64"; + case DataType::KIND_REAL32: + return "float32"; + case DataType::KIND_REAL64: + return "float64"; + case DataType::KIND_UINT64: + return "uint64"; + default: + return ""; + } +} + +::nb::dlpack::dtype to_nb_dtype( DataType const& dt ) { + switch ( dt.kind() ) { + case DataType::KIND_INT32: + return nb::dtype(); + case DataType::KIND_INT64: + return nb::dtype(); + case DataType::KIND_REAL32: + return nb::dtype(); + case DataType::KIND_REAL64: + return nb::dtype(); + case DataType::KIND_UINT64: + return nb::dtype(); + default: + return nb::dlpack::dtype(); + } +} + +DataType from_python_name( std::string const& dtype_name ) { + if (dtype_name == "int32") { + return DataType::KIND_INT32; + } + else if (dtype_name == "int64") { + return DataType::KIND_INT64; + } + else if (dtype_name == "float32" || dtype_name == "real32") { + return DataType::KIND_REAL32; + } + else if (dtype_name == "float64" || dtype_name == "real64") { + return DataType::KIND_REAL64; + } + else if (dtype_name == "uint64") { + return DataType::KIND_UINT64; + } + else { + throw std::out_of_range( "unsupported dtype: " + dtype_name ); + } +} + +DataType from_python_object( nb::object const& dtype ) { + if (nb::hasattr(dtype, "__name__")) { + return from_python_name( nb::cast(dtype.attr("__name__")) ); + } + else if (nb::hasattr(dtype, "name")) { + return from_python_name( nb::cast(dtype.attr("name")) ); + } + else if ( nb::isinstance( dtype ) ) { + return from_python_name( nb::cast(dtype) ); + } + else { + throw std::out_of_range( "unsupported dtype" ); + } +} +} + namespace { @@ -44,8 +138,8 @@ void atlasInitialise() { return; already_initialised = true; - py::module sys = py::module::import("sys"); - py::list sys_argv = sys.attr("argv"); + nb::module_ sys = nb::module_::import_("sys"); + nb::list sys_argv = sys.attr("argv"); int argc = sys_argv.size(); char** argv = new char*[argc]; for (int i = 0; i < argc; ++i) { @@ -53,33 +147,35 @@ void atlasInitialise() { } atlas::initialise(argc, argv); + // Warning: Don't delete argv, because this is being referenced within + // the atlas library and deleting it may cause issues. } -py::object toPyObject( eckit::Configuration const& v ); -py::object toPyObject( eckit::Configuration const& v, std::string& key ); +nb::object toPyObject( eckit::Configuration const& v ); +nb::object toPyObject( eckit::Configuration const& v, std::string const& key ); -py::object toPyObject(bool v) { - return py::bool_(v); +nb::object toPyObject(bool v) { + return nb::bool_(v); } -py::object toPyObject(long v) { - return py::int_(v); +nb::object toPyObject(long v) { + return nb::int_(v); } -py::object toPyObject(double v) { - return py::float_(v); +nb::object toPyObject(double v) { + return nb::float_(v); } -py::object toPyObject(std::string const& v) { - return py::str(v); +nb::object toPyObject(std::string const& v) { + return nb::str(v.c_str()); } template -py::object toPyObject( std::vector const& v ) { - py::list ret; +nb::object toPyObject( std::vector const& v ) { + nb::list ret; for ( auto const& val : v ) { ret.append( toPyObject( val ) ); } return ret; } -py::object toPyObject( eckit::Configuration const& v, std::string const& key ) { +nb::object toPyObject( eckit::Configuration const& v, std::string const& key ) { if ( v.isSubConfiguration ( key ) ) { return toPyObject( v.getSubConfiguration( key ) ); } @@ -119,43 +215,73 @@ py::object toPyObject( eckit::Configuration const& v, std::string const& key ) { } } -py::object toPyObject( eckit::Configuration const& v ) { - py::dict ret; +nb::object toPyObject( eckit::Configuration const& v ) { + nb::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: - return py::format_descriptor::format(); - case array::DataType::KIND_INT64: - return py::format_descriptor::format(); - case array::DataType::KIND_REAL32: - return py::format_descriptor::format(); - case array::DataType::KIND_REAL64: - return py::format_descriptor::format(); - case array::DataType::KIND_UINT64: - return py::format_descriptor::format(); - default: - return ""; +int get_nb_device_type( const void* ptr ) { + if (pluto::is_pinned(ptr)) { + return nb::device::cuda_host::value; + } + else if (pluto::is_host(ptr)) { + return nb::device::cpu::value; + } + else if (pluto::is_device(ptr)) { + return nb::device::cuda::value; + } + else if (pluto::is_managed(ptr)) { + return nb::device::cuda_managed::value; + } + else { + return nb::device::none::value; } } -array::DataType pybindToAtlas( py::dtype const& dtype ) { - if ( dtype.is( py::dtype::of() ) ) - return array::DataType::KIND_INT32; - else if ( dtype.is( py::dtype::of() ) ) - return array::DataType::KIND_INT64; - else if ( dtype.is( py::dtype::of() ) ) - return array::DataType::KIND_REAL32; - else if ( dtype.is( py::dtype::of() ) ) - return array::DataType::KIND_REAL64; - else if ( dtype.is( py::dtype::of() ) ) - return array::DataType::KIND_UINT64; - else - return { 0 }; + +enum class MemorySpace { + host, + device +}; +template +auto make_ndarray(atlas::array::Array& array, MemorySpace memory_space) { + void* data_ptr; + if (memory_space == MemorySpace::host) { + data_ptr = array.host_data(); + if (not pluto::is_host_accessible(data_ptr)) { + throw std::runtime_error( "array data is not host accessible" ); + } + } + else { + data_ptr = array.device_data(); + if (not pluto::is_device_accessible(data_ptr)) { + throw std::runtime_error( "array data is not device accessible" ); + } + } + constexpr int max_ndim = 8; + const auto ndim = array.rank(); + if (ndim > max_ndim) { + throw std::runtime_error( "array rank exceeds maximum supported by atlas4py" ); + } + std::array shape; + std::array strides; + for( int i=0; i( + data_ptr, // pointer to data + ndim, // ndim + shape.data(), // shape + nb::handle{}, // owner + strides.data(), // strides + atlas4py::dtype::to_nb_dtype( array.datatype()), // dtype + get_nb_device_type(data_ptr), // device_type + 0, // device_id + 'C' // order + ); } } // namespace @@ -163,131 +289,113 @@ array::DataType pybindToAtlas( py::dtype const& dtype ) { #define STRINGIFY(s) STRINGIFY_HELPER(s) #define STRINGIFY_HELPER(s) #s -PYBIND11_MODULE( _atlas4py, m ) { +NB_MODULE( _atlas4py, m ) { m.def("_initialise", atlasInitialise) .def("_finalise", atlas::finalise); m.attr("__version__") = STRINGIFY(ATLAS4PY_VERSION_STRING); - py::class_( m, "PointLonLat" ) - .def( py::init( []( double lon, double lat ) { - return PointLonLat( { lon, lat } ); - } ), - "lon"_a, "lat"_a ) - .def_property_readonly( "lon", py::overload_cast<>( &PointLonLat::lon, py::const_ ) ) - .def_property_readonly( "lat", py::overload_cast<>( &PointLonLat::lat, py::const_ ) ) + nb::class_( m, "PointLonLat" ) + .def( nb::init(), "lon"_a, "lat"_a ) + .def_prop_ro( "lon", nb::overload_cast<>( &PointLonLat::lon, nb::const_ ) ) + .def_prop_ro( "lat", nb::overload_cast<>( &PointLonLat::lat, nb::const_ ) ) .def( "__repr__", []( PointLonLat const& p ) { - return "_atlas4py.PointLonLat(lon=" + std::to_string( p.lon() ) + ", lat=" + std::to_string( p.lat() ) + - ")"; + return "_atlas4py.PointLonLat(lon=" + std::to_string( p.lon() ) + ", lat=" + std::to_string( p.lat() ) + ")"; } ); - py::class_( m, "PointXY" ) - .def( py::init( []( double x, double y ) { - return PointXY( { x, y } ); - } ), - "x"_a, "y"_a ) - .def_property_readonly( "x", py::overload_cast<>( &PointXY::x, py::const_ ) ) - .def_property_readonly( "y", py::overload_cast<>( &PointXY::y, py::const_ ) ) + nb::class_( m, "PointXY" ) + .def( nb::init(), "x"_a, "y"_a ) + .def_prop_ro( "x", nb::overload_cast<>( &PointXY::x, nb::const_ ) ) + .def_prop_ro( "y", nb::overload_cast<>( &PointXY::y, nb::const_ ) ) .def( "__repr__", []( PointXY const& p ) { return "_atlas4py.PointXY(x=" + std::to_string( p.x() ) + ", y=" + std::to_string( p.y() ) + ")"; } ); - py::class_( m, "Projection" ).def( "__repr__", []( Projection const& p ) { - return "_atlas4py.Projection("_s + py::str( toPyObject( p.spec() ) ) + ")"_s; - } ); - py::class_( m, "Domain" ) - .def_property_readonly( "type", &Domain::type ) - .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 ) + nb::class_( m, "Projection" ) + .def( "__repr__", []( Projection const& p ) { + return "_atlas4py.Projection("_s + nb::str( toPyObject( p.spec() ) ) + ")"_s; + } ); + + nb::class_( m, "Domain" ) + .def_prop_ro( "type", &Domain::type ) + .def_prop_ro( "is_global", &Domain::global ) + .def_prop_ro( "units", &Domain::units ) .def( "__repr__", []( Domain const& d ) { - return "_atlas4py.Domain("_s + ( d ? py::str( toPyObject( d.spec() ) ) : "" ) + ")"_s; + if (d) { + return nb::str("_atlas4py.Domain("_s + nb::str( toPyObject( d.spec() ) ) + ")"_s); + } + return nb::str("_atlas4py.Domain()"_s); } ); - py::class_( m, "RectangularDomain" ) - .def( py::init( []( std::tuple xInterval, std::tuple yInterval ) { - auto [xFrom, xTo] = xInterval; - auto [yFrom, yTo] = yInterval; - return RectangularDomain( { xFrom, xTo }, { yFrom, yTo } ); - } ), - "x_interval"_a, "y_interval"_a ); - - py::class_( m, "Grid" ) - .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 ) + nb::class_( m, "RectangularDomain" ) + .def( nb::init(), "x_interval"_a, "y_interval"_a, "units"_a = "degrees" ); + + nb::class_( m, "Grid" ) + .def( nb::init(), "name"_a ) + .def_prop_ro( "name", &Grid::name ) + .def_prop_ro( "uid", &Grid::uid ) + .def_prop_ro( "size", &Grid::size ) + .def_prop_ro( "projection", &Grid::projection ) + .def_prop_ro( "domain", &Grid::domain ) .def( "__repr__", - []( Grid const& g ) { return "_atlas4py.Grid("_s + py::str( toPyObject( g.spec() ) ) + ")"_s; } ); + []( Grid const& g ) { return "_atlas4py.Grid("_s + nb::str( toPyObject( g.spec() ) ) + ")"_s; } ); - py::class_( m, "Spacing" ) + nb::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() ) ) + ")"_s; + return "_atlas4py.Spacing("_s + nb::str( toPyObject( spacing.spec() ) ) + ")"_s; } ); - py::class_( m, "LinearSpacing" ) - .def( py::init( []( double start, double stop, long N, bool endpoint ) { - return grid::LinearSpacing{ start, stop, N, endpoint }; - } ), - "start"_a, "stop"_a, "N"_a, "endpoint_included"_a = true ); - py::class_( m, "GaussianSpacing" ) - .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 }; - } ), - "name"_a, "domain"_a = Domain() ) - .def( py::init( []( grid::LinearSpacing xSpacing, grid::Spacing ySpacing ) { - return StructuredGrid{ xSpacing, ySpacing }; - } ), - "x_spacing"_a, "y_spacing"_a ) - .def( - py::init( []( std::vector xLinearSpacings, grid::Spacing ySpacing, Domain const& d ) { - std::vector xSpacings; - std::copy( xLinearSpacings.begin(), xLinearSpacings.end(), std::back_inserter( xSpacings ) ); - 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_ ) ) - .def_property_readonly( "nxmax", &StructuredGrid::nxmax ) - .def_property_readonly( "y", py::overload_cast<>( &StructuredGrid::y, py::const_ ) ) - .def_property_readonly( "x", &StructuredGrid::x ) - .def( "xy", py::overload_cast( &StructuredGrid::xy, py::const_ ), "i"_a, "j"_a ) - .def( "lonlat", py::overload_cast( &StructuredGrid::lonlat, py::const_ ), "i"_a, "j"_a ) - .def_property_readonly( "reduced", &StructuredGrid::reduced ) - .def_property_readonly( "regular", &StructuredGrid::regular ) - .def_property_readonly( "periodic", &StructuredGrid::periodic ); - - py::class_( m, "eckit.Configuration" ); - py::class_( m, "eckit.LocalConfiguration" ); + nb::class_( m, "LinearSpacing" ) + .def( nb::init(), "start"_a, "stop"_a, "N"_a, "endpoint_included"_a = true ); + + + nb::class_( m, "GaussianSpacing" ) + .def( nb::init(), "N"_a ); + + nb::class_( m, "StructuredGrid" ) + .def( nb::init(), "grid"_a, "domain"_a = Domain() ) + .def( nb::init(), "name"_a, "domain"_a = Domain() ) + .def( nb::init(), "x_spacing"_a, "y_spacing"_a ) + .def("__init__", [](StructuredGrid *g, std::vector xLinearSpacings, grid::Spacing ySpacing, Domain const& d) { + std::vector xSpacings; + std::copy( xLinearSpacings.begin(), xLinearSpacings.end(), std::back_inserter( xSpacings ) ); + new (g) StructuredGrid( xSpacings, ySpacing, Projection(), d ); + }, "x_spacings"_a, "y_spacing"_a, "domain"_a = Domain() ) + .def( "__enter__", []( StructuredGrid& self ) { return &self; } ) + .def( "__exit__", []( StructuredGrid& self, nb::object exc_type, nb::object exc_value, nb::object traceback ) { self.reset(nullptr); }, "exc_type"_a = nb::none(), "exc_value"_a = nb::none(), "traceback"_a = nb::none() ) + .def("__bool__", &StructuredGrid::valid ) + .def_prop_ro( "valid", &StructuredGrid::valid ) + .def_prop_ro( "ny", &StructuredGrid::ny ) + .def_prop_ro( "nx", nb::overload_cast<>( &StructuredGrid::nx, nb::const_ ) ) + .def_prop_ro( "nxmax", &StructuredGrid::nxmax ) + .def_prop_ro( "y", nb::overload_cast<>( &StructuredGrid::y, nb::const_ ) ) + .def_prop_ro( "x", &StructuredGrid::x ) + .def( "xy", nb::overload_cast( &StructuredGrid::xy, nb::const_ ), "i"_a, "j"_a ) + .def( "lonlat", nb::overload_cast( &StructuredGrid::lonlat, nb::const_ ), "i"_a, "j"_a ) + .def_prop_ro( "reduced", &StructuredGrid::reduced ) + .def_prop_ro( "regular", &StructuredGrid::regular ) + .def_prop_ro( "periodic", &StructuredGrid::periodic ); + + nb::class_( m, "eckit.Configuration" ); + + nb::class_( m, "eckit.LocalConfiguration" ); // TODO This is a duplicate of metadata below (because same base class) - py::class_( m, "Config" ) - .def( py::init() ) + nb::class_( m, "Config" ) + .def( nb::init() ) .def( "__setitem__", - []( util::Config& config, std::string const& key, py::object value ) { - if ( py::isinstance( value ) ) - config.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - config.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - config.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - config.set( key, value.cast() ); + []( util::Config& config, std::string const& key, nb::object value ) { + if ( nb::isinstance( value ) ) + config.set( key, nb::cast( value ) ); + else if ( nb::isinstance( value ) ) + config.set( key, nb::cast( value ) ); + else if ( nb::isinstance( value ) ) + config.set( key, nb::cast( value ) ); + else if ( nb::isinstance( value ) ) + config.set( key, nb::cast( value ) ); else throw std::out_of_range( "type of value unsupported" ); } ) .def( "__getitem__", - []( util::Config& config, std::string const& key ) -> py::object { + []( util::Config& config, std::string const& key ) -> nb::object { if ( !config.has( key ) ) throw std::out_of_range( "key <" + key + "> could not be found" ); @@ -298,49 +406,90 @@ PYBIND11_MODULE( _atlas4py, m ) { return toPyObject( config, key ); } ) .def( "__repr__", []( util::Config const& config ) { - return "_atlas4py.Config("_s + py::str( toPyObject( config ) ) + ")"_s; + return "_atlas4py.Config("_s + nb::str( toPyObject( config ) ) + ")"_s; } ); - py::class_( m, "StructuredMeshGenerator" ) + nb::class_( m, "Field" ) + .def_prop_ro( "name", &Field::name ) + .def_prop_ro( "strides", &Field::strides ) + .def_prop_ro( "shape", nb::overload_cast<>( &Field::shape, nb::const_ ) ) + .def_prop_ro( "size", &Field::size ) + .def_prop_ro( "rank", &Field::rank ) + .def_prop_ro( "dtype", []( Field& f ) { return atlas4py::dtype::to_python_name(f.datatype()); } ) + .def_prop_ro( "metadata", nb::overload_cast<>( &Field::metadata, nb::const_ ), nb::rv_policy::reference_internal ) + .def("host_array", [](Field& self) { + return make_ndarray(self, MemorySpace::host); + }, nb::rv_policy::reference_internal) + .def("device_array", [](Field& self) { + return make_ndarray(self, MemorySpace::device); + }, nb::rv_policy::reference_internal) + // Numpy array interface, see https://numpy.org/doc/stable/reference/arrays.interface.html + .def("__array__", [](Field &self, nb::handle dtype, nb::handle copy) { + return make_ndarray(self, MemorySpace::host); + }, "dtype"_a = nb::none(), "copy"_a = nb::none(), + nb::rv_policy::reference_internal) + // CuPy array interface, see https://docs.cupy.dev/en/stable/reference/cupy.ndarray.html#cupy.ndarray.__array_interface__ + .def("__cuda_array_interface__", [](Field& self) { + // WARNING: not tested + return make_ndarray(self, MemorySpace::device); + }, nb::rv_policy::reference_internal) + // DLPack interface, see https://data-apis.org/array-api/latest/API_specification/generated/array_api.array.__dlpack__.html + .def("__dlpack__", [](nb::pointer_and_handle self, nb::kwargs kwargs) { + nb::object aa = nb::cast( make_ndarray(*self.p, MemorySpace::host), nb::rv_policy::reference_internal, self.h); + return aa.attr("__dlpack__")(**kwargs); + }) + .def("__dlpack_device__", [](nb::handle /*self*/) { + return std::make_pair(nb::device::cpu::value, 0); + }); + + nb::class_( m, "Mesh" ) + .def_prop_ro( "grid", &Mesh::grid ) + .def_prop_ro( "projection", &Mesh::projection ) + .def_prop_ro( "nodes", nb::overload_cast<>( &Mesh::nodes, nb::const_ )) + .def_prop_ro( "edges", nb::overload_cast<>( &Mesh::edges, nb::const_ )) + .def_prop_ro( "cells", nb::overload_cast<>( &Mesh::cells, nb::const_ )); + + nb::class_( m, "StructuredMeshGenerator" ) // TODO in FunctionSpace below we expose config options, not the whole config object - .def( py::init( []( util::Config const& config ) { return StructuredMeshGenerator( config ); } ) ) - .def( py::init() ) - .def( "generate", py::overload_cast( &StructuredMeshGenerator::generate, py::const_ ) ); - - py::class_( m, "Mesh" ) - .def_property_readonly( "grid", &Mesh::grid ) - .def_property_readonly( "projection", &Mesh::projection ) - .def_property( "nodes", py::overload_cast<>( &Mesh::nodes, py::const_ ), py::overload_cast<>( &Mesh::nodes ) ) - .def_property( "edges", py::overload_cast<>( &Mesh::edges, py::const_ ), py::overload_cast<>( &Mesh::edges ) ) - .def_property( "cells", py::overload_cast<>( &Mesh::cells, py::const_ ), py::overload_cast<>( &Mesh::cells ) ); - m.def( "build_edges", []( Mesh& mesh, std::optional> const& config ) { - if(config) - mesh::actions::build_edges( mesh, config.value().get()); - else - mesh::actions::build_edges( mesh); - }, "mesh"_a, "config"_a = std::nullopt ); + .def( nb::init(), "config"_a ) + .def( nb::init() ) + .def( "generate", nb::overload_cast( &StructuredMeshGenerator::generate, nb::const_ ) ); + + m.def( "build_edges", []( Mesh& mesh, const eckit::Configuration& config ) { + mesh::actions::build_edges( mesh, config); + }, "mesh"_a, "config"_a = util::Config() ); m.def( "build_node_to_edge_connectivity", - py::overload_cast( &mesh::actions::build_node_to_edge_connectivity ) ); + nb::overload_cast( &mesh::actions::build_node_to_edge_connectivity ) ); m.def( "build_element_to_edge_connectivity", - py::overload_cast( &mesh::actions::build_element_to_edge_connectivity ) ); + nb::overload_cast( &mesh::actions::build_element_to_edge_connectivity ) ); m.def( "build_node_to_cell_connectivity", - py::overload_cast( &mesh::actions:: build_node_to_cell_connectivity ) ); - m.def( "build_median_dual_mesh", py::overload_cast( &mesh::actions::build_median_dual_mesh ) ); - m.def( "build_periodic_boundaries", py::overload_cast( &mesh::actions::build_periodic_boundaries ) ); - m.def( "build_halo", py::overload_cast( &mesh::actions::build_halo ) ); - m.def( "build_parallel_fields", py::overload_cast( &mesh::actions::build_parallel_fields ) ); + nb::overload_cast( &mesh::actions::build_node_to_cell_connectivity ) ); + m.def( "build_median_dual_mesh", nb::overload_cast( &mesh::actions::build_median_dual_mesh ) ); + m.def( "build_periodic_boundaries", nb::overload_cast( &mesh::actions::build_periodic_boundaries ) ); + m.def( "build_halo", nb::overload_cast( &mesh::actions::build_halo ) ); + m.def( "build_parallel_fields", nb::overload_cast( &mesh::actions::build_parallel_fields ) ); - py::class_( m, "IrregularConnectivity" ) + nb::class_( m, "IrregularConnectivity" ) .def( "__getitem__", []( mesh::IrregularConnectivity const& c, std::tuple const& pos ) { auto const& [row, col] = pos; return c( row, col ); } ) - .def_property_readonly( "rows", &mesh::IrregularConnectivity::rows ) + .def_prop_ro( "rows", &mesh::IrregularConnectivity::rows ) .def( "cols", &mesh::IrregularConnectivity::cols, "row_idx"_a ) - .def_property_readonly( "maxcols", &mesh::IrregularConnectivity::maxcols ) - .def_property_readonly( "mincols", &mesh::IrregularConnectivity::mincols ); - py::class_( m, "MultiBlockConnectivity" ) + .def_prop_ro( "maxcols", &mesh::IrregularConnectivity::maxcols ) + .def_prop_ro( "mincols", &mesh::IrregularConnectivity::mincols ); + + nb::class_( m, "BlockConnectivity" ) + .def( "__getitem__", + []( mesh::BlockConnectivity const& c, std::tuple const& pos ) { + auto const& [row, col] = pos; + return c( row, col ); + } ) + .def_prop_ro( "rows", &mesh::BlockConnectivity::rows ) + .def_prop_ro( "cols", &mesh::BlockConnectivity::cols ); + + nb::class_( m, "MultiBlockConnectivity" ) .def( "__getitem__", []( mesh::MultiBlockConnectivity const& c, std::tuple const& pos ) { auto const& [row, col] = pos; @@ -351,58 +500,43 @@ PYBIND11_MODULE( _atlas4py, m ) { auto const& [block, row, col] = pos; return c( block, row, col ); } ) - .def_property_readonly( "blocks", &mesh::MultiBlockConnectivity::blocks ) - .def( "block", py::overload_cast( &mesh::MultiBlockConnectivity::block, py::const_ ), py::return_value_policy::reference_internal) - .def_property_readonly( "rows", &mesh::MultiBlockConnectivity::rows ) + .def_prop_ro( "rows", &mesh::MultiBlockConnectivity::rows ) .def( "cols", &mesh::MultiBlockConnectivity::cols, "row_idx"_a ) - .def_property_readonly( "maxcols", &mesh::MultiBlockConnectivity::maxcols ) - .def_property_readonly( "mincols", &mesh::MultiBlockConnectivity::mincols ); - py::class_( m, "BlockConnectivity" ) - .def( "__getitem__", - []( mesh::BlockConnectivity const& c, std::tuple const& pos ) { - auto const& [row, col] = pos; - return c( row, col ); - } ) - .def_property_readonly( "rows", &mesh::BlockConnectivity::rows ) - .def_property_readonly( "cols", &mesh::BlockConnectivity::cols ); - - py::class_( m, "Nodes" ) - .def_property_readonly( "size", &mesh::Nodes::size ) - .def_property_readonly( "edge_connectivity", - py::overload_cast<>( &mesh::Nodes::edge_connectivity, py::const_ ) ) - .def_property_readonly( "cell_connectivity", - py::overload_cast<>( &mesh::Nodes::cell_connectivity, py::const_ ) ) - .def_property_readonly( "lonlat", py::overload_cast<>( &Mesh::Nodes::lonlat, py::const_ ) ) - .def("field", []( mesh::Nodes const& n, std::string const& name ) { return n.field( name ); }, - "name"_a, py::return_value_policy::reference_internal ) - .def( "flags", []( mesh::Nodes const& n ) { return n.flags(); }, - py::return_value_policy::reference_internal); - - py::class_( m, "HybridElements" ) - .def_property_readonly( "size", &mesh::HybridElements::size ) + .def_prop_ro( "maxcols", &mesh::MultiBlockConnectivity::maxcols ) + .def_prop_ro( "mincols", &mesh::MultiBlockConnectivity::mincols ) + .def_prop_ro( "blocks", &mesh::MultiBlockConnectivity::blocks ) + .def( "block", nb::overload_cast( &mesh::MultiBlockConnectivity::block, nb::const_ ), nb::rv_policy::reference_internal ); + + nb::class_( m, "Nodes" ) + .def_prop_ro( "size", &mesh::Nodes::size ) + .def_prop_ro( "edge_connectivity", nb::overload_cast<>( &mesh::Nodes::edge_connectivity, nb::const_ ) ) + .def_prop_ro( "cell_connectivity", nb::overload_cast<>( &mesh::Nodes::cell_connectivity, nb::const_ ) ) + .def_prop_ro( "lonlat", nb::overload_cast<>( &Mesh::Nodes::lonlat, nb::const_ ) ) + .def("field", []( mesh::Nodes const& n, std::string const& name ) { return n.field( name ); }, "name"_a, nb::rv_policy::reference_internal ) + .def( "flags", []( mesh::Nodes const& n ) { return n.flags(); }, nb::rv_policy::reference_internal); + + nb::class_( m, "HybridElements" ) + .def_prop_ro( "size", &mesh::HybridElements::size ) .def( "nb_nodes", &mesh::HybridElements::nb_nodes ) .def( "nb_edges", &mesh::HybridElements::nb_edges ) - .def_property_readonly( "node_connectivity", - py::overload_cast<>( &mesh::HybridElements::node_connectivity, py::const_ ) ) - .def_property_readonly( "edge_connectivity", - py::overload_cast<>( &mesh::HybridElements::edge_connectivity, py::const_ ) ) - .def_property_readonly( "cell_connectivity", - py::overload_cast<>( &mesh::HybridElements::cell_connectivity, py::const_ ) ) - - .def("field", []( mesh::HybridElements const& he, std::string const& name ) { return he.field( name ); }, - "name"_a, py::return_value_policy::reference_internal ) - .def( "flags", []( mesh::HybridElements const& he ) { return he.flags(); }, - py::return_value_policy::reference_internal ); - + .def_prop_ro( "node_connectivity", nb::overload_cast<>( &mesh::HybridElements::node_connectivity, nb::const_ ) ) + .def_prop_ro( "edge_connectivity", nb::overload_cast<>( &mesh::HybridElements::edge_connectivity, nb::const_ ) ) + .def_prop_ro( "cell_connectivity", nb::overload_cast<>( &mesh::HybridElements::cell_connectivity, nb::const_ ) ) + .def( "field", []( mesh::HybridElements const& he, std::string const& name ) { return he.field( name ); }, "name"_a, nb::rv_policy::reference_internal ) + .def( "flags", []( mesh::HybridElements const& he ) { return he.flags(); }, nb::rv_policy::reference_internal ); auto m_fs = m.def_submodule( "functionspace" ); - py::class_( m_fs, "FunctionSpace" ) - .def_property_readonly( "size", &FunctionSpace::size ) - .def_property_readonly( "type", &FunctionSpace::type ) + nb::class_( m_fs, "FunctionSpace" ) + .def_prop_ro( "size", &FunctionSpace::size ) + .def_prop_ro( "type", &FunctionSpace::type ) .def( "create_field", - []( FunctionSpace const& fs, std::optional const& name, std::optional levels, - std::optional variables, py::object dtype ) { + []( FunctionSpace const& fs + , nb::object dtype + , std::optional const& name + , std::optional levels + , std::optional variables + ) { util::Config config; if ( name ) config = config | option::name( *name ); @@ -411,55 +545,47 @@ PYBIND11_MODULE( _atlas4py, m ) { config = config | option::levels( *levels ); if ( variables ) config = config | option::variables( *variables ); - config = config | option::datatype( pybindToAtlas( py::dtype::from_args( dtype ) ) ); + config = config | option::datatype( atlas4py::dtype::from_python_object( dtype ) ); return fs.createField( config ); - }, - "name"_a = std::nullopt, "levels"_a = std::nullopt, "variables"_a = std::nullopt, "dtype"_a ); - py::class_( m_fs, "EdgeColumns" ) - .def( py::init( []( Mesh const& m, int halo ) { - return functionspace::EdgeColumns( m, util::Config()( "halo", halo ) ); - } ), - "mesh"_a, "halo"_a = 0 ) - .def_property_readonly( "nb_edges", &functionspace::EdgeColumns::nb_edges ) - .def_property_readonly( "mesh", &functionspace::EdgeColumns::mesh ) - .def_property_readonly( "edges", &functionspace::EdgeColumns::edges ) - .def_property_readonly( "valid", &functionspace::EdgeColumns::valid ); - py::class_( m_fs, "NodeColumns" ) - .def( py::init( []( Mesh const& m, int halo ) { - return functionspace::NodeColumns( m, util::Config()( "halo", halo ) ); - } ), - "mesh"_a, "halo"_a = 0 ) - .def_property_readonly( "nb_nodes", &functionspace::NodeColumns::nb_nodes ) - .def_property_readonly( "mesh", &functionspace::NodeColumns::mesh ) - .def_property_readonly( "nodes", &functionspace::NodeColumns::nodes ) - .def_property_readonly( "valid", &functionspace::NodeColumns::valid ); - py::class_( m_fs, "CellColumns" ) - .def( py::init( []( Mesh const& m, int halo ) { - return functionspace::CellColumns( m, util::Config()( "halo", halo ) ); - } ), - "mesh"_a, "halo"_a = 0 ) - .def_property_readonly( "nb_cells", &functionspace::CellColumns::nb_cells ) - .def_property_readonly( "mesh", &functionspace::CellColumns::mesh ) - .def_property_readonly( "cells", &functionspace::CellColumns::cells ) - .def_property_readonly( "valid", &functionspace::CellColumns::valid ); - - py::class_( m, "Metadata" ) - .def_property_readonly( "keys", &util::Metadata::keys ) + }, "dtype"_a, "name"_a = std::nullopt, "levels"_a = std::nullopt, "variables"_a = std::nullopt ); + + nb::class_( m_fs, "EdgeColumns" ) + .def("__init__", [](functionspace::EdgeColumns *t, const Mesh&m, int halo) { new (t) functionspace::EdgeColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) + .def_prop_ro( "nb_edges", &functionspace::EdgeColumns::nb_edges ) + .def_prop_ro( "mesh", &functionspace::EdgeColumns::mesh ) + .def_prop_ro( "edges", &functionspace::EdgeColumns::edges ) + .def_prop_ro( "valid", &functionspace::EdgeColumns::valid ); + + nb::class_( m_fs, "NodeColumns" ) + .def("__init__", [](functionspace::NodeColumns *t, const Mesh&m, int halo) { new (t) functionspace::NodeColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) + .def_prop_ro( "nb_nodes", &functionspace::NodeColumns::nb_nodes ) + .def_prop_ro( "mesh", &functionspace::NodeColumns::mesh ) + .def_prop_ro( "nodes", &functionspace::NodeColumns::nodes ) + .def_prop_ro( "valid", &functionspace::NodeColumns::valid ); + nb::class_( m_fs, "CellColumns" ) + .def("__init__", [](functionspace::CellColumns *t, const Mesh&m, int halo) { new (t) functionspace::CellColumns(m, util::Config()( "halo", halo )); }, "mesh"_a, "halo"_a = 0 ) + .def_prop_ro( "nb_cells", &functionspace::CellColumns::nb_cells ) + .def_prop_ro( "mesh", &functionspace::CellColumns::mesh ) + .def_prop_ro( "cells", &functionspace::CellColumns::cells ) + .def_prop_ro( "valid", &functionspace::CellColumns::valid ); + + nb::class_( m, "Metadata" ) + .def_prop_ro( "keys", &util::Metadata::keys ) .def( "__setitem__", - []( util::Metadata& metadata, std::string const& key, py::object value ) { - if ( py::isinstance( value ) ) - metadata.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - metadata.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - metadata.set( key, value.cast() ); - else if ( py::isinstance( value ) ) - metadata.set( key, value.cast() ); + []( util::Metadata& metadata, std::string const& key, nb::object value ) { + if ( nb::isinstance( value ) ) + metadata.set( key, nb::cast(value) ); + else if ( nb::isinstance( value ) ) + metadata.set( key, nb::cast(value) ); + else if ( nb::isinstance( value ) ) + metadata.set( key, nb::cast(value) ); + else if ( nb::isinstance( value ) ) + metadata.set( key, nb::cast(value) ); else throw std::out_of_range( "type of value unsupported" ); } ) .def( "__getitem__", - []( util::Metadata& metadata, std::string const& key ) -> py::object { + []( util::Metadata& metadata, std::string const& key ) -> nb::object { if ( !metadata.has( key ) ) throw std::out_of_range( "key <" + key + "> could not be found" ); @@ -470,38 +596,20 @@ PYBIND11_MODULE( _atlas4py, m ) { return toPyObject( metadata, key ); } ) .def( "__repr__", []( util::Metadata const& metadata ) { - return "_atlas4py.Metadata("_s + py::str( toPyObject( metadata ) ) + ")"_s; - } ); - - py::class_( m, "Field", py::buffer_protocol() ) - .def_property_readonly( "name", &Field::name ) - .def_property_readonly( "strides", &Field::strides ) - .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 ) ) - .def_buffer( []( Field& f ) { - auto strides = f.strides(); - std::transform( strides.begin(), strides.end(), strides.begin(), - [&]( auto const& stride ) { return stride * f.datatype().size(); } ); - return py::buffer_info( f.storage(), f.datatype().size(), atlasToPybind( f.datatype() ), f.rank(), - f.shape(), strides ); + return "_atlas4py.Metadata("_s + nb::str( toPyObject( metadata ) ) + ")"_s; } ); - py::class_ topology( m, "Topology" ); - topology.attr( "NONE" ) = py::cast( int( mesh::Nodes::Topology::NONE ) ); - topology.attr( "GHOST" ) = py::cast( int( mesh::Nodes::Topology::GHOST ) ); - topology.attr( "PERIODIC" ) = py::cast( int( mesh::Nodes::Topology::PERIODIC ) ); - topology.attr( "BC" ) = py::cast( int( mesh::Nodes::Topology::BC ) ); - topology.attr( "WEST" ) = py::cast( int( mesh::Nodes::Topology::WEST ) ); - topology.attr( "EAST" ) = py::cast( int( mesh::Nodes::Topology::EAST ) ); - topology.attr( "NORTH" ) = py::cast( int( mesh::Nodes::Topology::NORTH ) ); - topology.attr( "SOUTH" ) = py::cast( int( mesh::Nodes::Topology::SOUTH ) ); - topology.attr( "PATCH" ) = py::cast( int( mesh::Nodes::Topology::PATCH ) ); - topology.attr( "POLE" ) = py::cast( int( mesh::Nodes::Topology::POLE ) ); + nb::class_ topology( m, "Topology" ); + topology.attr( "NONE" ) = nb::cast( int( mesh::Nodes::Topology::NONE ) ); + topology.attr( "GHOST" ) = nb::cast( int( mesh::Nodes::Topology::GHOST ) ); + topology.attr( "PERIODIC" ) = nb::cast( int( mesh::Nodes::Topology::PERIODIC ) ); + topology.attr( "BC" ) = nb::cast( int( mesh::Nodes::Topology::BC ) ); + topology.attr( "WEST" ) = nb::cast( int( mesh::Nodes::Topology::WEST ) ); + topology.attr( "EAST" ) = nb::cast( int( mesh::Nodes::Topology::EAST ) ); + topology.attr( "NORTH" ) = nb::cast( int( mesh::Nodes::Topology::NORTH ) ); + topology.attr( "SOUTH" ) = nb::cast( int( mesh::Nodes::Topology::SOUTH ) ); + topology.attr( "PATCH" ) = nb::cast( int( mesh::Nodes::Topology::PATCH ) ); + topology.attr( "POLE" ) = nb::cast( int( mesh::Nodes::Topology::POLE ) ); topology.def_static( "reset", &mesh::Nodes::Topology::reset ); topology.def_static( "set", &mesh::Nodes::Topology::set ); topology.def_static( "unset", &mesh::Nodes::Topology::unset ); @@ -510,12 +618,12 @@ PYBIND11_MODULE( _atlas4py, m ) { topology.def_static( "check_all", &mesh::Nodes::Topology::check_all ); topology.def_static( "check_any", &mesh::Nodes::Topology::check_any ); - 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& self, py::object exc_type, py::object exc_val, py::object exc_tb ) { self.reset( nullptr ); } ) + nb::class_( m, "Gmsh" ) + .def( nb::init(), "path"_a ) + .def( "__enter__", []( output::Gmsh& self ) { return &self; } ) + .def( "__exit__", []( output::Gmsh& self, nb::object exc_type, nb::object exc_value, nb::object traceback ) { self.reset(nullptr); }, "exc_type"_a = nb::none(), "exc_value"_a = nb::none(), "traceback"_a = nb::none() ) .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( "write", []( output::Gmsh& gmsh, Field const& field, FunctionSpace const& fs ) { gmsh.write( field, fs ); }, "field"_a, "functionspace"_a ) + .def("__repr__", []( output::Gmsh const& gmsh ) { return "_atlas4py.output.Gmsh()"; } ); }