From b913f8d6d4971f349661225e93a0e1ae908e356c Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Fri, 29 Sep 2023 16:26:49 +0200 Subject: [PATCH 01/30] Add Julia support to gprc4bmi Refs #101 --- README.md | 29 +- docs/container/building.rst | 24 ++ docs/server/Julia.rst | 47 +++ docs/server/index.rst | 1 + grpc4bmi/bmi_julia_model.py | 615 ++++++++++++++++++++++++++++++++++++ grpc4bmi/run_server.py | 14 + pyproject.toml | 1 + 7 files changed, 727 insertions(+), 4 deletions(-) create mode 100644 docs/server/Julia.rst create mode 100644 grpc4bmi/bmi_julia_model.py diff --git a/README.md b/README.md index f6622cd..06f8e98 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ on the client (Python) side. If your server model is implemented in Python, do t pip install grpc4bmi[R] ``` +If the model is implemented in Julia, run instead + +```bash +pip install grpc4bmi[julia] +``` + in the server environment. For bleeding edge version from GitHub use ```bash @@ -90,6 +96,25 @@ For example with [WALRUS](https://github.com/eWaterCycle/grpc4bmi-examples/tree/ run-bmi-server --lang R --path ~/git/eWaterCycle/grpc4bmi-examples/walrus/walrus-bmi.r --name WalrusBmi --port 55555 ``` +### Models written in Julia + +The grpc4bmi Python package can also run BMI models written in Julia if the model has an implementation of the [BasicModelInterface.jl](https://github.com/Deltares/BasicModelInterface.jl). + +Run the Julia model as a server with + +```bash +run-bmi-server --lang julia --name ,, --port +``` + +For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use + +```bash +# Install Wflow.jl package in the Julia environment managed by the juliacall Python package. +python3 -c 'from grpc4bmi.bmi_julia_model import install;install("Wflow")' +# Run the server +run-bmi-server --lang julia --name Wflow,Wflow.BMI,Wflow.Model --port 55555 +``` + ### The client side The client side has only a Python implementation. The default BMI client assumes a running server process on a given port. @@ -154,7 +179,3 @@ pip install -e .[docs] and install the C++ runtime and `protoc` command as described in . After this, simply executing the `proto_gen.sh` script should do the job. - -## Future work - -More language bindings are underway. diff --git a/docs/container/building.rst b/docs/container/building.rst index 6a47255..918ac44 100644 --- a/docs/container/building.rst +++ b/docs/container/building.rst @@ -68,6 +68,30 @@ The WALRUS model has a `Dockerfile`_ file which can be used as an example. .. _Dockerfile: https://github.com/eWaterCycle/grpc4bmi-examples/blob/master/walrus/Dockerfile +Julia +----- + +The docker file for the model container simply contains the installation instructions of grpc4bmi and the BMI-enabled model itself, and as entrypoint the ``run-bmi-server`` command. For the :ref:`python example ` the Docker file will read + +.. code-block:: Dockerfile + + FROM ubuntu:jammy + MAINTAINER your name + + # Install grpc4bmi + RUN pip install grpc4bmi + + # Install your BMI model: + python3 -c 'from grpc4bmi.bmi_julia_model import install;install("")' + + # Run bmi server + ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", ",,"] + + # Expose the magic grpc4bmi port + EXPOSE 55555 + +The port 55555 is the internal port in the Docker container that the model communicates over. It is the default port for ``run_bmi_server`` and also the default port that all clients listen to. + C/C++/Fortran ------------- diff --git a/docs/server/Julia.rst b/docs/server/Julia.rst new file mode 100644 index 0000000..35449b3 --- /dev/null +++ b/docs/server/Julia.rst @@ -0,0 +1,47 @@ +Julia +===== + +Grpc4bmi allows you to wrap a Hydrological model written in the `Julia language`_ into a GRPC server. + +.. _Julia language: https://julialang.org/ + +Creating +-------- + +The model should implement `BasicModelInterface.jl`_. + +.. _BasicModelInterface.jl: https://github.com/Deltares/BasicModelInterface.jl + +See `Wflow.jl`_ for an example. + +.. _Wflow.jl: https://deltares.github.io/Wflow.jl/dev/ + +Running +------- + +Once the model has an BMI interface it can be run as a GRPC server by installing the `grpc4bmi[julia]` Python package with + +.. code-block:: bash + + pip install grpc4bmi[julia] + +The model Julia package must be installed in the Julia environment managed by juliacall, +for Wflow use + +.. code-block:: bash + + python3 -c 'from grpc4bmi.bmi_julia_model import install;install("Wflow")' + +The server can be started with + +.. code-block:: sh + + run-bmi-server --lang julia --name ,, --port + +For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use + +.. code-block:: sh + + run-bmi-server --lang julia --name Wflow,Wflow.BMI,Wflow.Model --port 55555 + +The Python grpc4bmi :ref:`usage` can then be used to connect to the server. diff --git a/docs/server/index.rst b/docs/server/index.rst index 20eb970..5f40a1f 100644 --- a/docs/server/index.rst +++ b/docs/server/index.rst @@ -7,4 +7,5 @@ Creating a BMI server python R + Julia Cpp diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py new file mode 100644 index 0000000..94b02b6 --- /dev/null +++ b/grpc4bmi/bmi_julia_model.py @@ -0,0 +1,615 @@ +from typing import List + +from bmipy import Bmi +import numpy as np +from juliacall import Main as jl + +def install(package): + """Add package to Julia environment. + + Args: + package: Name of package to install. + """ + jl.Pkg.add(package) + +class BmiJulia(Bmi): + """Python Wrapper of a Julia based implementation of BasicModelInterface. + + BasicModelInterface is available in https://github.com/Deltares/BasicModelInterface.jl repo. + + Args: + package: Name of Julia package which contains interface and model classes + implementation_name: Name of Julia variable which implements BasicModelInterface + model_name: Name of Julia model class + + """ + def __init__(self, package, implementation_name, model_name): + self.module = package + self.model_name = model_name + jl.seval("using " + package) + self.model = getattr(getattr(jl, self.module), self.model_name) + self.implementation = getattr(getattr(jl, package), implementation_name) + + def initialize(self, config_file: str) -> None: + """Perform startup tasks for the model. + Perform all tasks that take place before entering the model's time + loop, including opening files and initializing the model state. Model + inputs are read from a text-based configuration file, specified by + `config_file`. + Parameters + ---------- + config_file : str, optional + The path to the model configuration file. + Notes + ----- + Models should be refactored, if necessary, to use a + configuration file. CSDMS does not impose any constraint on + how configuration files are formatted, although YAML is + recommended. A template of a model's configuration file + with placeholder values is used by the BMI. + """ + self.state = self.implementation.initialize(self.model, config_file) + + def update(self) -> None: + """Advance model state by one time step. + Perform all tasks that take place within one pass through the model's + time loop. This typically includes incrementing all of the model's + state variables. If the model's state variables don't change in time, + then they can be computed by the :func:`initialize` method and this + method can return with no action. + """ + self.implementation.update(self.state) + + def update_until(self, time: float) -> None: + """Advance model state until the given time. + Parameters + ---------- + time : float + A model time later than the current model time. + """ + self.implementation.update_until(self.state, time) + + def finalize(self) -> None: + """Perform tear-down tasks for the model. + Perform all tasks that take place after exiting the model's time + loop. This typically includes deallocating memory, closing files and + printing reports. + """ + self.implementation.finalize(self.state) + + def get_component_name(self) -> str: + """Name of the component. + Returns + ------- + str + The name of the component. + """ + return self.implementation.get_component_name(self.state) + + def get_input_item_count(self) -> int: + """Count of a model's input variables. + Returns + ------- + int + The number of input variables. + """ + return self.implementation.get_input_item_count(self.state) + + def get_output_item_count(self) -> int: + """Count of a model's output variables. + Returns + ------- + int + The number of output variables. + """ + return self.implementation.get_output_item_count(self.state) + + def get_input_var_names(self) -> List[str]: + """List of a model's input variables. + Input variable names must be CSDMS Standard Names, also known + as *long variable names*. + Returns + ------- + list of str + The input variables for the model. + Notes + ----- + Standard Names enable the CSDMS framework to determine whether + an input variable in one model is equivalent to, or compatible + with, an output variable in another model. This allows the + framework to automatically connect components. + Standard Names do not have to be used within the model. + """ + return list(self.implementation.get_input_var_names(self.state)) + + def get_output_var_names(self) -> List[str]: + """List of a model's output variables. + Output variable names must be CSDMS Standard Names, also known + as *long variable names*. + Returns + ------- + list of str + The output variables for the model. + """ + return list(self.implementation.get_output_var_names(self.state)) + + def get_var_grid(self, name: str) -> int: + """Get grid identifier for the given variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + int + The grid identifier. + """ + return self.implementation.get_var_grid(self.state, name) + + def get_var_type(self, name: str) -> str: + """Get data type of the given variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + str + The Python variable type; e.g., ``str``, ``int``, ``float``. + """ + return self.implementation.get_var_type(self.state, name) + + def get_var_units(self, name: str) -> str: + """Get units of the given variable. + Standard unit names, in lower case, should be used, such as + ``meters`` or ``seconds``. Standard abbreviations, like ``m`` for + meters, are also supported. For variables with compound units, + each unit name is separated by a single space, with exponents + other than 1 placed immediately after the name, as in ``m s-1`` + for velocity, ``W m-2`` for an energy flux, or ``km2`` for an + area. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + str + The variable units. + Notes + ----- + CSDMS uses the `UDUNITS`_ standard from Unidata. + .. _UDUNITS: http://www.unidata.ucar.edu/software/udunits + """ + return self.implementation.get_var_units(self.state, name) + + def get_var_itemsize(self, name: str) -> int: + """Get memory use for each array element in bytes. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + int + Item size in bytes. + """ + return self.implementation.get_var_itemsize(self.state, name) + + def get_var_nbytes(self, name: str) -> int: + """Get size, in bytes, of the given variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + int + The size of the variable, counted in bytes. + """ + return self.implementation.get_var_nbytes(self.state, name) + + def get_var_location(self, name: str) -> str: + """Get the grid element type that the a given variable is defined on. + The grid topology can be composed of *nodes*, *edges*, and *faces*. + *node* + A point that has a coordinate pair or triplet: the most + basic element of the topology. + *edge* + A line or curve bounded by two *nodes*. + *face* + A plane or surface enclosed by a set of edges. In a 2D + horizontal application one may consider the word “polygon”, + but in the hierarchy of elements the word “face” is most common. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + str + The grid location on which the variable is defined. Must be one of + `"node"`, `"edge"`, or `"face"`. + Notes + ----- + CSDMS uses the `ugrid conventions`_ to define unstructured grids. + .. _ugrid conventions: http://ugrid-conventions.github.io/ugrid-conventions + """ + return self.implementation.get_var_location(self.state, name) + + def get_current_time(self) -> float: + """Current time of the model. + Returns + ------- + float + The current model time. + """ + return self.implementation.get_current_time(self.state) + + def get_start_time(self) -> float: + """Start time of the model. + Model times should be of type float. + Returns + ------- + float + The model start time. + """ + return self.implementation.get_start_time(self.state) + + def get_end_time(self) -> float: + """End time of the model. + Returns + ------- + float + The maximum model time. + """ + return self.implementation.get_end_time(self.state) + + def get_time_units(self) -> str: + """Time units of the model. + Returns + ------- + str + The model time unit; e.g., `days` or `s`. + Notes + ----- + CSDMS uses the UDUNITS standard from Unidata. + """ + return self.implementation.get_time_units(self.state) + + def get_time_step(self) -> float: + """Current time step of the model. + The model time step should be of type float. + Returns + ------- + float + The time step used in model. + """ + return self.implementation.get_time_step(self.state) + + # pylint: disable=arguments-differ + def get_value(self, name: str) -> np.ndarray: + """Get a copy of values of the given variable. + This is a getter for the model, used to access the model's + current state. It returns a *copy* of a model variable, with + the return type, size and rank dependent on the variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + + Returns + ------- + ndarray + A numpy array containing the requested value(s). + """ + return np.array(self.implementation.get_value(self.state, name)) + + def get_value_ptr(self, name: str) -> np.ndarray: + """Get a reference to values of the given variable. + This is a getter for the model, used to access the model's + current state. It returns a reference to a model variable, + with the return type, size and rank dependent on the variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + Returns + ------- + array_like + A reference to a model variable. + """ + raise NotImplementedError( + "This method is incompatible with Julia-Python interface" + ) + + # pylint: disable=arguments-differ + def get_value_at_indices(self, name: str, inds: np.ndarray) -> np.ndarray: + """Get values at particular indices. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + dest : ndarray + A numpy array into which to place the values. + inds : array_like + The indices into the variable array. + Returns + ------- + array_like + Value of the model variable at the given location. + """ + if np.any(inds == 0): + raise ValueError( + "Julia indices start at 1. Please adjust your indices accordingly." + ) + + return np.array( + self.implementation.get_value_at_indices( + self.state, name, jl.convert(jl.Vector[jl.Int64], inds) + ) + ) + + def set_value(self, name: str, values: np.ndarray) -> None: + """Specify a new value for a model variable. + This is the setter for the model, used to change the model's + current state. It accepts, through *values*, a new value for a + model variable, with the type, size and rank of *values* + dependent on the variable. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + values : array_like + The new value for the specified variable. + """ + self.implementation.set_value(self.state, name, jl.convert(jl.Vector, values)) + + def set_value_at_indices( + self, name: str, inds: np.ndarray, src: np.ndarray + ) -> None: + """Specify a new value for a model variable at particular indices. + Parameters + ---------- + name : str + An input or output variable name, a CSDMS Standard Name. + inds : array_like + The indices into the variable array. + src : array_like + The new value for the specified variable. + """ + self.implementation.set_value_at_indices( + self.state, + name, + jl.convert(jl.Vector[jl.Int64], inds), + jl.convert(jl.Vector, src), + ) + + # Grid information + def get_grid_rank(self, grid: int) -> int: + """Get number of dimensions of the computational grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + int + Rank of the grid. + """ + return self.implementation.get_grid_rank(self.state, grid) + + def get_grid_size(self, grid: int) -> int: + """Get the total number of elements in the computational grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + int + Size of the grid. + """ + return self.implementation.get_grid_size(self.state, grid) + + def get_grid_type(self, grid: int) -> str: + """Get the grid type as a string. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + str + Type of grid as a string. + """ + return self.implementation.get_grid_type(self.state, grid) + + # Uniform rectilinear + def get_grid_shape(self, grid: int) -> np.ndarray: + """Get dimensions of the computational grid. + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of int + A numpy array that holds the grid's shape. + """ + return np.array(self.implementation.get_grid_shape(self.state, grid)) + + def get_grid_spacing(self, grid: int) -> np.ndarray: + """Get distance between nodes of the computational grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + ndarray of float + A numpy array that holds the grid's spacing between grid rows and columns. + """ + return np.array(self.implementation.get_grid_spacing(self.state, grid)) + + def get_grid_origin(self, grid: int) -> np.ndarray: + """Get coordinates for the lower-left corner of the computational grid. + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of float + A numpy array that holds the coordinates of the grid's + lower-left corner. + """ + return np.array(self.implementation.get_grid_origin(self.state, grid)) + + # Non-uniform rectilinear, curvilinear + def get_grid_x(self, grid: int) -> np.ndarray: + """Get coordinates of grid nodes in the x direction. + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of float + The input numpy array that holds the grid's column x-coordinates. + """ + return np.array(self.implementation.get_grid_x(self.state, grid)) + + def get_grid_y(self, grid: int) -> np.ndarray: + """Get coordinates of grid nodes in the y direction. + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of float + The input numpy array that holds the grid's row y-coordinates. + """ + return np.array(self.implementation.get_grid_y(self.state, grid)) + + def get_grid_z(self, grid: int) -> np.ndarray: + """Get coordinates of grid nodes in the z direction. + Parameters + ---------- + grid : int + A grid identifier. + z : ndarray of float, shape *(nlayers,)* + A numpy array to hold the z-coordinates of the grid nodes layers. + Returns + ------- + ndarray of float + The input numpy array that holds the grid's layer z-coordinates. + """ + return np.array(self.implementation.get_grid_z(self.state, grid)) + + def get_grid_node_count(self, grid: int) -> int: + """Get the number of nodes in the grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + int + The total number of grid nodes. + """ + return self.implementation.get_grid_node_count(self.state, grid) + + def get_grid_edge_count(self, grid: int) -> int: + """Get the number of edges in the grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + int + The total number of grid edges. + """ + return self.implementation.get_grid_edge_count(self.state, grid) + + def get_grid_face_count(self, grid: int) -> int: + """Get the number of faces in the grid. + Parameters + ---------- + grid : int + A grid identifier. + Returns + ------- + int + The total number of grid faces. + """ + return self.implementation.get_grid_face_count(self.state, grid) + + def get_grid_edge_nodes(self, grid: int) -> np.ndarray: + """Get the edge-node connectivity. + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of int, shape *(2 x nnodes,)* + A numpy array that holds the edge-node connectivity. For each edge, + connectivity is given as node at edge tail, followed by node at + edge head. + """ + return np.array(self.implementation.get_grid_edge_nodes(self.state, grid)) + + def get_grid_face_edges(self, grid: int) -> np.ndarray: + """Get the face-edge connectivity. + + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of int + A numpy array that holds the face-edge connectivity. + """ + return np.array(self.implementation.get_grid_face_edges(self.state, grid)) + + def get_grid_face_nodes(self, grid: int) -> np.ndarray: + """Get the face-node connectivity. + + Parameters + ---------- + grid : int + A grid identifier. + + Returns + ------- + ndarray of int + A numpy array that holds the face-node connectivity. For each face, + the nodes (listed in a counter-clockwise direction) that form the + boundary of the face. + """ + return np.array(self.implementation.get_grid_face_nodes(self.state, grid)) + + def get_grid_nodes_per_face(self, grid: int) -> np.ndarray: + """Get the number of nodes for each face. + Parameters + ---------- + grid : int + A grid identifier. + nodes_per_face : ndarray of int, shape *(nfaces,)* + A numpy array to place the number of nodes per face. + Returns + ------- + ndarray of int, shape *(nfaces,)* + A numpy array that holds the number of nodes per face. + """ + return np.array(self.implementation.get_grid_nodes_per_face(self.state, grid)) diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index 626f265..8dcdcc5 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -24,6 +24,11 @@ except ImportError: BmiR = None +try: + from .bmi_julia_model import BmiJulia +except ImportError: + BmiJulia = None + """ Run server script, turning a BMI implementation into an executable by looping indefinitely, until interrupt signals are handled. The command line tool needs at least a module and class name to instantiate the BMI wrapper class that exposes @@ -73,6 +78,11 @@ def build_r(class_name, source_fn): raise ValueError('Missing R dependencies, install with `pip install grpc4bmi[R]') return BmiR(class_name, source_fn) +def build_julia(name: str): + if not BmiJulia: + raise ValueError('Missing Julia dependencies, install with `pip install grpc4bmi[julia]') + module, implementation_name, model_name = name.split(',') + return BmiJulia(module, implementation_name, model_name) def serve(model, port): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) @@ -110,6 +120,8 @@ def main(argv=sys.argv[1:]): if args.language == "R": model = build_r(args.name, path) + elif args.language == "julia": + model = build_julia(args.name) else: model = build(args.name, path) @@ -142,6 +154,8 @@ def build_parser(): lang_choices = ['python'] if BmiR: lang_choices.append('R') + if BmiJulia: + lang_choices.append('julia') parser.add_argument("--language", default="python", choices=lang_choices, help="Language in which BMI implementation class is written") parser.add_argument("--bmi-version", default="2.0.0", choices=["2.0.0", "0.2"], diff --git a/pyproject.toml b/pyproject.toml index 5cc16c9..9c86158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ [project.optional-dependencies] R = ["rpy2"] +julia = ["juliacall"] dev = [ "build", "pytest", From 57bb251cccf807e28ac21155f50a6a206f92ac69 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 2 Oct 2023 12:06:57 +0200 Subject: [PATCH 02/30] Switch to 2 arg constructor + Add tests for julia --- grpc4bmi/bmi_julia_model.py | 18 +++++----- test/test_julia.py | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 test/test_julia.py diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 94b02b6..39d1339 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -18,17 +18,19 @@ class BmiJulia(Bmi): BasicModelInterface is available in https://github.com/Deltares/BasicModelInterface.jl repo. Args: - package: Name of Julia package which contains interface and model classes - implementation_name: Name of Julia variable which implements BasicModelInterface model_name: Name of Julia model class + implementation_name: Name of Julia variable which implements BasicModelInterface """ - def __init__(self, package, implementation_name, model_name): - self.module = package - self.model_name = model_name - jl.seval("using " + package) - self.model = getattr(getattr(jl, self.module), self.model_name) - self.implementation = getattr(getattr(jl, package), implementation_name) + state = None + + def __init__(self, model_name, implementation_name = 'BasicModelInterface'): + package4model = model_name.split('.')[0] + package4implementation = implementation_name.split('.')[0] + jl.seval("import " + package4model) + jl.seval("import " + package4implementation) + self.model = jl.seval(model_name) + self.implementation = jl.seval(implementation_name) def initialize(self, config_file: str) -> None: """Perform startup tasks for the model. diff --git a/test/test_julia.py b/test/test_julia.py new file mode 100644 index 0000000..a4ee125 --- /dev/null +++ b/test/test_julia.py @@ -0,0 +1,67 @@ +from pathlib import Path +from textwrap import dedent +import numpy as np +import pytest + +try: + from grpc4bmi.bmi_julia_model import install, BmiJulia + from juliacall import Main as jl +except ImportError: + BmiJulia = None + + +@pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") +@pytest.fixture(scope="module") +def install_heat(): + jl.Pkg.add( + url="https://github.com/csdms/bmi-example-julia.git", + rev="d8b354ceddf6b048727c2e214031f43d62964120", + ) + + +@pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") +class TestFakeFailingModel: + @pytest.fixture + def cfg_file(self, tmp_path: Path): + fn = tmp_path / "heat.toml" + fn.write_text( + dedent( + """\ + # Heat model configuration + shape = [6, 8] + spacing = [1.0, 1.0] + origin = [0.0, 0.0] + alpha = 1.0 + """ + ) + ) + return fn + + @pytest.fixture + def model(self, cfg_file): + model = BmiJulia("Heat.Model") + model.initialize(str(cfg_file)) + return model + + @pytest.mark.parametrize( + "fn_name,fn_args,expected", + [ + ("get_component_name", tuple(), "The 2D Heat Equation"), + ('get_input_item_count', tuple(), 1), + ('get_output_item_count', tuple(), 1), + ('get_input_var_names', tuple(), ['plate_surface__temperature']), + ('get_output_var_names', tuple(), ['plate_surface__temperature']), + ('get_start_time', tuple(), 0.0), + ('get_end_time', tuple(), np.Inf), + ('get_time_step', tuple(), 0.25), + ('get_time_units', tuple(), 's'), + ], + ) + def test_methods(self, model: BmiJulia, fn_name, fn_args, expected): + fn = getattr(model, fn_name) + if fn_args == tuple(): + result = fn() + else: + result = fn(*fn_args) + # TODO almost equal + assert result == expected From 3607d5624ba47a6b8b33aeae07f842c7dee320fe Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 2 Oct 2023 16:02:07 +0200 Subject: [PATCH 03/30] Add output arg + get package from model name + more tests --- .github/workflows/ci.yml | 6 +- README.md | 4 +- docs/container/building.rst | 2 +- docs/server/Julia.rst | 4 +- grpc4bmi/bmi_julia_model.py | 118 ++++++++++++++++++++++++------------ grpc4bmi/run_server.py | 3 +- test/test_julia.py | 103 ++++++++++++++++++++++--------- 7 files changed, 166 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52be444..c8bfea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: run: | python -m pip install --upgrade pip wheel pip install -r dev-requirements.txt - pip install -e .[R] + pip install -e .[R,julia] - name: Setup Apptainer uses: eWaterCycle/setup-apptainer@v2 with: @@ -45,6 +45,10 @@ jobs: run: | Rscript -e "install.packages('remotes')" Rscript -e "install.packages('R6')" + - name: Install Julia + uses: julia-actions/setup-julia@v1 + with: + version: '^1.9' - name: Test with pytest run: | pytest -vv --cov=grpc4bmi --cov-report xml diff --git a/README.md b/README.md index 06f8e98..3edca2a 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The grpc4bmi Python package can also run BMI models written in Julia if the mode Run the Julia model as a server with ```bash -run-bmi-server --lang julia --name ,, --port +run-bmi-server --lang julia --name --port ``` For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use @@ -112,7 +112,7 @@ For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use # Install Wflow.jl package in the Julia environment managed by the juliacall Python package. python3 -c 'from grpc4bmi.bmi_julia_model import install;install("Wflow")' # Run the server -run-bmi-server --lang julia --name Wflow,Wflow.BMI,Wflow.Model --port 55555 +run-bmi-server --lang julia --name Wflow.Model --port 55555 ``` ### The client side diff --git a/docs/container/building.rst b/docs/container/building.rst index 918ac44..60e9fef 100644 --- a/docs/container/building.rst +++ b/docs/container/building.rst @@ -85,7 +85,7 @@ The docker file for the model container simply contains the installation instruc python3 -c 'from grpc4bmi.bmi_julia_model import install;install("")' # Run bmi server - ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", ",,"] + ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", ""] # Expose the magic grpc4bmi port EXPOSE 55555 diff --git a/docs/server/Julia.rst b/docs/server/Julia.rst index 35449b3..1e639e5 100644 --- a/docs/server/Julia.rst +++ b/docs/server/Julia.rst @@ -36,12 +36,12 @@ The server can be started with .. code-block:: sh - run-bmi-server --lang julia --name ,, --port + run-bmi-server --lang julia --name --port For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use .. code-block:: sh - run-bmi-server --lang julia --name Wflow,Wflow.BMI,Wflow.Model --port 55555 + run-bmi-server --lang julia --name Wflow.Model --port 55555 The Python grpc4bmi :ref:`usage` can then be used to connect to the server. diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 39d1339..55d8f6c 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -2,7 +2,7 @@ from bmipy import Bmi import numpy as np -from juliacall import Main as jl +from juliacall import Main as jl, ModuleValue, TypeValue def install(package): """Add package to Julia environment. @@ -18,19 +18,30 @@ class BmiJulia(Bmi): BasicModelInterface is available in https://github.com/Deltares/BasicModelInterface.jl repo. Args: - model_name: Name of Julia model class - implementation_name: Name of Julia variable which implements BasicModelInterface - + model: Julia model class + implementation: Julia variable which implements BasicModelInterface """ state = None - def __init__(self, model_name, implementation_name = 'BasicModelInterface'): + @classmethod + def from_name(cls, model_name, implementation_name = 'BasicModelInterface'): + """Construct BmiJulia from Julia model class name and implementation name. + + Args: + model_name: Name of Julia model class + implementation_name: Name of Julia variable which implements BasicModelInterface + """ package4model = model_name.split('.')[0] package4implementation = implementation_name.split('.')[0] jl.seval("import " + package4model) jl.seval("import " + package4implementation) - self.model = jl.seval(model_name) - self.implementation = jl.seval(implementation_name) + model = jl.seval(model_name) + implementation = jl.seval(implementation_name) + return BmiJulia(model, implementation) + + def __init__(self, model: TypeValue, implementation: ModuleValue): + self.model = model + self.implementation = implementation def initialize(self, config_file: str) -> None: """Perform startup tasks for the model. @@ -290,7 +301,7 @@ def get_time_step(self) -> float: return self.implementation.get_time_step(self.state) # pylint: disable=arguments-differ - def get_value(self, name: str) -> np.ndarray: + def get_value(self, name: str, dest: np.ndarray) -> np.ndarray: """Get a copy of values of the given variable. This is a getter for the model, used to access the model's current state. It returns a *copy* of a model variable, with @@ -305,7 +316,8 @@ def get_value(self, name: str) -> np.ndarray: ndarray A numpy array containing the requested value(s). """ - return np.array(self.implementation.get_value(self.state, name)) + self.implementation.get_value(self.state, name, dest) + return dest def get_value_ptr(self, name: str) -> np.ndarray: """Get a reference to values of the given variable. @@ -326,7 +338,7 @@ def get_value_ptr(self, name: str) -> np.ndarray: ) # pylint: disable=arguments-differ - def get_value_at_indices(self, name: str, inds: np.ndarray) -> np.ndarray: + def get_value_at_indices(self, name: str, dest: np.ndarray, inds: np.ndarray) -> np.ndarray: """Get values at particular indices. Parameters ---------- @@ -346,13 +358,16 @@ def get_value_at_indices(self, name: str, inds: np.ndarray) -> np.ndarray: "Julia indices start at 1. Please adjust your indices accordingly." ) - return np.array( - self.implementation.get_value_at_indices( - self.state, name, jl.convert(jl.Vector[jl.Int64], inds) - ) + self.implementation.get_value_at_indices( + self.state, + name, + jl.convert(jl.Vector[jl.Int64], inds), + # inds, + dest ) - - def set_value(self, name: str, values: np.ndarray) -> None: + return dest + + def set_value(self, name: str, src: np.ndarray) -> None: """Specify a new value for a model variable. This is the setter for the model, used to change the model's current state. It accepts, through *values*, a new value for a @@ -365,7 +380,10 @@ def set_value(self, name: str, values: np.ndarray) -> None: values : array_like The new value for the specified variable. """ - self.implementation.set_value(self.state, name, jl.convert(jl.Vector, values)) + self.implementation.set_value(self.state, name, + src, + # jl.convert(jl.Vector, src) + ) def set_value_at_indices( self, name: str, inds: np.ndarray, src: np.ndarray @@ -428,7 +446,7 @@ def get_grid_type(self, grid: int) -> str: return self.implementation.get_grid_type(self.state, grid) # Uniform rectilinear - def get_grid_shape(self, grid: int) -> np.ndarray: + def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: """Get dimensions of the computational grid. Parameters ---------- @@ -440,9 +458,10 @@ def get_grid_shape(self, grid: int) -> np.ndarray: ndarray of int A numpy array that holds the grid's shape. """ - return np.array(self.implementation.get_grid_shape(self.state, grid)) + self.implementation.get_grid_shape(self.state, grid ,shape) + return shape - def get_grid_spacing(self, grid: int) -> np.ndarray: + def get_grid_spacing(self, grid: int, spacing: np.ndarray) -> np.ndarray: """Get distance between nodes of the computational grid. Parameters ---------- @@ -453,9 +472,10 @@ def get_grid_spacing(self, grid: int) -> np.ndarray: ndarray of float A numpy array that holds the grid's spacing between grid rows and columns. """ - return np.array(self.implementation.get_grid_spacing(self.state, grid)) + self.implementation.get_grid_spacing(self.state, grid, spacing) + return spacing - def get_grid_origin(self, grid: int) -> np.ndarray: + def get_grid_origin(self, grid: int, origin: np.ndarray) -> np.ndarray: """Get coordinates for the lower-left corner of the computational grid. Parameters ---------- @@ -468,38 +488,45 @@ def get_grid_origin(self, grid: int) -> np.ndarray: A numpy array that holds the coordinates of the grid's lower-left corner. """ - return np.array(self.implementation.get_grid_origin(self.state, grid)) + self.implementation.get_grid_origin(self.state, grid, origin) + return origin # Non-uniform rectilinear, curvilinear - def get_grid_x(self, grid: int) -> np.ndarray: + def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: """Get coordinates of grid nodes in the x direction. Parameters ---------- grid : int A grid identifier. + x: An array to hold the x-coordinates of the grid nodes. + Returns ------- ndarray of float The input numpy array that holds the grid's column x-coordinates. """ - return np.array(self.implementation.get_grid_x(self.state, grid)) + self.implementation.get_grid_x(self.state, grid, x) + return x - def get_grid_y(self, grid: int) -> np.ndarray: + def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: """Get coordinates of grid nodes in the y direction. Parameters ---------- grid : int A grid identifier. + y: + An array to hold the y-coordinates of the grid nodes. Returns ------- ndarray of float The input numpy array that holds the grid's row y-coordinates. """ - return np.array(self.implementation.get_grid_y(self.state, grid)) + self.implementation.get_grid_y(self.state, grid,y) + return y - def get_grid_z(self, grid: int) -> np.ndarray: + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: """Get coordinates of grid nodes in the z direction. Parameters ---------- @@ -512,7 +539,8 @@ def get_grid_z(self, grid: int) -> np.ndarray: ndarray of float The input numpy array that holds the grid's layer z-coordinates. """ - return np.array(self.implementation.get_grid_z(self.state, grid)) + self.implementation.get_grid_z(self.state, grid, z) + return z def get_grid_node_count(self, grid: int) -> int: """Get the number of nodes in the grid. @@ -553,12 +581,16 @@ def get_grid_face_count(self, grid: int) -> int: """ return self.implementation.get_grid_face_count(self.state, grid) - def get_grid_edge_nodes(self, grid: int) -> np.ndarray: + def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: """Get the edge-node connectivity. Parameters ---------- grid : int A grid identifier. + edge_nodes: An array of integers to place the edge-node connectivity. + For each edge, connectivity is given as node at edge tail, + followed by node at edge head. Therefore this array must be twice + the number of nodes long. Returns ------- @@ -567,31 +599,39 @@ def get_grid_edge_nodes(self, grid: int) -> np.ndarray: connectivity is given as node at edge tail, followed by node at edge head. """ - return np.array(self.implementation.get_grid_edge_nodes(self.state, grid)) + self.implementation.get_grid_edge_nodes(self.state, grid, edge_nodes) + return edge_nodes - def get_grid_face_edges(self, grid: int) -> np.ndarray: + def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: """Get the face-edge connectivity. Parameters ---------- grid : int A grid identifier. + face_edges: + An array of integers in which to place the face-edge connectivity. Returns ------- ndarray of int A numpy array that holds the face-edge connectivity. """ - return np.array(self.implementation.get_grid_face_edges(self.state, grid)) + self.implementation.get_grid_face_edges(self.state, grid, face_edges) + return face_edges - def get_grid_face_nodes(self, grid: int) -> np.ndarray: + def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: """Get the face-node connectivity. Parameters ---------- grid : int A grid identifier. - + face_nodes : ndarray of int + A numpy array to place the face-node connectivity. For each face, + the nodes (listed in a counter-clockwise direction) that form the + boundary of the face. + Returns ------- ndarray of int @@ -599,9 +639,10 @@ def get_grid_face_nodes(self, grid: int) -> np.ndarray: the nodes (listed in a counter-clockwise direction) that form the boundary of the face. """ - return np.array(self.implementation.get_grid_face_nodes(self.state, grid)) + self.implementation.get_grid_face_nodes(self.state, grid, face_nodes) + return face_nodes - def get_grid_nodes_per_face(self, grid: int) -> np.ndarray: + def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: """Get the number of nodes for each face. Parameters ---------- @@ -614,4 +655,5 @@ def get_grid_nodes_per_face(self, grid: int) -> np.ndarray: ndarray of int, shape *(nfaces,)* A numpy array that holds the number of nodes per face. """ - return np.array(self.implementation.get_grid_nodes_per_face(self.state, grid)) + self.implementation.get_grid_nodes_per_face(self.state, grid,nodes_per_face) + return nodes_per_face diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index 8dcdcc5..c4593bf 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -81,8 +81,7 @@ def build_r(class_name, source_fn): def build_julia(name: str): if not BmiJulia: raise ValueError('Missing Julia dependencies, install with `pip install grpc4bmi[julia]') - module, implementation_name, model_name = name.split(',') - return BmiJulia(module, implementation_name, model_name) + return BmiJulia.from_name(name) def serve(model, port): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) diff --git a/test/test_julia.py b/test/test_julia.py index a4ee125..de5e5ca 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -1,45 +1,43 @@ from pathlib import Path from textwrap import dedent import numpy as np +from numpy.testing import assert_array_equal import pytest try: - from grpc4bmi.bmi_julia_model import install, BmiJulia + from grpc4bmi.bmi_julia_model import BmiJulia from juliacall import Main as jl except ImportError: BmiJulia = None - @pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") -@pytest.fixture(scope="module") -def install_heat(): - jl.Pkg.add( - url="https://github.com/csdms/bmi-example-julia.git", - rev="d8b354ceddf6b048727c2e214031f43d62964120", - ) - +class TestJuliaHeatModel: + @pytest.fixture(scope="class") + def install_heat(self): + jl.Pkg.add( + url="https://github.com/csdms/bmi-example-julia.git", + rev="d8b354ceddf6b048727c2e214031f43d62964120", + ) -@pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") -class TestFakeFailingModel: @pytest.fixture def cfg_file(self, tmp_path: Path): fn = tmp_path / "heat.toml" fn.write_text( dedent( """\ - # Heat model configuration - shape = [6, 8] - spacing = [1.0, 1.0] - origin = [0.0, 0.0] - alpha = 1.0 - """ + # Heat model configuration + shape = [6, 8] + spacing = [1.0, 1.0] + origin = [0.0, 0.0] + alpha = 1.0 + """ ) ) return fn @pytest.fixture def model(self, cfg_file): - model = BmiJulia("Heat.Model") + model = BmiJulia.from_name("Heat.Model") model.initialize(str(cfg_file)) return model @@ -47,14 +45,32 @@ def model(self, cfg_file): "fn_name,fn_args,expected", [ ("get_component_name", tuple(), "The 2D Heat Equation"), - ('get_input_item_count', tuple(), 1), - ('get_output_item_count', tuple(), 1), - ('get_input_var_names', tuple(), ['plate_surface__temperature']), - ('get_output_var_names', tuple(), ['plate_surface__temperature']), - ('get_start_time', tuple(), 0.0), - ('get_end_time', tuple(), np.Inf), - ('get_time_step', tuple(), 0.25), - ('get_time_units', tuple(), 's'), + ("get_input_item_count", tuple(), 1), + ("get_output_item_count", tuple(), 1), + ("get_input_var_names", tuple(), ["plate_surface__temperature"]), + ("get_output_var_names", tuple(), ["plate_surface__temperature"]), + ("get_start_time", tuple(), 0.0), + ("get_end_time", tuple(), np.Inf), + ("get_time_step", tuple(), 0.25), + ("get_time_units", tuple(), "s"), + # TODO Float54 is a Julia type, not a numpy type, should use lookup table to + ("get_var_type", ["plate_surface__temperature"], "Float64"), + ("get_var_units", ["plate_surface__temperature"], "K"), + ("get_var_itemsize", ["plate_surface__temperature"], 8), + ("get_var_nbytes", ["plate_surface__temperature"], 384), + ("get_var_grid", ["plate_surface__temperature"], 0), + ("get_var_location", ["plate_surface__temperature"], "node"), + ("get_grid_shape", [0, np.zeros((2,))], [6, 8]), + ("get_grid_spacing", [0, np.zeros((2,))], [1.0, 1.0]), + ("get_grid_origin", [0, np.zeros((2,))], [0.0, 0.0]), + ("get_grid_rank", [0], 2), + ("get_grid_size", [0], 48), + ("get_grid_type", [0], "uniform_rectilinear"), + ("get_grid_node_count", [0], 48), + ("update", tuple(), None), + ("update_until", [2], None), + ("finalize", tuple(), None), + ("get_current_time", tuple(), 0.0), ], ) def test_methods(self, model: BmiJulia, fn_name, fn_args, expected): @@ -63,5 +79,36 @@ def test_methods(self, model: BmiJulia, fn_name, fn_args, expected): result = fn() else: result = fn(*fn_args) - # TODO almost equal - assert result == expected + + try: + assert_array_equal(result, expected) + except: + assert result == expected + + def test_get_value(self, model: BmiJulia): + result = model.get_value("plate_surface__temperature", np.zeros((48,))) + assert result.shape == (48,) + assert result.dtype == np.float64 + # cannot test values as model is initialized with random values + + def test_get_value_ptr(self, model: BmiJulia): + with pytest.raises(NotImplementedError): + model.get_value_ptr("plate_surface__temperature") + + # TODO fix gives no method matching error + # def test_get_value_at_indices(self, model: BmiJulia): + # result = model.get_value_at_indices( + # "plate_surface__temperature", np.zeros((3,)), np.array([5, 6, 7]) + # ) + # assert result.shape == (3,) + # assert result.dtype == np.float64 + # cannot test values as model is initialized with random values + + # TODO fix gives DimensionMismatch error now + # def test_set_value(self, model: BmiJulia): + # model.set_value("plate_surface__temperature", np.ones((48,))) + + # result = model.get_value("plate_surface__temperature", np.zeros((48,))) + # assert_array_equal(result, np.ones((48,))) + +# TODO Heat.jl does not implement all methods, use fake.jl to test all methods not covered by Heat.jl From 2b612dc5d9d1b1f68e08dc1256eb517a26ef3774 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 2 Oct 2023 16:04:42 +0200 Subject: [PATCH 04/30] Convert Julia type to Python type --- grpc4bmi/bmi_julia_model.py | 2 +- test/test_julia.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 55d8f6c..911f799 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -170,7 +170,7 @@ def get_var_type(self, name: str) -> str: str The Python variable type; e.g., ``str``, ``int``, ``float``. """ - return self.implementation.get_var_type(self.state, name) + return self.implementation.get_var_type(self.state, name).lower() def get_var_units(self, name: str) -> str: """Get units of the given variable. diff --git a/test/test_julia.py b/test/test_julia.py index de5e5ca..38ef9c5 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -53,8 +53,7 @@ def model(self, cfg_file): ("get_end_time", tuple(), np.Inf), ("get_time_step", tuple(), 0.25), ("get_time_units", tuple(), "s"), - # TODO Float54 is a Julia type, not a numpy type, should use lookup table to - ("get_var_type", ["plate_surface__temperature"], "Float64"), + ("get_var_type", ["plate_surface__temperature"], "float64"), ("get_var_units", ["plate_surface__temperature"], "K"), ("get_var_itemsize", ["plate_surface__temperature"], 8), ("get_var_nbytes", ["plate_surface__temperature"], 384), From 5d64b15324d9a57a369b42853eb9dfdb760bcc8d Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 2 Oct 2023 16:16:56 +0200 Subject: [PATCH 05/30] Force install of heat.jl --- test/test_julia.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_julia.py b/test/test_julia.py index 38ef9c5..75c9a1f 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -12,7 +12,7 @@ @pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") class TestJuliaHeatModel: - @pytest.fixture(scope="class") + @pytest.fixture(scope="class", autouse=True) def install_heat(self): jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", @@ -59,6 +59,7 @@ def model(self, cfg_file): ("get_var_nbytes", ["plate_surface__temperature"], 384), ("get_var_grid", ["plate_surface__temperature"], 0), ("get_var_location", ["plate_surface__temperature"], "node"), + # TODO spec says order should be y,x not x,y ("get_grid_shape", [0, np.zeros((2,))], [6, 8]), ("get_grid_spacing", [0, np.zeros((2,))], [1.0, 1.0]), ("get_grid_origin", [0, np.zeros((2,))], [0.0, 0.0]), From 45c9879367e7d6d97df460477ff607cd7e924aed Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Mon, 2 Oct 2023 16:37:45 +0200 Subject: [PATCH 06/30] Make sure BasicModelInterface.jl is installed --- test/test_julia.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_julia.py b/test/test_julia.py index 75c9a1f..aeb7941 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -4,8 +4,9 @@ from numpy.testing import assert_array_equal import pytest + try: - from grpc4bmi.bmi_julia_model import BmiJulia + from grpc4bmi.bmi_julia_model import BmiJulia,install from juliacall import Main as jl except ImportError: BmiJulia = None @@ -14,10 +15,14 @@ class TestJuliaHeatModel: @pytest.fixture(scope="class", autouse=True) def install_heat(self): + # TODO for other Julia models do we need to install BasicModelInterface? + # it is dep of Heat.jl, but we use it directly + install('BasicModelInterface') jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", rev="d8b354ceddf6b048727c2e214031f43d62964120", ) + @pytest.fixture def cfg_file(self, tmp_path: Path): From 6bb4e4723593b75d4244ccbc44128417dd0c425c Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 5 Oct 2023 10:18:48 +0200 Subject: [PATCH 07/30] Upgrade sonar action To resolve: warning The version of Java (11.0.3) you have used to run this analysis is deprecated and we will stop accepting it soon. Please update to at least Java 17. --- .github/workflows/ci.yml | 2 +- grpc4bmi/bmi_julia_model.py | 3 +++ test/test_julia.py | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8bfea6..e81500c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - name: Correct coverage paths run: sed -i "s+$PWD/++g" coverage.xml - name: SonarCloud analysis - uses: sonarsource/sonarcloud-github-action@v1.3 + uses: sonarsource/sonarcloud-github-action@v2.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 911f799..d55aae1 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -29,7 +29,10 @@ def from_name(cls, model_name, implementation_name = 'BasicModelInterface'): Args: model_name: Name of Julia model class + The package of model_name should be installed. implementation_name: Name of Julia variable which implements BasicModelInterface + The package of implementation_name should be installed. + Uses https://juliapackages.com/p/basicmodelinterface by default. """ package4model = model_name.split('.')[0] package4implementation = implementation_name.split('.')[0] diff --git a/test/test_julia.py b/test/test_julia.py index aeb7941..4fd701c 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -64,7 +64,8 @@ def model(self, cfg_file): ("get_var_nbytes", ["plate_surface__temperature"], 384), ("get_var_grid", ["plate_surface__temperature"], 0), ("get_var_location", ["plate_surface__temperature"], "node"), - # TODO spec says order should be y,x not x,y + # result of get_grid_shape is incompatible with spec, + # as it says order should be y,x not x,y ("get_grid_shape", [0, np.zeros((2,))], [6, 8]), ("get_grid_spacing", [0, np.zeros((2,))], [1.0, 1.0]), ("get_grid_origin", [0, np.zeros((2,))], [0.0, 0.0]), From 4f1de4efb41e6247a917e50976a2fd35279ae745 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 5 Oct 2023 10:23:09 +0200 Subject: [PATCH 08/30] Dont need BasicModelInterface.jl to be installed explicitly? --- test/test_julia.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_julia.py b/test/test_julia.py index 4fd701c..0dd35aa 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -6,7 +6,7 @@ try: - from grpc4bmi.bmi_julia_model import BmiJulia,install + from grpc4bmi.bmi_julia_model import BmiJulia from juliacall import Main as jl except ImportError: BmiJulia = None @@ -15,9 +15,6 @@ class TestJuliaHeatModel: @pytest.fixture(scope="class", autouse=True) def install_heat(self): - # TODO for other Julia models do we need to install BasicModelInterface? - # it is dep of Heat.jl, but we use it directly - install('BasicModelInterface') jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", rev="d8b354ceddf6b048727c2e214031f43d62964120", From 340067ef2dd96084ddb91e7436e36d4adf6b92f5 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 16 Oct 2023 12:33:30 +0200 Subject: [PATCH 09/30] Revert "Dont need BasicModelInterface.jl to be installed explicitly?" This reverts commit 4f1de4efb41e6247a917e50976a2fd35279ae745. --- test/test_julia.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_julia.py b/test/test_julia.py index 0dd35aa..4fd701c 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -6,7 +6,7 @@ try: - from grpc4bmi.bmi_julia_model import BmiJulia + from grpc4bmi.bmi_julia_model import BmiJulia,install from juliacall import Main as jl except ImportError: BmiJulia = None @@ -15,6 +15,9 @@ class TestJuliaHeatModel: @pytest.fixture(scope="class", autouse=True) def install_heat(self): + # TODO for other Julia models do we need to install BasicModelInterface? + # it is dep of Heat.jl, but we use it directly + install('BasicModelInterface') jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", rev="d8b354ceddf6b048727c2e214031f43d62964120", From ceff03c40439bc1f8807c0e738168383342cf7f9 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 16 Oct 2023 13:43:54 +0200 Subject: [PATCH 10/30] Try to test against a jl file --- test/fake.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/fake.jl diff --git a/test/fake.jl b/test/fake.jl new file mode 100644 index 0000000..a7184c8 --- /dev/null +++ b/test/fake.jl @@ -0,0 +1,15 @@ +module FakeModel + +import BasicModelInterface as BMI + +Base.@kwdef mutable struct Model + time::Float64 = 0.0 +end + +BMI.initialize(::Type{Model}) = Model() + +BMI.get_grid_x(m::Model, grid, x) + copyto!(x, [0.0, 1.0]) +end + +end From ac4fdac9e234b65ae342ba46078d06cac153d11d Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 16 Oct 2023 13:45:14 +0200 Subject: [PATCH 11/30] Try to test against a jl file (python side) --- test/fake.jl | 2 +- test/test_julia.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/test/fake.jl b/test/fake.jl index a7184c8..db63a64 100644 --- a/test/fake.jl +++ b/test/fake.jl @@ -9,7 +9,7 @@ end BMI.initialize(::Type{Model}) = Model() BMI.get_grid_x(m::Model, grid, x) - copyto!(x, [0.0, 1.0]) + copyto!(x, [1.0, 2.0]) end end diff --git a/test/test_julia.py b/test/test_julia.py index 4fd701c..5e62c43 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -11,7 +11,7 @@ except ImportError: BmiJulia = None -@pytest.mark.skipif(not BmiJulia, reason="R and its dependencies are not installed") +@pytest.mark.skipif(not BmiJulia, reason="Julia and its dependencies are not installed") class TestJuliaHeatModel: @pytest.fixture(scope="class", autouse=True) def install_heat(self): @@ -117,4 +117,49 @@ def test_get_value_ptr(self, model: BmiJulia): # result = model.get_value("plate_surface__temperature", np.zeros((48,))) # assert_array_equal(result, np.ones((48,))) + # TODO test set_value_at_indices method + # TODO Heat.jl does not implement all methods, use fake.jl to test all methods not covered by Heat.jl +""" +To test + +get_grid_x +get_grid_y +get_grid_z +get_grid_edge_count +get_grid_face_count +get_grid_edge_nodes +get_grid_face_edges +get_grid_face_nodes +get_grid_nodes_per_face +""" +@pytest.mark.skipif(not BmiJulia, reason="Julia and its dependencies are not installed") +class TestJuliaFakeModel: + @pytest.fixture(scope="class", autouse=True) + def install_fake(self): + install('BasicModelInterface') + + @pytest.fixture + def model(self, cfg_file): + jl.seval('include("fake.jl")') + model = BmiJulia.from_name(".FakeModel.Model", ".FakeModel.BMI") + model.initialize(str(cfg_file)) + return model + + @pytest.mark.parametrize( + "fn_name,fn_args,expected", + [ + ("get_grid_x", [0, np.zeros((2,))], [1, 2]), + ], + ) + def test_methods(self, model: BmiJulia, fn_name, fn_args, expected): + fn = getattr(model, fn_name) + if fn_args == tuple(): + result = fn() + else: + result = fn(*fn_args) + + try: + assert_array_equal(result, expected) + except: + assert result == expected \ No newline at end of file From d5d38cddb37484bae6b06e46e62c6e382af56860 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 16 Oct 2023 16:00:39 +0200 Subject: [PATCH 12/30] Fake model.jl does not config file --- test/test_julia.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_julia.py b/test/test_julia.py index 5e62c43..48c3642 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -140,10 +140,10 @@ def install_fake(self): install('BasicModelInterface') @pytest.fixture - def model(self, cfg_file): + def model(self): jl.seval('include("fake.jl")') model = BmiJulia.from_name(".FakeModel.Model", ".FakeModel.BMI") - model.initialize(str(cfg_file)) + model.initialize('') return model @pytest.mark.parametrize( From badc5f78fb47141e7eaf4e29814c542368ed7b0e Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 11:55:14 +0200 Subject: [PATCH 13/30] More tests + py index to julia index + 1 --- grpc4bmi/bmi_julia_model.py | 15 +++------ test/fake.jl | 34 ++++++++++++++++++-- test/test_julia.py | 64 ++++++++++++++++++------------------- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index d55aae1..0c380ee 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -356,17 +356,11 @@ def get_value_at_indices(self, name: str, dest: np.ndarray, inds: np.ndarray) -> array_like Value of the model variable at the given location. """ - if np.any(inds == 0): - raise ValueError( - "Julia indices start at 1. Please adjust your indices accordingly." - ) - self.implementation.get_value_at_indices( self.state, name, - jl.convert(jl.Vector[jl.Int64], inds), - # inds, - dest + jl.convert(jl.Vector[jl.Int64], inds + 1), + dest, ) return dest @@ -384,8 +378,7 @@ def set_value(self, name: str, src: np.ndarray) -> None: The new value for the specified variable. """ self.implementation.set_value(self.state, name, - src, - # jl.convert(jl.Vector, src) + jl.convert(jl.Vector, src) ) def set_value_at_indices( @@ -404,7 +397,7 @@ def set_value_at_indices( self.implementation.set_value_at_indices( self.state, name, - jl.convert(jl.Vector[jl.Int64], inds), + jl.convert(jl.Vector[jl.Int64], inds + 1), jl.convert(jl.Vector, src), ) diff --git a/test/fake.jl b/test/fake.jl index db63a64..6aa4801 100644 --- a/test/fake.jl +++ b/test/fake.jl @@ -3,13 +3,41 @@ module FakeModel import BasicModelInterface as BMI Base.@kwdef mutable struct Model - time::Float64 = 0.0 end -BMI.initialize(::Type{Model}) = Model() +BMI.initialize(::Type{Model}, config_file) = Model() -BMI.get_grid_x(m::Model, grid, x) +BMI.get_component_name(m::Model) = "The 2D Heat Equation" + +function BMI.get_grid_x(m::Model, grid, x) copyto!(x, [1.0, 2.0]) end +function BMI.get_grid_y(m::Model, grid, y) + copyto!(y, [3.0, 4.0]) +end + +function BMI.get_grid_z(m::Model, grid, z) + copyto!(z, [5.0, 6.0]) end + +BMI.get_grid_edge_count(m::Model, grid) = 10 +BMI.get_grid_face_count(m::Model, grid) = 11 + +function BMI.get_grid_edge_nodes(m::Model, grid, edge_nodes) + copyto!(edge_nodes, [7.0, 8.0]) +end + +function BMI.get_grid_face_edges(m::Model, grid, face_edges) + copyto!(face_edges, [9.0, 10.0]) +end + +function BMI.get_grid_face_nodes(m::Model, grid, face_nodes) + copyto!(face_nodes, [11.0, 12.0]) +end + +function BMI.get_grid_nodes_per_face(m::Model, grid, nodes_per_face) + copyto!(nodes_per_face, [13.0, 14.0]) +end + +end # FakeModel module diff --git a/test/test_julia.py b/test/test_julia.py index 48c3642..1ae246b 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -20,7 +20,7 @@ def install_heat(self): install('BasicModelInterface') jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", - rev="d8b354ceddf6b048727c2e214031f43d62964120", + rev="80c34b4f2217599e600fe9372b1bae50e1229edf", ) @@ -102,47 +102,39 @@ def test_get_value_ptr(self, model: BmiJulia): model.get_value_ptr("plate_surface__temperature") # TODO fix gives no method matching error - # def test_get_value_at_indices(self, model: BmiJulia): - # result = model.get_value_at_indices( - # "plate_surface__temperature", np.zeros((3,)), np.array([5, 6, 7]) - # ) - # assert result.shape == (3,) - # assert result.dtype == np.float64 + def test_get_value_at_indices(self, model: BmiJulia): + result = model.get_value_at_indices( + "plate_surface__temperature", np.zeros((3,)), np.array([5, 6, 7]) + ) + assert result.shape == (3,) + assert result.dtype == np.float64 # cannot test values as model is initialized with random values - # TODO fix gives DimensionMismatch error now - # def test_set_value(self, model: BmiJulia): - # model.set_value("plate_surface__temperature", np.ones((48,))) - - # result = model.get_value("plate_surface__temperature", np.zeros((48,))) - # assert_array_equal(result, np.ones((48,))) - - # TODO test set_value_at_indices method - -# TODO Heat.jl does not implement all methods, use fake.jl to test all methods not covered by Heat.jl -""" -To test - -get_grid_x -get_grid_y -get_grid_z -get_grid_edge_count -get_grid_face_count -get_grid_edge_nodes -get_grid_face_edges -get_grid_face_nodes -get_grid_nodes_per_face -""" + def test_set_value(self, model: BmiJulia): + model.set_value("plate_surface__temperature", np.ones((48,))) + + result = model.get_value("plate_surface__temperature", np.zeros((48,))) + assert_array_equal(result, np.ones((48,))) + + def test_set_value_at_indices(self, model: BmiJulia): + model.set_value_at_indices( + "plate_surface__temperature", np.array([5, 6, 7]), np.ones((3,)) + ) + + result = model.get_value("plate_surface__temperature", np.zeros((48,))) + assert_array_equal(result[5:8], np.ones((3,))) + +# Heat.jl does not implement all methods, use fake.jl to test all methods not covered by Heat.jl @pytest.mark.skipif(not BmiJulia, reason="Julia and its dependencies are not installed") class TestJuliaFakeModel: @pytest.fixture(scope="class", autouse=True) def install_fake(self): install('BasicModelInterface') + jl.seval('include("test/fake.jl")') @pytest.fixture def model(self): - jl.seval('include("fake.jl")') - model = BmiJulia.from_name(".FakeModel.Model", ".FakeModel.BMI") + model = BmiJulia.from_name("Main.FakeModel.Model", "Main.FakeModel.BMI") model.initialize('') return model @@ -150,6 +142,14 @@ def model(self): "fn_name,fn_args,expected", [ ("get_grid_x", [0, np.zeros((2,))], [1, 2]), + ("get_grid_y", [0, np.zeros((2,))], [3, 4]), + ("get_grid_z", [0, np.zeros((2,))], [5, 6]), + ('get_grid_edge_count', [0], 10), + ('get_grid_face_count', [0], 11), + ('get_grid_edge_nodes',[0, np.zeros((2,))], [7, 8]), + ('get_grid_face_edges',[0, np.zeros((2,))], [9,10]), + ('get_grid_face_nodes',[0, np.zeros((2,))], [11, 12]), + ('get_grid_nodes_per_face',[0, np.zeros((2,))], [13, 14]), ], ) def test_methods(self, model: BmiJulia, fn_name, fn_args, expected): From a8d4e5940c745326f3325837fa51ddc22a956c95 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 11:59:58 +0200 Subject: [PATCH 14/30] Fix get_value_at_indices --- grpc4bmi/bmi_julia_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 0c380ee..a817a6d 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -359,8 +359,8 @@ def get_value_at_indices(self, name: str, dest: np.ndarray, inds: np.ndarray) -> self.implementation.get_value_at_indices( self.state, name, - jl.convert(jl.Vector[jl.Int64], inds + 1), dest, + inds + 1 ) return dest @@ -397,7 +397,7 @@ def set_value_at_indices( self.implementation.set_value_at_indices( self.state, name, - jl.convert(jl.Vector[jl.Int64], inds + 1), + inds + 1, jl.convert(jl.Vector, src), ) From daeca16aee092b338a2f792ea37ad15b4110c5c1 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 14:37:33 +0200 Subject: [PATCH 15/30] Make implementatation name settable from cli --- docs/server/Julia.rst | 4 ++-- grpc4bmi/run_server.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/server/Julia.rst b/docs/server/Julia.rst index 1e639e5..4fdb513 100644 --- a/docs/server/Julia.rst +++ b/docs/server/Julia.rst @@ -36,12 +36,12 @@ The server can be started with .. code-block:: sh - run-bmi-server --lang julia --name --port + run-bmi-server --lang julia --name [,IMPLEMENTATION-NAME] --port For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use .. code-block:: sh - run-bmi-server --lang julia --name Wflow.Model --port 55555 + run-bmi-server --lang julia --name Wflow.Model,Wflow.bmi.BMI --port 55555 The Python grpc4bmi :ref:`usage` can then be used to connect to the server. diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index c4593bf..a850143 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -78,10 +78,10 @@ def build_r(class_name, source_fn): raise ValueError('Missing R dependencies, install with `pip install grpc4bmi[R]') return BmiR(class_name, source_fn) -def build_julia(name: str): +def build_julia(name: str, implementation_name: str = 'BasicModelInterface'): if not BmiJulia: raise ValueError('Missing Julia dependencies, install with `pip install grpc4bmi[julia]') - return BmiJulia.from_name(name) + return BmiJulia.from_name(name, implementation_name) def serve(model, port): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) @@ -120,7 +120,13 @@ def main(argv=sys.argv[1:]): if args.language == "R": model = build_r(args.name, path) elif args.language == "julia": - model = build_julia(args.name) + names = args.name.split(',') + if len(names) == 2: + model = build_julia(names[0], names[1]) + else: + model = build_julia(names[0]) + # model.initialize('t') + # print(model.get_component_name()) else: model = build(args.name, path) From 575b0f99c33d0704b83b650a4b10a8dc91787be1 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 15:36:13 +0200 Subject: [PATCH 16/30] Trying to call Julia from C --- test/heat-images/c-julia/CMakeLists.txt | 25 +++++++ test/heat-images/c-julia/Dockerfile | 80 ++++++++++++++++++++++ test/heat-images/c-julia/run_bmi_server.cc | 23 +++++++ 3 files changed, 128 insertions(+) create mode 100644 test/heat-images/c-julia/CMakeLists.txt create mode 100644 test/heat-images/c-julia/Dockerfile create mode 100644 test/heat-images/c-julia/run_bmi_server.cc diff --git a/test/heat-images/c-julia/CMakeLists.txt b/test/heat-images/c-julia/CMakeLists.txt new file mode 100644 index 0000000..65c0e98 --- /dev/null +++ b/test/heat-images/c-julia/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.0) +project(run_bmi_server C CXX) + +set(_cflags "-ansi -Wall -Wundef -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -O2") +find_package(PkgConfig REQUIRED) +pkg_check_modules(BMIC REQUIRED IMPORTED_TARGET bmic) +message("-- bmic include - ${BMIC_INCLUDE_DIRS}") +include_directories(${BMIC_INCLUDE_DIRS}) +pkg_check_modules(GRPC4BMI REQUIRED grpc4bmi) +# See https://github.com/barche/embedding-julia + +find_package(Julia REQUIRED) +add_definitions(-DJULIA_ENABLE_THREADING) + +add_executable(run_bmi_server run_bmi_server.cc) + +target_include_directories(run_bmi_server PRIVATE "$") + +target_link_libraries(run_bmi_server + ${GRPC4BMI_LINK_LIBRARIES} + $ + ) + +install(TARGETS run_bmi_server + RUNTIME DESTINATION bin) diff --git a/test/heat-images/c-julia/Dockerfile b/test/heat-images/c-julia/Dockerfile new file mode 100644 index 0000000..06e4679 --- /dev/null +++ b/test/heat-images/c-julia/Dockerfile @@ -0,0 +1,80 @@ +# Build with +# +# docker build -t heat:c-julia -f test/heat-images/c-julia/Dockerfile . +# +# Run with +# +# ipython +# from grpc4bmi.bmi_client_docker import BmiClientDocker +# model = BmiClientDocker('heat:c-julia', work_dir='/tmp', delay=1) +# model.get_component_name() +# model.get_output_var_names() +# from pathlib import Path +# config = Path('/tmp/heat.config.txt') +# config.write_text('1.5, 8.0, 7, 6') +# model.initialize(str(config)) +# model.update() +# import numpy as np +# model.get_value('plate_surface__temperature', np.zeros(42)) +# +FROM julia:bullseye AS builder + +# Install build deps +RUN apt-get update && apt-get install -qy git build-essential cmake autoconf libtool pkg-config libssl-dev + +# Compile gRPC +WORKDIR /opt/grpc +ARG GRPC_VERSION=v1.51.1 +RUN echo ${GRPC_VERSION} +RUN git clone -b ${GRPC_VERSION} --depth 1 https://github.com/grpc/grpc . && git submodule update --init +WORKDIR /opt/grpc/cmake/build +RUN cmake -DgRPC_INSTALL=ON -DgRPC_SSL_PROVIDER=package -DgRPC_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=ON ../.. \ + && make -j6 && make install && ldconfig + +# Install bmi-cxx +ARG BMICXX_VERSION=v2.0 +RUN git clone -b ${BMICXX_VERSION} https://github.com/csdms/bmi-cxx.git /opt/bmi-cxx +WORKDIR /opt/bmi-cxx/build +RUN cmake .. && make install + +# Install bmi-c +ARG BMIC_VERSION=v2.1 +RUN git clone -b ${BMIC_VERSION} https://github.com/csdms/bmi-c.git /opt/bmi-c +WORKDIR /opt/bmi-c/build +RUN cmake .. && make install + +# Install heat-julia +ARG HEAT_VERSION=80c34b4f2217599e600fe9372b1bae50e1229edf +RUN git clone -b ${HEAT_VERSION} https://github.com/csdms/bmi-example-julia.git /opt/bmi-example-julia +WORKDIR /opt/bmi-example-julia/build +RUN cmake .. && make install && ldconfig + +# Install grpc4bmi, use commit c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 +# head of bmi2 branch, PR https://github.com/eWaterCycle/grpc4bmi/pull/124 +ARG GRPC4BMI_VERSION=c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 +RUN git clone https://github.com/eWaterCycle/grpc4bmi /opt/grpc4bmi \ + && cd /opt/grpc4bmi && git checkout ${GRPC4BMI_VERSION} +WORKDIR /opt/grpc4bmi/cpp/build +RUN cmake .. && make install + +# Compile main +WORKDIR /opt/grpc4bmiheatc-julia/build +COPY test/heat-images/c-julia/run_bmi_server.cc /opt/grpc4bmiheatc-julia-julia +COPY test/heat-images/c-julia/CMakeLists.txt /opt/grpc4bmiheatc-julia +RUN cmake .. && make install + +# run container +FROM julia:bullseye + +# Install runtime deps +RUN apt-get update && apt-get install -qy libssl1.1 && rm -rf /var/lib/apt/lists/* + +# Copy compiled and deps +COPY --from=builder /usr/local/bin/run_bmi_server /usr/local/bin/run_bmi_server +COPY --from=builder /usr/local/lib/ /usr/local/lib/ + +RUN ldconfig + +ENV BMI_PORT=50051 + +ENTRYPOINT ["/usr/local/bin/run_bmi_server"] diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc new file mode 100644 index 0000000..6360f9e --- /dev/null +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -0,0 +1,23 @@ +// #include "bmi_grpc_server.h" +// #include +// #include "bmi_heat.h" +#include + +int main(int argc, char* argv[]) +{ + jl_init(); + { + { + // Simple running Julia code + + jl_eval_string("println(sqrt(2.0))"); + } + + // Bmi *model = (Bmi *) malloc(sizeof(Bmi)); + + // run_bmi_server(model, argc, argv); + } + int ret = 0; + jl_atexit_hook(ret); + return ret; +} \ No newline at end of file From fe42fc780eb3ffb2386d8b9cd7860f01fb2dc856 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 16:16:58 +0200 Subject: [PATCH 17/30] Calling sqrt in julia from c works, now try to call https://github.com/csdms/bmi-example-julia methods --- grpc4bmi/run_server.py | 2 - test/heat-images/c-julia/CMakeLists.txt | 8 +- test/heat-images/c-julia/Dockerfile | 17 +-- test/heat-images/c-julia/FindJulia.cmake | 133 +++++++++++++++++++++ test/heat-images/c-julia/run_bmi_server.cc | 18 ++- 5 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 test/heat-images/c-julia/FindJulia.cmake diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index a850143..676976f 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -125,8 +125,6 @@ def main(argv=sys.argv[1:]): model = build_julia(names[0], names[1]) else: model = build_julia(names[0]) - # model.initialize('t') - # print(model.get_component_name()) else: model = build(args.name, path) diff --git a/test/heat-images/c-julia/CMakeLists.txt b/test/heat-images/c-julia/CMakeLists.txt index 65c0e98..1284d9e 100644 --- a/test/heat-images/c-julia/CMakeLists.txt +++ b/test/heat-images/c-julia/CMakeLists.txt @@ -1,15 +1,21 @@ cmake_minimum_required(VERSION 3.0) project(run_bmi_server C CXX) +set(CMAKE_MACOSX_RPATH 1) +set(CMAKE_SKIP_BUILD_RPATH FALSE) +set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) + set(_cflags "-ansi -Wall -Wundef -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -O2") find_package(PkgConfig REQUIRED) pkg_check_modules(BMIC REQUIRED IMPORTED_TARGET bmic) message("-- bmic include - ${BMIC_INCLUDE_DIRS}") include_directories(${BMIC_INCLUDE_DIRS}) pkg_check_modules(GRPC4BMI REQUIRED grpc4bmi) -# See https://github.com/barche/embedding-julia +# See https://github.com/barche/embedding-julia +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) find_package(Julia REQUIRED) + add_definitions(-DJULIA_ENABLE_THREADING) add_executable(run_bmi_server run_bmi_server.cc) diff --git a/test/heat-images/c-julia/Dockerfile b/test/heat-images/c-julia/Dockerfile index 06e4679..7d4f8bc 100644 --- a/test/heat-images/c-julia/Dockerfile +++ b/test/heat-images/c-julia/Dockerfile @@ -43,12 +43,6 @@ RUN git clone -b ${BMIC_VERSION} https://github.com/csdms/bmi-c.git /opt/bmi-c WORKDIR /opt/bmi-c/build RUN cmake .. && make install -# Install heat-julia -ARG HEAT_VERSION=80c34b4f2217599e600fe9372b1bae50e1229edf -RUN git clone -b ${HEAT_VERSION} https://github.com/csdms/bmi-example-julia.git /opt/bmi-example-julia -WORKDIR /opt/bmi-example-julia/build -RUN cmake .. && make install && ldconfig - # Install grpc4bmi, use commit c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 # head of bmi2 branch, PR https://github.com/eWaterCycle/grpc4bmi/pull/124 ARG GRPC4BMI_VERSION=c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 @@ -57,9 +51,11 @@ RUN git clone https://github.com/eWaterCycle/grpc4bmi /opt/grpc4bmi \ WORKDIR /opt/grpc4bmi/cpp/build RUN cmake .. && make install + # Compile main WORKDIR /opt/grpc4bmiheatc-julia/build -COPY test/heat-images/c-julia/run_bmi_server.cc /opt/grpc4bmiheatc-julia-julia +COPY test/heat-images/c-julia/FindJulia.cmake /opt/grpc4bmiheatc-julia +COPY test/heat-images/c-julia/run_bmi_server.cc /opt/grpc4bmiheatc-julia COPY test/heat-images/c-julia/CMakeLists.txt /opt/grpc4bmiheatc-julia RUN cmake .. && make install @@ -73,7 +69,12 @@ RUN apt-get update && apt-get install -qy libssl1.1 && rm -rf /var/lib/apt/lists COPY --from=builder /usr/local/bin/run_bmi_server /usr/local/bin/run_bmi_server COPY --from=builder /usr/local/lib/ /usr/local/lib/ -RUN ldconfig +# Install heat-julia +RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' + +RUN curl -L -O /usr/local/share/bmi/heat.toml https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml + +RUN echo '/usr/local/julia/lib' > /etc/ld.so.conf.d/julia.conf && ldconfig ENV BMI_PORT=50051 diff --git a/test/heat-images/c-julia/FindJulia.cmake b/test/heat-images/c-julia/FindJulia.cmake new file mode 100644 index 0000000..6af9f6e --- /dev/null +++ b/test/heat-images/c-julia/FindJulia.cmake @@ -0,0 +1,133 @@ + +if(Julia_FOUND) + return() +endif() + +#################### +# Julia Executable # +#################### + +find_program(Julia_EXECUTABLE julia DOC "Julia executable") +MESSAGE(STATUS "Julia_EXECUTABLE: ${Julia_EXECUTABLE}") + +################# +# Julia Version # +################# + +execute_process( + COMMAND "${Julia_EXECUTABLE}" --startup-file=no --version + OUTPUT_VARIABLE Julia_VERSION_STRING +) + +string( + REGEX REPLACE ".*([0-9]+\\.[0-9]+\\.[0-9]+).*" "\\1" + Julia_VERSION_STRING "${Julia_VERSION_STRING}" +) + +MESSAGE(STATUS "Julia_VERSION_STRING: ${Julia_VERSION_STRING}") + +################## +# Julia Includes # +################## + +if(DEFINED ENV{JULIA_INCLUDE_DIRS}) + set(Julia_INCLUDE_DIRS $ENV{JULIA_INCLUDE_DIRS} + CACHE STRING "Location of Julia include files") +else() + execute_process( + COMMAND ${Julia_EXECUTABLE} --startup-file=no -E "julia_include_dir = joinpath(match(r\"(.*)(bin)\",Sys.BINDIR).captures[1],\"include\",\"julia\")\n + if !isdir(julia_include_dir) # then we're running directly from build\n + julia_base_dir_aux = splitdir(splitdir(Sys.BINDIR)[1])[1] # useful for running-from-build\n + julia_include_dir = joinpath(julia_base_dir_aux, \"usr\", \"include\" )\n + julia_include_dir *= \";\" * joinpath(julia_base_dir_aux, \"src\", \"support\" )\n + julia_include_dir *= \";\" * joinpath(julia_base_dir_aux, \"src\" )\n + end\n + julia_include_dir" + OUTPUT_VARIABLE Julia_INCLUDE_DIRS + ) + + string(REGEX REPLACE "\"" "" Julia_INCLUDE_DIRS "${Julia_INCLUDE_DIRS}") + string(REGEX REPLACE "\n" "" Julia_INCLUDE_DIRS "${Julia_INCLUDE_DIRS}") + set(Julia_INCLUDE_DIRS ${Julia_INCLUDE_DIRS} + CACHE PATH "Location of Julia include files") +endif() +MESSAGE(STATUS "Julia_INCLUDE_DIRS: ${Julia_INCLUDE_DIRS}") + +################### +# Julia Libraries # +################### + +execute_process( + COMMAND ${Julia_EXECUTABLE} --startup-file=no -E "using Libdl; abspath(dirname(Libdl.dlpath(\"libjulia\")))" + OUTPUT_VARIABLE Julia_LIBRARY_DIR +) + +string(REGEX REPLACE "\"" "" Julia_LIBRARY_DIR "${Julia_LIBRARY_DIR}") +string(REGEX REPLACE "\n" "" Julia_LIBRARY_DIR "${Julia_LIBRARY_DIR}") + +string(STRIP "${Julia_LIBRARY_DIR}" Julia_LIBRARY_DIR) +set(Julia_LIBRARY_DIR "${Julia_LIBRARY_DIR}" + CACHE PATH "Julia library directory") + +if(WIN32) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES} .a) + find_library(Julia_LIBRARY + NAMES libjulia.dll.a + PATHS ${Julia_LIBRARY_DIR}//..//lib + NO_DEFAULT_PATH + ) +else() + find_library(Julia_LIBRARY + NAMES julia libjulia + PATHS ${Julia_LIBRARY_DIR} + NO_DEFAULT_PATH + ) +endif() + +MESSAGE(STATUS "Julia_LIBRARY_DIR: ${Julia_LIBRARY_DIR}") +MESSAGE(STATUS "Julia_LIBRARY: ${Julia_LIBRARY}") + +############## +# Sys.BINDIR # +############## + +execute_process( + COMMAND ${Julia_EXECUTABLE} --startup-file=no -E "Sys.BINDIR" + OUTPUT_VARIABLE Sys.BINDIR +) + +string(REGEX REPLACE "\"" "" Sys.BINDIR "${Sys.BINDIR}") +string(REGEX REPLACE "\n" "" Sys.BINDIR "${Sys.BINDIR}") + +MESSAGE(STATUS "Sys.BINDIR: ${Sys.BINDIR}") + +################### +# libLLVM version # +################### + +execute_process( + COMMAND ${Julia_EXECUTABLE} --startup-file=no -E "Base.libllvm_version" + OUTPUT_VARIABLE Julia_LLVM_VERSION +) + +string(REGEX REPLACE "\"" "" Julia_LLVM_VERSION "${Julia_LLVM_VERSION}") +string(REGEX REPLACE "\n" "" Julia_LLVM_VERSION "${Julia_LLVM_VERSION}") + +################################## +# Check for Existence of Headers # +################################## + +find_path(Julia_MAIN_HEADER julia.h HINTS ${Julia_INCLUDE_DIRS}) + +MESSAGE(STATUS "Julia_LLVM_VERSION: ${Julia_LLVM_VERSION}") + +########################### +# FindPackage Boilerplate # +########################### + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Julia + REQUIRED_VARS Julia_LIBRARY Julia_LIBRARY_DIR Julia_INCLUDE_DIRS Julia_MAIN_HEADER + VERSION_VAR Julia_VERSION_STRING + FAIL_MESSAGE "Julia not found" +) \ No newline at end of file diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc index 6360f9e..4905a2b 100644 --- a/test/heat-images/c-julia/run_bmi_server.cc +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -1,16 +1,30 @@ // #include "bmi_grpc_server.h" // #include // #include "bmi_heat.h" +#include #include +using namespace std; + int main(int argc, char* argv[]) { jl_init(); { { // Simple running Julia code - - jl_eval_string("println(sqrt(2.0))"); + jl_eval_string("x = sqrt(2.0)"); + jl_eval_string("print(x)"); + } + { + jl_eval_string("import Heat"); + } + { + jl_function_t *initialize = jl_get_function(jl_main_module, "Heat.BMI.initialize"); + jl_value_t* path = jl_cstr_to_string("/usr/local/share/bmi/heat.toml"); + jl_value_t *model = jl_call1(initialize, path); + jl_function_t *get_component_name = jl_get_function(jl_main_module, "Heat.BMI.get_component_name"); + jl_value_t *name = jl_call1(get_component_name, model); + cout << jl_string_ptr(name) << endl; } // Bmi *model = (Bmi *) malloc(sizeof(Bmi)); From 92cb27ce9b432cca470b1fde66c3584e955c0383 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 17 Oct 2023 16:34:18 +0200 Subject: [PATCH 18/30] Trying to convert repl calls into c Need to add handle_julia_exception from https://blog.esciencecenter.nl/10-examples-of-embedding-julia-in-c-c-66282477e62c to get better error Got error: 1.4142135623730951 [7] signal (11.1): Segmentation fault in expression starting at none:0 jl_lookup_generic_ at /cache/build/default-amdci5-5/julialang/julia-release-1-dot-9/src/gf.c:2819 [inlined] ijl_apply_generic at /cache/build/default-amdci5-5/julialang/julia-release-1-dot-9/src/gf.c:2936 jl_apply at /cache/build/default-amdci5-5/julialang/julia-release-1-dot-9/src/julia.h:1880 [inlined] ijl_call1 at /cache/build/default-amdci5-5/julialang/julia-release-1-dot-9/src/jlapi.c:230 main at /usr/local/bin/run_bmi_server (unknown line) __libc_start_main at /lib/x86_64-linux-gnu/libc.so.6 (unknown line) _start at /usr/local/bin/run_bmi_server (unknown line) Allocations: 2060272 (Pool: 2059240; Big: 1032); GC: 3 Segmentation fault --- test/heat-images/c-julia/Dockerfile | 5 ++- test/heat-images/c-julia/run_bmi_server.cc | 45 +++++++++++++++------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/test/heat-images/c-julia/Dockerfile b/test/heat-images/c-julia/Dockerfile index 7d4f8bc..04e58c4 100644 --- a/test/heat-images/c-julia/Dockerfile +++ b/test/heat-images/c-julia/Dockerfile @@ -70,9 +70,10 @@ COPY --from=builder /usr/local/bin/run_bmi_server /usr/local/bin/run_bmi_server COPY --from=builder /usr/local/lib/ /usr/local/lib/ # Install heat-julia -RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' +RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' && \ + julia -e 'using Pkg; Pkg.add("BasicModelInterface")' -RUN curl -L -O /usr/local/share/bmi/heat.toml https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml +RUN curl -L https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml > /usr/local/share/heat.toml RUN echo '/usr/local/julia/lib' > /etc/ld.so.conf.d/julia.conf && ldconfig diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc index 4905a2b..39c8bf6 100644 --- a/test/heat-images/c-julia/run_bmi_server.cc +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -6,25 +6,42 @@ using namespace std; -int main(int argc, char* argv[]) +int main(int argc, char *argv[]) { jl_init(); { - { - // Simple running Julia code - jl_eval_string("x = sqrt(2.0)"); - jl_eval_string("print(x)"); - } - { - jl_eval_string("import Heat"); + { + // Simple running Julia code + jl_eval_string("x = sqrt(2.0)"); + jl_eval_string("print(x)"); + } + { } { - jl_function_t *initialize = jl_get_function(jl_main_module, "Heat.BMI.initialize"); - jl_value_t* path = jl_cstr_to_string("/usr/local/share/bmi/heat.toml"); - jl_value_t *model = jl_call1(initialize, path); - jl_function_t *get_component_name = jl_get_function(jl_main_module, "Heat.BMI.get_component_name"); - jl_value_t *name = jl_call1(get_component_name, model); - cout << jl_string_ptr(name) << endl; + + /* + In Julia repl: + + import BasicModelInterface as BMI + import Heat + # https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml + model = BMI.initialize(Heat.Model, "/usr/local/share/heat.toml") + BMI.get_component_name(model) + + */ + + jl_eval_string("import BasicModelInterface as BMI"); + jl_eval_string("import Heat"); + jl_value_t *model = jl_eval_string("BMI.initialize(Heat.Model, \"/usr/local/share/heat.toml\")"); + // jl_function_t *initialize = jl_get_function(jl_main_module, "BMI.initialize"); + // jl_value_t *modelt = jl_eval_string("Heat.Model"); + // jl_value_t *path = jl_cstr_to_string("/usr/local/share/heat.toml"); + // jl_value_t *model = jl_call2(initialize, modelt, path); + jl_function_t *get_component_name = jl_get_function(jl_main_module, "BMI.get_component_name"); + jl_value_t *name = jl_call1(get_component_name, model); + cout << jl_string_ptr(name) << endl; + + // TODO move initialize and get_component_name to a bmi interface } // Bmi *model = (Bmi *) malloc(sizeof(Bmi)); From 05e1b470b8a2110f852951bda1be5c6d7341edd9 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 18 Oct 2023 11:33:05 +0200 Subject: [PATCH 19/30] Got it to work, until it segfaults somewhere --- cpp/bmi_grpc_server.cc | 1 + test/heat-images/c-julia/CMakeLists.txt | 8 +- test/heat-images/c-julia/Dockerfile | 28 +- test/heat-images/c-julia/demo.ipynb | 175 +++++++++++++ test/heat-images/c-julia/run_bmi_server.cc | 281 +++++++++++++++++++-- 5 files changed, 454 insertions(+), 39 deletions(-) create mode 100644 test/heat-images/c-julia/demo.ipynb diff --git a/cpp/bmi_grpc_server.cc b/cpp/bmi_grpc_server.cc index 4d8afbd..3a3daae 100644 --- a/cpp/bmi_grpc_server.cc +++ b/cpp/bmi_grpc_server.cc @@ -824,6 +824,7 @@ void run_bmi_server(BmiClass *model, int argc, char *argv[]) grpc::EnableDefaultHealthCheckService(true); grpc::reflection::InitProtoReflectionServerBuilderPlugin(); grpc::ServerBuilder builder; + // builder.SetResourceQuota(grpc::ResourceQuota().SetMaxThreads(2)); builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr server(builder.BuildAndStart()); diff --git a/test/heat-images/c-julia/CMakeLists.txt b/test/heat-images/c-julia/CMakeLists.txt index 1284d9e..ab127cd 100644 --- a/test/heat-images/c-julia/CMakeLists.txt +++ b/test/heat-images/c-julia/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.0) -project(run_bmi_server C CXX) +project(run_bmi_server CXX) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_SKIP_BUILD_RPATH FALSE) @@ -7,9 +7,9 @@ set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) set(_cflags "-ansi -Wall -Wundef -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -O2") find_package(PkgConfig REQUIRED) -pkg_check_modules(BMIC REQUIRED IMPORTED_TARGET bmic) -message("-- bmic include - ${BMIC_INCLUDE_DIRS}") -include_directories(${BMIC_INCLUDE_DIRS}) +pkg_check_modules(BMICXX REQUIRED IMPORTED_TARGET bmicxx) +message("-- bmicxx include - ${BMICXX_INCLUDE_DIRS}") +include_directories(${BMICXX_INCLUDE_DIRS}) pkg_check_modules(GRPC4BMI REQUIRED grpc4bmi) # See https://github.com/barche/embedding-julia diff --git a/test/heat-images/c-julia/Dockerfile b/test/heat-images/c-julia/Dockerfile index 04e58c4..a15b0b3 100644 --- a/test/heat-images/c-julia/Dockerfile +++ b/test/heat-images/c-julia/Dockerfile @@ -2,6 +2,8 @@ # # docker build -t heat:c-julia -f test/heat-images/c-julia/Dockerfile . # +# docker run -ti --rm -p 55555:55555 heat:c-julia +# # Run with # # ipython @@ -45,9 +47,11 @@ RUN cmake .. && make install # Install grpc4bmi, use commit c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 # head of bmi2 branch, PR https://github.com/eWaterCycle/grpc4bmi/pull/124 -ARG GRPC4BMI_VERSION=c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 -RUN git clone https://github.com/eWaterCycle/grpc4bmi /opt/grpc4bmi \ - && cd /opt/grpc4bmi && git checkout ${GRPC4BMI_VERSION} +# ARG GRPC4BMI_VERSION=c5cc9b9bf33b6043e8db242a07c6fb92b9c63f66 +# RUN git clone https://github.com/eWaterCycle/grpc4bmi /opt/grpc4bmi \ +# && cd /opt/grpc4bmi && git checkout ${GRPC4BMI_VERSION} +COPY cpp /opt/grpc4bmi/cpp +COPY proto /opt/grpc4bmi/proto WORKDIR /opt/grpc4bmi/cpp/build RUN cmake .. && make install @@ -59,6 +63,14 @@ COPY test/heat-images/c-julia/run_bmi_server.cc /opt/grpc4bmiheatc-julia COPY test/heat-images/c-julia/CMakeLists.txt /opt/grpc4bmiheatc-julia RUN cmake .. && make install +FROM julia:bullseye AS jldeps + +# Install heat-julia +RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' && \ + julia -e 'using Pkg; Pkg.add("BasicModelInterface")' + +RUN curl -L https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml > /usr/local/share/heat.toml + # run container FROM julia:bullseye @@ -68,15 +80,11 @@ RUN apt-get update && apt-get install -qy libssl1.1 && rm -rf /var/lib/apt/lists # Copy compiled and deps COPY --from=builder /usr/local/bin/run_bmi_server /usr/local/bin/run_bmi_server COPY --from=builder /usr/local/lib/ /usr/local/lib/ - -# Install heat-julia -RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' && \ - julia -e 'using Pkg; Pkg.add("BasicModelInterface")' - -RUN curl -L https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml > /usr/local/share/heat.toml +COPY --from=jldeps /root/.julia/ /root/.julia/ +COPY --from=jldeps /usr/local/share/heat.toml /usr/local/share/heat.toml RUN echo '/usr/local/julia/lib' > /etc/ld.so.conf.d/julia.conf && ldconfig -ENV BMI_PORT=50051 +# TODO run server as non-root user ENTRYPOINT ["/usr/local/bin/run_bmi_server"] diff --git a/test/heat-images/c-julia/demo.ipynb b/test/heat-images/c-julia/demo.ipynb new file mode 100644 index 0000000..c27ba77 --- /dev/null +++ b/test/heat-images/c-julia/demo.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import grpc\n", + "from grpc4bmi.bmi_grpc_client import BmiClient\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "mymodel = BmiClient(grpc.insecure_channel(\"localhost:55555\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mymodel.initialize(\"/usr/local/share/heat.toml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The 2D Heat Equation'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mymodel.get_component_name()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "ename": "_InactiveRpcError", + "evalue": "<_InactiveRpcError of RPC that terminated with:\n\tstatus = StatusCode.UNAVAILABLE\n\tdetails = \"Socket closed\"\n\tdebug_error_string = \"UNKNOWN:Error received from peer ipv4:127.0.0.1:55555 {grpc_message:\"Socket closed\", grpc_status:14, created_time:\"2023-10-18T11:32:54.922549729+02:00\"}\"\n>", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31m_InactiveRpcError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/home/verhoes/git/eWaterCycle/grpc4bmi/test/heat-images/c-julia/demo.ipynb Cell 5\u001b[0m line \u001b[0;36m1\n\u001b[0;32m----> 1\u001b[0m mymodel\u001b[39m.\u001b[39;49mget_component_name()\n", + "File \u001b[0;32m~/git/eWaterCycle/grpc4bmi/grpc4bmi/bmi_grpc_client.py:125\u001b[0m, in \u001b[0;36mBmiClient.get_component_name\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 123\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mstr\u001b[39m(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39mstub\u001b[39m.\u001b[39mgetComponentName(bmi_pb2\u001b[39m.\u001b[39mEmpty())\u001b[39m.\u001b[39mname)\n\u001b[1;32m 124\u001b[0m \u001b[39mexcept\u001b[39;00m grpc\u001b[39m.\u001b[39mRpcError \u001b[39mas\u001b[39;00m e:\n\u001b[0;32m--> 125\u001b[0m handle_error(e)\n", + "File \u001b[0;32m~/git/eWaterCycle/grpc4bmi/grpc4bmi/bmi_grpc_client.py:123\u001b[0m, in \u001b[0;36mBmiClient.get_component_name\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 121\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mget_component_name\u001b[39m(\u001b[39mself\u001b[39m):\n\u001b[1;32m 122\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m--> 123\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mstr\u001b[39m(\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mstub\u001b[39m.\u001b[39;49mgetComponentName(bmi_pb2\u001b[39m.\u001b[39;49mEmpty())\u001b[39m.\u001b[39mname)\n\u001b[1;32m 124\u001b[0m \u001b[39mexcept\u001b[39;00m grpc\u001b[39m.\u001b[39mRpcError \u001b[39mas\u001b[39;00m e:\n\u001b[1;32m 125\u001b[0m handle_error(e)\n", + "File \u001b[0;32m~/git/eWaterCycle/grpc4bmi/.venv/lib/python3.10/site-packages/grpc/_channel.py:946\u001b[0m, in \u001b[0;36m_UnaryUnaryMultiCallable.__call__\u001b[0;34m(self, request, timeout, metadata, credentials, wait_for_ready, compression)\u001b[0m\n\u001b[1;32m 937\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__call__\u001b[39m(\u001b[39mself\u001b[39m,\n\u001b[1;32m 938\u001b[0m request,\n\u001b[1;32m 939\u001b[0m timeout\u001b[39m=\u001b[39m\u001b[39mNone\u001b[39;00m,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 942\u001b[0m wait_for_ready\u001b[39m=\u001b[39m\u001b[39mNone\u001b[39;00m,\n\u001b[1;32m 943\u001b[0m compression\u001b[39m=\u001b[39m\u001b[39mNone\u001b[39;00m):\n\u001b[1;32m 944\u001b[0m state, call, \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_blocking(request, timeout, metadata, credentials,\n\u001b[1;32m 945\u001b[0m wait_for_ready, compression)\n\u001b[0;32m--> 946\u001b[0m \u001b[39mreturn\u001b[39;00m _end_unary_response_blocking(state, call, \u001b[39mFalse\u001b[39;49;00m, \u001b[39mNone\u001b[39;49;00m)\n", + "File \u001b[0;32m~/git/eWaterCycle/grpc4bmi/.venv/lib/python3.10/site-packages/grpc/_channel.py:849\u001b[0m, in \u001b[0;36m_end_unary_response_blocking\u001b[0;34m(state, call, with_call, deadline)\u001b[0m\n\u001b[1;32m 847\u001b[0m \u001b[39mreturn\u001b[39;00m state\u001b[39m.\u001b[39mresponse\n\u001b[1;32m 848\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m--> 849\u001b[0m \u001b[39mraise\u001b[39;00m _InactiveRpcError(state)\n", + "\u001b[0;31m_InactiveRpcError\u001b[0m: <_InactiveRpcError of RPC that terminated with:\n\tstatus = StatusCode.UNAVAILABLE\n\tdetails = \"Socket closed\"\n\tdebug_error_string = \"UNKNOWN:Error received from peer ipv4:127.0.0.1:55555 {grpc_message:\"Socket closed\", grpc_status:14, created_time:\"2023-10-18T11:32:54.922549729+02:00\"}\"\n>" + ] + } + ], + "source": [ + "mymodel.get_component_name()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The 2D Heat Equation'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mymodel.get_component_name()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from time import sleep" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".\r" + ] + }, + { + "ename": "AttributeError", + "evalue": "module 'os' has no attribute 'sleep'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/home/verhoes/git/eWaterCycle/grpc4bmi/test/heat-images/c-julia/demo.ipynb Cell 8\u001b[0m line \u001b[0;36m3\n\u001b[1;32m 1\u001b[0m \u001b[39mfor\u001b[39;00m _ \u001b[39min\u001b[39;00m \u001b[39mrange\u001b[39m(\u001b[39m100\u001b[39m):\n\u001b[1;32m 2\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m'\u001b[39m\u001b[39m.\u001b[39m\u001b[39m'\u001b[39m, end\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39m\\r\u001b[39;00m\u001b[39m'\u001b[39m)\n\u001b[0;32m----> 3\u001b[0m os\u001b[39m.\u001b[39;49msleep(\u001b[39m0.01\u001b[39m)\n\u001b[1;32m 4\u001b[0m mymodel\u001b[39m.\u001b[39mget_component_name()\n", + "\u001b[0;31mAttributeError\u001b[0m: module 'os' has no attribute 'sleep'" + ] + } + ], + "source": [ + "for _ in range(100):\n", + " print('.', end='\\r')\n", + " sleep(0.01)\n", + " mymodel.get_component_name()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc index 39c8bf6..5a59d8c 100644 --- a/test/heat-images/c-julia/run_bmi_server.cc +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -1,21 +1,260 @@ -// #include "bmi_grpc_server.h" -// #include -// #include "bmi_heat.h" +#include +#include #include +#include +#include + +#include #include +#include "bmi_grpc_server.h" using namespace std; +class NotImplemented : public std::logic_error +{ +public: + NotImplemented() : std::logic_error("Not Implemented"){}; +}; + +void handle_julia_exception(void) { + if (jl_value_t *ex = jl_exception_occurred()) { + jl_printf(jl_stderr_stream(), "Exception:\n"); + jl_call2( + jl_get_function(jl_base_module, "showerror"), + jl_stderr_obj(), + ex + ); + jl_printf(jl_stderr_stream(), "\nI quit!\n"); + jl_atexit_hook(1); + exit(1); + } +} + +class BmiJulia : public bmi::Bmi +{ +public: + BmiJulia(std::string package_name, std::string model_name) + { + this->modelt = model_name; + this->package_name = package_name; + } + void Initialize(std::string config_file) + { + jl_init(); + jl_eval_string("import BasicModelInterface as BMI"); + handle_julia_exception(); + std::string import = "import " + this->package_name; + // cout << import << endl; + jl_eval_string(import.c_str()); + handle_julia_exception(); + + std::string cmd = "model = BMI.initialize("; + cmd.append(this->modelt); + cmd.append(", \""); + cmd.append(config_file); + cmd.append("\")"); + // cout << cmd << endl; + jl_eval_string(cmd.c_str()); + handle_julia_exception(); + } + + + void Update() + { + throw NotImplemented(); + } + void UpdateUntil(double time) + { + throw NotImplemented(); + } + void Finalize() + { + throw NotImplemented(); + } + + std::string GetComponentName() + { + cout << "GetComponentName" << jl_is_initialized << endl; + jl_eval_string("print(model)"); + // If we cant get passed line above + // then we are running in another thread + // that initialize has not run in + handle_julia_exception(); + std::string cmd = "BMI.get_component_name(model)"; + cout << cmd << endl; + jl_value_t *jname = jl_eval_string(cmd.c_str()); + handle_julia_exception(); + const char* cname = jl_string_ptr(jname); + handle_julia_exception(); + std::string name(cname); + return name; + } + int GetInputItemCount() + { + throw NotImplemented(); + } + int GetOutputItemCount() + { + throw NotImplemented(); + } + std::vector GetInputVarNames() + { + throw NotImplemented(); + } + std::vector GetOutputVarNames() + { + throw NotImplemented(); + } + + int GetVarGrid(std::string name) + { + throw NotImplemented(); + } + std::string GetVarType(std::string name) + { + throw NotImplemented(); + } + int GetVarItemsize(std::string name) + { + throw NotImplemented(); + } + std::string GetVarUnits(std::string name) + { + throw NotImplemented(); + } + int GetVarNbytes(std::string name) + { + throw NotImplemented(); + } + std::string GetVarLocation(std::string name) + { + throw NotImplemented(); + } + + double GetCurrentTime() + { + throw NotImplemented(); + } + double GetStartTime() + { + throw NotImplemented(); + } + double GetEndTime() + { + throw NotImplemented(); + } + std::string GetTimeUnits() + { + throw NotImplemented(); + } + double GetTimeStep() + { + throw NotImplemented(); + } + + void GetValue(std::string name, void *dest) + { + throw NotImplemented(); + } + void *GetValuePtr(std::string name) + { + throw NotImplemented(); + } + void GetValueAtIndices(std::string name, void *dest, int *inds, int count) + { + throw NotImplemented(); + } + + void SetValue(std::string name, void *src) + { + throw NotImplemented(); + } + void SetValueAtIndices(std::string name, int *inds, int len, void *src) + { + throw NotImplemented(); + } + + int GetGridRank(const int grid) + { + throw NotImplemented(); + } + int GetGridSize(const int grid) + { + throw NotImplemented(); + } + std::string GetGridType(const int grid) + { + throw NotImplemented(); + } + + void GetGridShape(const int grid, int *shape) + { + throw NotImplemented(); + } + void GetGridSpacing(const int grid, double *spacing) + { + throw NotImplemented(); + } + void GetGridOrigin(const int grid, double *origin) + { + throw NotImplemented(); + } + + void GetGridX(const int grid, double *x) + { + throw NotImplemented(); + } + void GetGridY(const int grid, double *y) + { + throw NotImplemented(); + } + void GetGridZ(const int grid, double *z) + { + throw NotImplemented(); + } + + int GetGridNodeCount(const int grid) + { + throw NotImplemented(); + } + int GetGridEdgeCount(const int grid) + { + throw NotImplemented(); + } + int GetGridFaceCount(const int grid) + { + throw NotImplemented(); + } + + void GetGridEdgeNodes(const int grid, int *edge_nodes) + { + throw NotImplemented(); + } + void GetGridFaceEdges(const int grid, int *face_edges) + { + throw NotImplemented(); + } + void GetGridFaceNodes(const int grid, int *face_nodes) + { + throw NotImplemented(); + } + void GetGridNodesPerFace(const int grid, int *nodes_per_face) + { + throw NotImplemented(); + } + +private: + std::string modelt; + std::string package_name; +}; + int main(int argc, char *argv[]) { - jl_init(); { { - // Simple running Julia code - jl_eval_string("x = sqrt(2.0)"); - jl_eval_string("print(x)"); - } - { + // // Simple running Julia code + // jl_eval_string("x = sqrt(2.0)"); + // jl_eval_string("print(x)"); } { @@ -29,24 +268,16 @@ int main(int argc, char *argv[]) BMI.get_component_name(model) */ - - jl_eval_string("import BasicModelInterface as BMI"); - jl_eval_string("import Heat"); - jl_value_t *model = jl_eval_string("BMI.initialize(Heat.Model, \"/usr/local/share/heat.toml\")"); - // jl_function_t *initialize = jl_get_function(jl_main_module, "BMI.initialize"); - // jl_value_t *modelt = jl_eval_string("Heat.Model"); - // jl_value_t *path = jl_cstr_to_string("/usr/local/share/heat.toml"); - // jl_value_t *model = jl_call2(initialize, modelt, path); - jl_function_t *get_component_name = jl_get_function(jl_main_module, "BMI.get_component_name"); - jl_value_t *name = jl_call1(get_component_name, model); - cout << jl_string_ptr(name) << endl; - - // TODO move initialize and get_component_name to a bmi interface } + { + bmi::Bmi* model = new BmiJulia("Heat", "Heat.Model"); + // Calling without grpc works + // model->Initialize("/usr/local/share/heat.toml"); + // cout << model->GetComponentName() << endl; - // Bmi *model = (Bmi *) malloc(sizeof(Bmi)); - - // run_bmi_server(model, argc, argv); + // Calling from grpc client causes segfault when calling jl_eval_string with BMI.initialize() + run_bmi_server(model, argc, argv); + } } int ret = 0; jl_atexit_hook(ret); From ffbe7021047374f3bc3e7d27cde78f8f946b3b91 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 18 Oct 2023 11:45:15 +0200 Subject: [PATCH 20/30] TODOs --- test/heat-images/c-julia/Dockerfile | 3 +++ test/heat-images/c-julia/run_bmi_server.cc | 1 + 2 files changed, 4 insertions(+) diff --git a/test/heat-images/c-julia/Dockerfile b/test/heat-images/c-julia/Dockerfile index a15b0b3..6be7aff 100644 --- a/test/heat-images/c-julia/Dockerfile +++ b/test/heat-images/c-julia/Dockerfile @@ -69,6 +69,7 @@ FROM julia:bullseye AS jldeps RUN julia -e 'using Pkg; Pkg.add(url="https://github.com/csdms/bmi-example-julia.git",rev="80c34b4f2217599e600fe9372b1bae50e1229edf")' && \ julia -e 'using Pkg; Pkg.add("BasicModelInterface")' +# TODO use config file from outside container RUN curl -L https://github.com/csdms/bmi-example-julia/raw/main/example/heat.toml > /usr/local/share/heat.toml # run container @@ -88,3 +89,5 @@ RUN echo '/usr/local/julia/lib' > /etc/ld.so.conf.d/julia.conf && ldconfig # TODO run server as non-root user ENTRYPOINT ["/usr/local/bin/run_bmi_server"] + +# TODO document how to use this Dockerfile together with another Julia BMI model diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc index 5a59d8c..d12d816 100644 --- a/test/heat-images/c-julia/run_bmi_server.cc +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -270,6 +270,7 @@ int main(int argc, char *argv[]) */ } { + // TODO read BmiJulia from env var or cli arguments bmi::Bmi* model = new BmiJulia("Heat", "Heat.Model"); // Calling without grpc works // model->Initialize("/usr/local/share/heat.toml"); From 0a2037aaf67d4c9f2c683176d88b0721639c9e1c Mon Sep 17 00:00:00 2001 From: Bart Schilperoort Date: Tue, 24 Oct 2023 16:48:37 +0200 Subject: [PATCH 21/30] Convert numpy array to Julia vector --- grpc4bmi/bmi_julia_model.py | 52 ++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index a817a6d..5706e88 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -319,7 +319,9 @@ def get_value(self, name: str, dest: np.ndarray) -> np.ndarray: ndarray A numpy array containing the requested value(s). """ - self.implementation.get_value(self.state, name, dest) + dest[:] = self.implementation.get_value( + self.state, name, jl.convert(jl.Vector, dest) + ) return dest def get_value_ptr(self, name: str) -> np.ndarray: @@ -356,11 +358,11 @@ def get_value_at_indices(self, name: str, dest: np.ndarray, inds: np.ndarray) -> array_like Value of the model variable at the given location. """ - self.implementation.get_value_at_indices( + dest[:] = self.implementation.get_value_at_indices( self.state, name, - dest, - inds + 1 + jl.convert(jl.Vector, dest), + jl.convert(jl.Vector, inds) + 1 ) return dest @@ -397,7 +399,7 @@ def set_value_at_indices( self.implementation.set_value_at_indices( self.state, name, - inds + 1, + jl.convert(jl.Vector, inds) + 1, jl.convert(jl.Vector, src), ) @@ -454,7 +456,9 @@ def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: ndarray of int A numpy array that holds the grid's shape. """ - self.implementation.get_grid_shape(self.state, grid ,shape) + shape[:] = self.implementation.get_grid_shape( + self.state, grid, jl.convert(jl.Vector, shape), + ) return shape def get_grid_spacing(self, grid: int, spacing: np.ndarray) -> np.ndarray: @@ -468,7 +472,9 @@ def get_grid_spacing(self, grid: int, spacing: np.ndarray) -> np.ndarray: ndarray of float A numpy array that holds the grid's spacing between grid rows and columns. """ - self.implementation.get_grid_spacing(self.state, grid, spacing) + spacing[:] = self.implementation.get_grid_spacing( + self.state, grid, jl.convert(jl.Vector, spacing), + ) return spacing def get_grid_origin(self, grid: int, origin: np.ndarray) -> np.ndarray: @@ -484,7 +490,9 @@ def get_grid_origin(self, grid: int, origin: np.ndarray) -> np.ndarray: A numpy array that holds the coordinates of the grid's lower-left corner. """ - self.implementation.get_grid_origin(self.state, grid, origin) + origin[:] = self.implementation.get_grid_origin( + self.state, grid, jl.convert(jl.Vector, origin), + ) return origin # Non-uniform rectilinear, curvilinear @@ -502,7 +510,9 @@ def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: ndarray of float The input numpy array that holds the grid's column x-coordinates. """ - self.implementation.get_grid_x(self.state, grid, x) + x[:] = self.implementation.get_grid_x( + self.state, grid, jl.convert(jl.Vector, x), + ) return x def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: @@ -519,7 +529,9 @@ def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: ndarray of float The input numpy array that holds the grid's row y-coordinates. """ - self.implementation.get_grid_y(self.state, grid,y) + y[:] = self.implementation.get_grid_y( + self.state, grid, jl.convert(jl.Vector, y), + ) return y def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: @@ -535,7 +547,9 @@ def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: ndarray of float The input numpy array that holds the grid's layer z-coordinates. """ - self.implementation.get_grid_z(self.state, grid, z) + z[:] = self.implementation.get_grid_z( + self.state, grid, jl.convert(jl.Vector, z), + ) return z def get_grid_node_count(self, grid: int) -> int: @@ -595,7 +609,9 @@ def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: connectivity is given as node at edge tail, followed by node at edge head. """ - self.implementation.get_grid_edge_nodes(self.state, grid, edge_nodes) + edge_nodes[:] = self.implementation.get_grid_edge_nodes( + self.state, grid, jl.convert(jl.Vector, edge_nodes), + ) return edge_nodes def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: @@ -613,7 +629,9 @@ def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: ndarray of int A numpy array that holds the face-edge connectivity. """ - self.implementation.get_grid_face_edges(self.state, grid, face_edges) + face_edges[:] = self.implementation.get_grid_face_edges( + self.state, grid, jl.convert(jl.Vector, face_edges), + ) return face_edges def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: @@ -635,7 +653,9 @@ def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: the nodes (listed in a counter-clockwise direction) that form the boundary of the face. """ - self.implementation.get_grid_face_nodes(self.state, grid, face_nodes) + face_nodes[:] = self.implementation.get_grid_face_nodes( + self.state, grid, jl.convert(jl.Vector, face_nodes), + ) return face_nodes def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: @@ -651,5 +671,7 @@ def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np. ndarray of int, shape *(nfaces,)* A numpy array that holds the number of nodes per face. """ - self.implementation.get_grid_nodes_per_face(self.state, grid,nodes_per_face) + nodes_per_face[:] = self.implementation.get_grid_nodes_per_face( + self.state, grid, jl.convert(jl.Vector, nodes_per_face), + ) return nodes_per_face From 9f413af7bcd2b56425173b865fb83ab316257055 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 25 Oct 2023 11:16:18 +0200 Subject: [PATCH 22/30] Remove install method use jl.Pkg.add directly --- grpc4bmi/bmi_julia_model.py | 8 -------- test/test_julia.py | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 5706e88..94f4abc 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -4,14 +4,6 @@ import numpy as np from juliacall import Main as jl, ModuleValue, TypeValue -def install(package): - """Add package to Julia environment. - - Args: - package: Name of package to install. - """ - jl.Pkg.add(package) - class BmiJulia(Bmi): """Python Wrapper of a Julia based implementation of BasicModelInterface. diff --git a/test/test_julia.py b/test/test_julia.py index 1ae246b..177f685 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -6,7 +6,7 @@ try: - from grpc4bmi.bmi_julia_model import BmiJulia,install + from grpc4bmi.bmi_julia_model import BmiJulia from juliacall import Main as jl except ImportError: BmiJulia = None @@ -17,7 +17,7 @@ class TestJuliaHeatModel: def install_heat(self): # TODO for other Julia models do we need to install BasicModelInterface? # it is dep of Heat.jl, but we use it directly - install('BasicModelInterface') + jl.Pkg.add('BasicModelInterface') jl.Pkg.add( url="https://github.com/csdms/bmi-example-julia.git", rev="80c34b4f2217599e600fe9372b1bae50e1229edf", @@ -129,7 +129,7 @@ def test_set_value_at_indices(self, model: BmiJulia): class TestJuliaFakeModel: @pytest.fixture(scope="class", autouse=True) def install_fake(self): - install('BasicModelInterface') + jl.Pkg.add('BasicModelInterface') jl.seval('include("test/fake.jl")') @pytest.fixture From 125560a36f2bdaa97500fff78f9040a3d2b406f3 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 25 Oct 2023 11:55:07 +0200 Subject: [PATCH 23/30] Keep BmiJulia and heat-images/julia-c, remove other mentions of Julia --- README.md | 20 ++++++++++++---- cpp/bmi_grpc_server.cc | 1 - docs/container/building.rst | 21 +---------------- docs/server/Julia.rst | 47 ------------------------------------- docs/server/index.rst | 1 - grpc4bmi/bmi_julia_model.py | 4 ++-- grpc4bmi/run_server.py | 18 -------------- test/fake.jl | 2 +- test/test_julia.py | 1 - 9 files changed, 19 insertions(+), 96 deletions(-) delete mode 100644 docs/server/Julia.rst diff --git a/README.md b/README.md index 3edca2a..5921afa 100644 --- a/README.md +++ b/README.md @@ -100,21 +100,31 @@ run-bmi-server --lang R --path ~/git/eWaterCycle/grpc4bmi-examples/walrus/walrus The grpc4bmi Python package can also run BMI models written in Julia if the model has an implementation of the [BasicModelInterface.jl](https://github.com/Deltares/BasicModelInterface.jl). -Run the Julia model as a server with +Run the Julia model in Python with ```bash -run-bmi-server --lang julia --name --port +from grpc4bmi.bmi_julia_model import BmiJulia + +mymodel = BmiJulia.from_name('.', 'BasicModelInterface') ``` For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use ```bash # Install Wflow.jl package in the Julia environment managed by the juliacall Python package. -python3 -c 'from grpc4bmi.bmi_julia_model import install;install("Wflow")' -# Run the server -run-bmi-server --lang julia --name Wflow.Model --port 55555 +from juliacall import Main as jl +jl.Pkg.add("Wflow") +# Create the model +from grpc4bmi.bmi_julia_model import BmiJulia +mymodel = BmiJulia.from_name('Wflow.Model', 'Wflow.bmi.BMI') ``` +A Julia model has to be run locally. It can not be run in the default gRPC client/server Docker container mode because: + +1. Julia has no gRPC server implementation +2. Calling Julia methods from Python gRPC server causes 100% CPU usage and no progress +3. Calling Julia methods from C++ gRPC server causes segmentation faults + ### The client side The client side has only a Python implementation. The default BMI client assumes a running server process on a given port. diff --git a/cpp/bmi_grpc_server.cc b/cpp/bmi_grpc_server.cc index 3a3daae..4d8afbd 100644 --- a/cpp/bmi_grpc_server.cc +++ b/cpp/bmi_grpc_server.cc @@ -824,7 +824,6 @@ void run_bmi_server(BmiClass *model, int argc, char *argv[]) grpc::EnableDefaultHealthCheckService(true); grpc::reflection::InitProtoReflectionServerBuilderPlugin(); grpc::ServerBuilder builder; - // builder.SetResourceQuota(grpc::ResourceQuota().SetMaxThreads(2)); builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr server(builder.BuildAndStart()); diff --git a/docs/container/building.rst b/docs/container/building.rst index 60e9fef..8511be8 100644 --- a/docs/container/building.rst +++ b/docs/container/building.rst @@ -71,26 +71,7 @@ The WALRUS model has a `Dockerfile`_ file which can be used as an example. Julia ----- -The docker file for the model container simply contains the installation instructions of grpc4bmi and the BMI-enabled model itself, and as entrypoint the ``run-bmi-server`` command. For the :ref:`python example ` the Docker file will read - -.. code-block:: Dockerfile - - FROM ubuntu:jammy - MAINTAINER your name - - # Install grpc4bmi - RUN pip install grpc4bmi - - # Install your BMI model: - python3 -c 'from grpc4bmi.bmi_julia_model import install;install("")' - - # Run bmi server - ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", ""] - - # Expose the magic grpc4bmi port - EXPOSE 55555 - -The port 55555 is the internal port in the Docker container that the model communicates over. It is the default port for ``run_bmi_server`` and also the default port that all clients listen to. +A Julia model can not be run as a server, see https://github.com/eWaterCycle/grpc4bmi/blob/main/README.md#model-written-in-julia . C/C++/Fortran ------------- diff --git a/docs/server/Julia.rst b/docs/server/Julia.rst deleted file mode 100644 index 4fdb513..0000000 --- a/docs/server/Julia.rst +++ /dev/null @@ -1,47 +0,0 @@ -Julia -===== - -Grpc4bmi allows you to wrap a Hydrological model written in the `Julia language`_ into a GRPC server. - -.. _Julia language: https://julialang.org/ - -Creating --------- - -The model should implement `BasicModelInterface.jl`_. - -.. _BasicModelInterface.jl: https://github.com/Deltares/BasicModelInterface.jl - -See `Wflow.jl`_ for an example. - -.. _Wflow.jl: https://deltares.github.io/Wflow.jl/dev/ - -Running -------- - -Once the model has an BMI interface it can be run as a GRPC server by installing the `grpc4bmi[julia]` Python package with - -.. code-block:: bash - - pip install grpc4bmi[julia] - -The model Julia package must be installed in the Julia environment managed by juliacall, -for Wflow use - -.. code-block:: bash - - python3 -c 'from grpc4bmi.bmi_julia_model import install;install("Wflow")' - -The server can be started with - -.. code-block:: sh - - run-bmi-server --lang julia --name [,IMPLEMENTATION-NAME] --port - -For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use - -.. code-block:: sh - - run-bmi-server --lang julia --name Wflow.Model,Wflow.bmi.BMI --port 55555 - -The Python grpc4bmi :ref:`usage` can then be used to connect to the server. diff --git a/docs/server/index.rst b/docs/server/index.rst index 5f40a1f..20eb970 100644 --- a/docs/server/index.rst +++ b/docs/server/index.rst @@ -7,5 +7,4 @@ Creating a BMI server python R - Julia Cpp diff --git a/grpc4bmi/bmi_julia_model.py b/grpc4bmi/bmi_julia_model.py index 94f4abc..2dd36a3 100644 --- a/grpc4bmi/bmi_julia_model.py +++ b/grpc4bmi/bmi_julia_model.py @@ -354,7 +354,7 @@ def get_value_at_indices(self, name: str, dest: np.ndarray, inds: np.ndarray) -> self.state, name, jl.convert(jl.Vector, dest), - jl.convert(jl.Vector, inds) + 1 + jl.convert(jl.Vector, inds + 1) ) return dest @@ -391,7 +391,7 @@ def set_value_at_indices( self.implementation.set_value_at_indices( self.state, name, - jl.convert(jl.Vector, inds) + 1, + jl.convert(jl.Vector, inds + 1), jl.convert(jl.Vector, src), ) diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index 676976f..9e4a50c 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -24,11 +24,6 @@ except ImportError: BmiR = None -try: - from .bmi_julia_model import BmiJulia -except ImportError: - BmiJulia = None - """ Run server script, turning a BMI implementation into an executable by looping indefinitely, until interrupt signals are handled. The command line tool needs at least a module and class name to instantiate the BMI wrapper class that exposes @@ -78,11 +73,6 @@ def build_r(class_name, source_fn): raise ValueError('Missing R dependencies, install with `pip install grpc4bmi[R]') return BmiR(class_name, source_fn) -def build_julia(name: str, implementation_name: str = 'BasicModelInterface'): - if not BmiJulia: - raise ValueError('Missing Julia dependencies, install with `pip install grpc4bmi[julia]') - return BmiJulia.from_name(name, implementation_name) - def serve(model, port): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) bmi_pb2_grpc.add_BmiServiceServicer_to_server(model, server) @@ -119,12 +109,6 @@ def main(argv=sys.argv[1:]): if args.language == "R": model = build_r(args.name, path) - elif args.language == "julia": - names = args.name.split(',') - if len(names) == 2: - model = build_julia(names[0], names[1]) - else: - model = build_julia(names[0]) else: model = build(args.name, path) @@ -157,8 +141,6 @@ def build_parser(): lang_choices = ['python'] if BmiR: lang_choices.append('R') - if BmiJulia: - lang_choices.append('julia') parser.add_argument("--language", default="python", choices=lang_choices, help="Language in which BMI implementation class is written") parser.add_argument("--bmi-version", default="2.0.0", choices=["2.0.0", "0.2"], diff --git a/test/fake.jl b/test/fake.jl index 6aa4801..494c219 100644 --- a/test/fake.jl +++ b/test/fake.jl @@ -9,7 +9,7 @@ BMI.initialize(::Type{Model}, config_file) = Model() BMI.get_component_name(m::Model) = "The 2D Heat Equation" -function BMI.get_grid_x(m::Model, grid, x) +function BMI.get_grid_x(m::Model, grid, x::Vector{T}) where {T<:AbstractFloat} copyto!(x, [1.0, 2.0]) end diff --git a/test/test_julia.py b/test/test_julia.py index 177f685..17eef7c 100644 --- a/test/test_julia.py +++ b/test/test_julia.py @@ -101,7 +101,6 @@ def test_get_value_ptr(self, model: BmiJulia): with pytest.raises(NotImplementedError): model.get_value_ptr("plate_surface__temperature") - # TODO fix gives no method matching error def test_get_value_at_indices(self, model: BmiJulia): result = model.get_value_at_indices( "plate_surface__temperature", np.zeros((3,)), np.array([5, 6, 7]) From 7d1c3e56b0b8f2c2f13032a3516e2f591ed89fea Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 25 Oct 2023 11:57:44 +0200 Subject: [PATCH 24/30] Newline --- grpc4bmi/run_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/grpc4bmi/run_server.py b/grpc4bmi/run_server.py index 9e4a50c..626f265 100755 --- a/grpc4bmi/run_server.py +++ b/grpc4bmi/run_server.py @@ -73,6 +73,7 @@ def build_r(class_name, source_fn): raise ValueError('Missing R dependencies, install with `pip install grpc4bmi[R]') return BmiR(class_name, source_fn) + def serve(model, port): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) bmi_pb2_grpc.add_BmiServiceServicer_to_server(model, server) From a54c7312787fe9e659d7bc9ea6c33890c837048e Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 25 Oct 2023 11:58:43 +0200 Subject: [PATCH 25/30] Add note that test/heat-images/c-julia is broken --- test/heat-images/c-julia/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/heat-images/c-julia/README.md diff --git a/test/heat-images/c-julia/README.md b/test/heat-images/c-julia/README.md new file mode 100644 index 0000000..2b50cf6 --- /dev/null +++ b/test/heat-images/c-julia/README.md @@ -0,0 +1,3 @@ +# gRPC server in C++ calling Julia methods + +> Does not work, it segmentation faults after a few calls. From c6bddde532959b4b0ea444583fc59b0b994da41f Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 25 Oct 2023 14:47:51 +0200 Subject: [PATCH 26/30] Add timeout to test --- test/test_subproc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_subproc.py b/test/test_subproc.py index 7a0e20f..423e87d 100644 --- a/test/test_subproc.py +++ b/test/test_subproc.py @@ -22,7 +22,7 @@ def make_bmi_classes(init=False): numpy.random.seed(0) os.environ["PYTHONPATH"] = os.path.dirname(os.path.abspath(__file__)) - client = BmiClientSubProcess("heat.BmiHeat") + client = BmiClientSubProcess("heat.BmiHeat", timeout=10) local = BmiHeat() if init: client.initialize(None) From ead67cd0fbff94361ba898e44caf73f41d0f3c8d Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 26 Oct 2023 08:01:13 +0200 Subject: [PATCH 27/30] Log thread id --- test/heat-images/c-julia/run_bmi_server.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/heat-images/c-julia/run_bmi_server.cc b/test/heat-images/c-julia/run_bmi_server.cc index d12d816..123273f 100644 --- a/test/heat-images/c-julia/run_bmi_server.cc +++ b/test/heat-images/c-julia/run_bmi_server.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -56,6 +57,8 @@ class BmiJulia : public bmi::Bmi // cout << cmd << endl; jl_eval_string(cmd.c_str()); handle_julia_exception(); + std::cout << "Initialize has Thread ID: " << std::this_thread::get_id() << std::endl; + } @@ -74,6 +77,7 @@ class BmiJulia : public bmi::Bmi std::string GetComponentName() { + std::cout << "GetComponentName has Thread ID: " << std::this_thread::get_id() << std::endl; cout << "GetComponentName" << jl_is_initialized << endl; jl_eval_string("print(model)"); // If we cant get passed line above From 7c29974f8505bfdf41d9f41c9410c286f14b4df7 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Thu, 26 Oct 2023 08:11:25 +0200 Subject: [PATCH 28/30] Increase delay in BmiClientSubProcess tests --- grpc4bmi/bmi_client_subproc.py | 4 ++-- test/test_subproc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grpc4bmi/bmi_client_subproc.py b/grpc4bmi/bmi_client_subproc.py index 4e2104a..277440c 100644 --- a/grpc4bmi/bmi_client_subproc.py +++ b/grpc4bmi/bmi_client_subproc.py @@ -15,14 +15,14 @@ class BmiClientSubProcess(BmiClient): >>> mymodel = BmiClientSubProcess(..) """ - def __init__(self, module_name, path=None, timeout=None): + def __init__(self, module_name, path=None, timeout=None, delay=1): host = "localhost" port = BmiClient.get_unique_port(host) name_options = ["--name", module_name] port_options = ["--port", str(port)] path_options = ["--path", path] if path else [] self.pipe = subprocess.Popen(["run-bmi-server"] + name_options + port_options + path_options, env=dict(os.environ)) - time.sleep(1) + time.sleep(delay) super(BmiClientSubProcess, self).__init__(BmiClient.create_grpc_channel(port=port, host=host), timeout=timeout) def __del__(self): diff --git a/test/test_subproc.py b/test/test_subproc.py index 423e87d..e57b6a2 100644 --- a/test/test_subproc.py +++ b/test/test_subproc.py @@ -22,7 +22,7 @@ def make_bmi_classes(init=False): numpy.random.seed(0) os.environ["PYTHONPATH"] = os.path.dirname(os.path.abspath(__file__)) - client = BmiClientSubProcess("heat.BmiHeat", timeout=10) + client = BmiClientSubProcess("heat.BmiHeat", timeout=10, delay=3) local = BmiHeat() if init: client.initialize(None) From 3fbb905f79f4f800dd292e50ead38a6a96a61f3d Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 26 Oct 2023 14:11:12 +0200 Subject: [PATCH 29/30] Dont wait forever after subproces is terminated --- .github/workflows/ci.yml | 1 + grpc4bmi/bmi_client_subproc.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e81500c..86c6569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: - name: Test with pytest run: | pytest -vv --cov=grpc4bmi --cov-report xml + timeout-minutes: 20 - name: Correct coverage paths run: sed -i "s+$PWD/++g" coverage.xml - name: SonarCloud analysis diff --git a/grpc4bmi/bmi_client_subproc.py b/grpc4bmi/bmi_client_subproc.py index 277440c..9b84081 100644 --- a/grpc4bmi/bmi_client_subproc.py +++ b/grpc4bmi/bmi_client_subproc.py @@ -27,7 +27,7 @@ def __init__(self, module_name, path=None, timeout=None, delay=1): def __del__(self): self.pipe.terminate() - self.pipe.wait() + self.pipe.wait(timeout=0.1) def get_value_ref(self, var_name): raise NotImplementedError("Cannot exchange memory references across process boundary") From 2fb9ffb98fd794a15d45562c63fb52ea59f92e95 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 26 Oct 2023 15:54:27 +0200 Subject: [PATCH 30/30] Kill subprocesses instead of letting them terminate --- grpc4bmi/bmi_client_docker.py | 3 ++- grpc4bmi/bmi_client_subproc.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/grpc4bmi/bmi_client_docker.py b/grpc4bmi/bmi_client_docker.py index 328400a..e102c5c 100644 --- a/grpc4bmi/bmi_client_docker.py +++ b/grpc4bmi/bmi_client_docker.py @@ -102,7 +102,8 @@ def __init__(self, image: str, work_dir: str, image_port=50051, host=None, super(BmiClientDocker, self).__init__(BmiClient.create_grpc_channel(port=port, host=host), timeout=timeout) def __del__(self): - self.container.stop() + if hasattr(self, 'container'): + self.container.stop() def logs(self) -> str: """Returns complete combined stdout and stderr written by the Docker container. diff --git a/grpc4bmi/bmi_client_subproc.py b/grpc4bmi/bmi_client_subproc.py index 9b84081..6e81d92 100644 --- a/grpc4bmi/bmi_client_subproc.py +++ b/grpc4bmi/bmi_client_subproc.py @@ -26,7 +26,7 @@ def __init__(self, module_name, path=None, timeout=None, delay=1): super(BmiClientSubProcess, self).__init__(BmiClient.create_grpc_channel(port=port, host=host), timeout=timeout) def __del__(self): - self.pipe.terminate() + self.pipe.kill() self.pipe.wait(timeout=0.1) def get_value_ref(self, var_name):