Skip to content

Commit

Permalink
Improved back-and-forth type-casting between Python and Java.
Browse files Browse the repository at this point in the history
* Added test that rewrites every node property in the demo model.
* Except for 'Selection' types, which can be read, but not written.
* They would have to be modified via the sub-group `selection()`.
* The test is deactivated in client-server mode where it is very slow.
* Added support for 'DoubleRowMatrix' Java types.
* They are mapped to 2d NumPy arrays of `dtype=object`.
* Lists are now explicitly cast to Java string arrays/matrices.
* Fixed handling of empty lists. (#25)
* Return `None` for empty Java strings.
* Return empty dictionary if node has no properties.
  • Loading branch information
john-hen committed Apr 9, 2021
1 parent f2b1065 commit e9165c4
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 89 deletions.
55 changes: 42 additions & 13 deletions mph/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ def property(self, name, value=None):
def properties(self):
"""Returns names and values of all node properties as a dictionary."""
java = self.java
if not hasattr(java, 'properties'):
return {}
names = sorted(str(name) for name in java.properties())
return {name: get(java, name) for name in names}

Expand Down Expand Up @@ -386,14 +388,23 @@ def cast(value):
elif isinstance(value, Path):
return JString(str(value))
elif isinstance(value, (list, tuple)):
return value
dimension = array(value).ndim
if value == [[]]:
value = []
return JArray(JString, dimension)(value)
elif isinstance(value, ndarray):
if value.dtype.kind == 'i':
return JArray(JInt, value.ndim)(value)
if value.dtype.kind == 'b':
return JArray(JBoolean, value.ndim)(value)
elif value.dtype.kind == 'f':
return JArray(JDouble, value.ndim)(value)
elif value.dtype.kind == 'b':
return JArray(JBoolean, value.ndim)(value)
elif value.dtype.kind == 'i':
return JArray(JInt, value.ndim)(value)
elif value.dtype.kind == 'O':
if value.ndim > 2:
error = 'Cannot cast object arrays with more than two rows.'
logger.error(error)
raise TypeError(error)
return JArray(JDouble, 2)([row.astype(float) for row in value])
else:
error = f'Cannot cast arrays of data type "{value.dtype}".'
logger.error(error)
Expand All @@ -419,6 +430,19 @@ def get(java, name):
return array(java.getDoubleArray(name))
elif datatype == 'DoubleMatrix':
return array([line for line in java.getDoubleMatrix(name)])
elif datatype == 'DoubleRowMatrix':
value = java.getDoubleMatrix(name)
if len(value) == 0:
rows = []
elif len(value) == 1:
rows = [array(value[0])]
elif len(value) == 2:
rows = [array(value[0]), array(value[1])]
else:
error = 'Cannot convert double-row matrix with more than two rows.'
logger.error(error)
raise TypeError(error)
return array(rows, dtype=object)
elif datatype == 'File':
return Path(str(java.getString(name)))
elif datatype == 'Int':
Expand All @@ -429,15 +453,23 @@ def get(java, name):
return array([line for line in java.getIntMatrix(name)])
elif datatype == 'None':
return None
elif datatype == 'Selection':
return [str(string) for string in java.getEntryKeys(name)]
elif datatype == 'String':
return str(java.getString(name))
value = java.getString(name)
return str(value) if value else None
elif datatype == 'StringArray':
return [str(string) for string in java.getStringArray(name)]
elif datatype == 'StringMatrix':
return [[str(string) for string in line]
for line in java.getStringMatrix(name)]
value = java.getStringMatrix(name)
if value:
return [[str(string) for string in line] for line in value]
else:
return [[]]
else:
raise TypeError(f'Cannot convert Java data type "{datatype}".')
error = f'Cannot convert Java data type "{datatype}".'
logger.error(error)
raise TypeError(error)


########################################
Expand Down Expand Up @@ -489,10 +521,7 @@ def inspect(java):
print('properties:')
names = [str(name) for name in java.properties()]
for name in names:
try:
value = get(java, name)
except TypeError:
value = '[?]'
value = get(java, name)
print(f' {name}: {value}')

# Define a list of common methods to be suppressed in the output.
Expand Down
73 changes: 10 additions & 63 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import parent # noqa F401
import mph
from pathlib import Path
from numpy import array, isclose
from sys import argv
import logging
import warnings
Expand Down Expand Up @@ -284,68 +283,16 @@ def test_descriptions():


def test_property():
# Test conversion to and from 'Boolean'.
old = model.property('functions/image', 'flipx')
model.property('functions/image', 'flipx', False)
assert model.property('functions/image', 'flipx') is False
model.property('functions/image', 'flipx', old)
assert model.property('functions/image', 'flipx') == old
# Test conversion to and from 'Double'.
old = model.property('functions/image', 'xmin')
model.property('functions/image', 'xmin', -10.0)
assert isclose(model.property('functions/image', 'xmin'), -10)
model.property('functions/image', 'xmin', old)
assert isclose(model.property('functions/image', 'xmin'), old)
# Test conversion to and from 'DoubleArray'.
old = model.property('exports/field', 'outersolnumindices')
new = array([1.0, 2.0, 3.0])
model.property('exports/field', 'outersolnumindices', new)
assert isclose(model.property('exports/field', 'outersolnumindices'),
new).all()
model.property('exports/field', 'outersolnumindices', old)
assert isclose(model.property('exports/field', 'outersolnumindices'),
old).all()
# Test conversion to and from 'File'.
old = model.property('functions/image', 'filename')
model.property('functions/image', 'filename', Path('new.tif'))
assert model.property('functions/image', 'filename') == Path('new.tif')
model.property('functions/image', 'filename', old)
assert model.property('functions/image', 'filename') == old
# Test conversion to and from 'Int'.
old = model.property('functions/image', 'refreshcount')
model.property('functions/image', 'refreshcount', 1)
assert model.property('functions/image', 'refreshcount') == 1
model.property('functions/image', 'refreshcount', old)
assert model.property('functions/image', 'refreshcount') == old
# Test conversion to and from 'IntArray'.
old = model.property('plots/evolution', 'solnum')
new = array([1, 2, 3])
model.property('plots/evolution', 'solnum', new)
assert (model.property('plots/evolution', 'solnum') == new).all()
model.property('plots/evolution', 'solnum', old)
assert (model.property('plots/evolution', 'solnum') == old).all()
# Test conversion from 'None'.
none = model.property('functions/image', 'exportfilename')
assert none is None
# Test conversion to and from 'String'.
old = model.property('functions/image', 'funcname')
model.property('functions/image', 'funcname', 'new')
assert model.property('functions/image', 'funcname') == 'new'
model.property('functions/image', 'funcname', old)
assert model.property('functions/image', 'funcname') == old
# Test conversion to and from 'StringArray'.
old = model.property('exports/vector', 'descr')
model.property('exports/vector', 'descr', ['x', 'y', 'z'])
assert model.property('exports/vector', 'descr') == ['x', 'y', 'z']
model.property('exports/vector', 'descr', old)
assert model.property('exports/vector', 'descr') == old
# Test conversion to and from 'StringMatrix'.
old = model.property('plots/evolution', 'plotonsecyaxis')
new = [['medium 1', 'on', 'ptgr1'], ['medium 2', 'on', 'ptgr2']]
model.property('plots/evolution', 'plotonsecyaxis', new)
assert model.property('plots/evolution', 'plotonsecyaxis') == new
model.property('plots/evolution', 'plotonsecyaxis', old)
assert model.property('plots/evolution', 'plotonsecyaxis') == old
assert model.property('functions/step', 'funcname') == 'step'
model.property('functions/step', 'funcname', 'renamed')
assert model.property('functions/step', 'funcname') == 'renamed'
model.property('functions/step', 'funcname', 'step')
assert model.property('functions/step', 'funcname') == 'step'
assert model.property('functions/step', 'from') == 0.0
model.property('functions/step', 'from', 0.1)
assert model.property('functions/step', 'from') == 0.1
model.property('functions/step', 'from', 0.0)
assert model.property('functions/step', 'from') == 0.0


def test_properties():
Expand Down
100 changes: 89 additions & 11 deletions tests/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import mph
from mph import node
from mph.node import Node
from numpy import array, isclose
from sys import argv
from pathlib import Path
import logging
Expand Down Expand Up @@ -166,18 +167,95 @@ def test_rename():
assert not renamed.exists()


def test_rewrite(node):
java = node.java
if hasattr(java, 'properties'):
names = [str(name) for name in node.java.properties()]
else:
names = []
for name in names:
value = node.property(name)
# Changing selections is not (yet) implemented.
if node.java.getValueType(name) == 'Selection':
continue
# Writing "sol" changes certain node names.
if name == 'sol':
continue
node.property(name, value)
for child in node:
test_rewrite(child)


def test_property():
node = Node(model, 'functions/step')
assert node.property('funcname') == 'step'
node.property('funcname', 'renamed')
assert node.property('funcname') == 'renamed'
node.property('funcname', 'step')
assert node.property('funcname') == 'step'
assert node.property('from') == 0.0
node.property('from', 0.1)
assert node.property('from') == 0.1
node.property('from', 0.0)
assert node.property('from') == 0.0
root = Node(model, '')
image = Node(model, 'functions/image')
plot = Node(model, 'plots/evolution')
field = Node(model, 'exports/field')
vector = Node(model, 'exports/vector')
# Test conversion to and from 'Boolean'.
old = image.property('flipx')
image.property('flipx', False)
assert image.property('flipx') is False
image.property('flipx', old)
assert image.property('flipx') == old
# Test conversion to and from 'Double'.
old = image.property('xmin')
image.property('xmin', -10.0)
assert isclose(image.property('xmin'), -10)
image.property('xmin', old)
assert isclose(image.property('xmin'), old)
# Test conversion to and from 'DoubleArray'.
old = field.property('outersolnumindices')
new = array([1.0, 2.0, 3.0])
field.property('outersolnumindices', new)
assert isclose(field.property('outersolnumindices'), new).all()
field.property('outersolnumindices', old)
assert isclose(field.property('outersolnumindices'), old).all()
# Test conversion to and from 'File'.
old = image.property('filename')
image.property('filename', Path('new.tif'))
assert image.property('filename') == Path('new.tif')
image.property('filename', old)
assert image.property('filename') == old
# Test conversion to and from 'Int'.
old = image.property('refreshcount')
image.property('refreshcount', 1)
assert image.property('refreshcount') == 1
image.property('refreshcount', old)
assert image.property('refreshcount') == old
# Test conversion to and from 'IntArray'.
old = plot.property('solnum')
new = array([1, 2, 3])
plot.property('solnum', new)
assert (plot.property('solnum') == new).all()
plot.property('solnum', old)
assert (plot.property('solnum') == old).all()
# Test conversion from 'None'.
none = image.property('exportfilename')
assert none is None
# Test conversion to and from 'String'.
old = image.property('funcname')
image.property('funcname', 'new')
assert image.property('funcname') == 'new'
image.property('funcname', old)
assert image.property('funcname') == old
# Test conversion to and from 'StringArray'.
old = vector.property('descr')
vector.property('descr', ['x', 'y', 'z'])
assert vector.property('descr') == ['x', 'y', 'z']
vector.property('descr', old)
assert vector.property('descr') == old
# Test conversion to and from 'StringMatrix'.
old = plot.property('plotonsecyaxis')
new = [['medium 1', 'on', 'ptgr1'], ['medium 2', 'on', 'ptgr2']]
plot.property('plotonsecyaxis', new)
assert plot.property('plotonsecyaxis') == new
plot.property('plotonsecyaxis', old)
assert plot.property('plotonsecyaxis') == old
# Read and write back every node property in the model.
if not client.port:
# Skip test in client-server mode where it's excruciatingly slow.
test_rewrite(root)


def test_properties():
Expand Down
5 changes: 3 additions & 2 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ def test_getitem():


def test_remove():
assert model in client
name = model.name()
assert name in client.names()
client.remove(model)
assert model not in client
assert name not in client.names()
message = ''
try:
model.java.component()
Expand Down

0 comments on commit e9165c4

Please sign in to comment.