Skip to content

Commit

Permalink
Add output arg + get package from model name + more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sverhoeven committed Oct 2, 2023
1 parent 57bb251 commit 3607d56
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 74 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PACKAGE>,<BMI-IMPLEMENTATION-NAME>,<MODEL-NAME> --port <PORT>
run-bmi-server --lang julia --name <MODEL-NAME> --port <PORT>
```

For example with [Wflow.jl](https://github.com/Deltares/Wflow.jl/) use
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/container/building.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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("<JULIA-PACKAGE-NAME>")'
# Run bmi server
ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", "<PACKAGE>,<BMI-IMPLEMENTATION-NAME>,<MODEL-NAME>"]
ENTRYPOINT ["run-bmi-server", "--lang", "julia", "--name", "<MODEL-NAME>"]
# Expose the magic grpc4bmi port
EXPOSE 55555
Expand Down
4 changes: 2 additions & 2 deletions docs/server/Julia.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ The server can be started with

.. code-block:: sh
run-bmi-server --lang julia --name <PACKAGE>,<BMI-IMPLEMENTATION-NAME>,<MODEL-NAME> --port <PORT>
run-bmi-server --lang julia --name <MODEL-NAME> --port <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.
118 changes: 80 additions & 38 deletions grpc4bmi/bmi_julia_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
----------
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
----------
Expand All @@ -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
----------
Expand All @@ -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
----------
Expand All @@ -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
----------
Expand All @@ -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.
Expand Down Expand Up @@ -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
-------
Expand All @@ -567,41 +599,50 @@ 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
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))
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
----------
Expand All @@ -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
3 changes: 1 addition & 2 deletions grpc4bmi/run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 3607d56

Please sign in to comment.