Skip to content

Commit

Permalink
Merge branch 'julia' of github.com:eWaterCycle/grpc4bmi into julia
Browse files Browse the repository at this point in the history
  • Loading branch information
sverhoeven committed Oct 26, 2023
2 parents ead67cd + c6bddde commit 4333f13
Show file tree
Hide file tree
Showing 11 changed files with 61 additions and 120 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <MODEL-NAME> --port <PORT>
from grpc4bmi.bmi_julia_model import BmiJulia

mymodel = BmiJulia.from_name('<package>.<model>', '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.
Expand Down
1 change: 0 additions & 1 deletion cpp/bmi_grpc_server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<grpc::Server> server(builder.BuildAndStart());
Expand Down
21 changes: 1 addition & 20 deletions docs/container/building.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <python-example>` the Docker file will read

.. code-block:: Dockerfile
FROM ubuntu:jammy
MAINTAINER your name <your email address>
# Install grpc4bmi
RUN pip install grpc4bmi
# Install your BMI model:
python3 -c 'from grpc4bmi.bmi_julia_model import install;install("<JULIA-PACKAGE-NAME>")'
# Run bmi server
ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", "<MODEL-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
-------------
Expand Down
47 changes: 0 additions & 47 deletions docs/server/Julia.rst

This file was deleted.

1 change: 0 additions & 1 deletion docs/server/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ Creating a BMI server

python
R
Julia
Cpp
60 changes: 37 additions & 23 deletions grpc4bmi/bmi_julia_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -319,7 +311,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:
Expand Down Expand Up @@ -356,11 +350,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

Expand Down Expand Up @@ -397,7 +391,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),
)

Expand Down Expand Up @@ -454,7 +448,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:
Expand All @@ -468,7 +464,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:
Expand All @@ -484,7 +482,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
Expand All @@ -502,7 +502,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:
Expand All @@ -519,7 +521,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:
Expand All @@ -535,7 +539,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:
Expand Down Expand Up @@ -595,7 +601,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:
Expand All @@ -613,7 +621,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:
Expand All @@ -635,7 +645,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:
Expand All @@ -651,5 +663,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
17 changes: 0 additions & 17 deletions grpc4bmi/run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,10 +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))
Expand Down Expand Up @@ -119,12 +110,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)

Expand Down Expand Up @@ -157,8 +142,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"],
Expand Down
2 changes: 1 addition & 1 deletion test/fake.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions test/heat-images/c-julia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# gRPC server in C++ calling Julia methods

> Does not work, it segmentation faults after a few calls.
7 changes: 3 additions & 4 deletions test/test_julia.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -129,7 +128,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
Expand Down
2 changes: 1 addition & 1 deletion test/test_subproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 4333f13

Please sign in to comment.