Skip to content
Closed
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- Added support for struct (dict) options via the new `kStruct` DataType enum
value, enabling complex nested data to be declared and passed as discipline
options (#49).
- Added discrete variable support throughout the stack. Disciplines can
now declare discrete inputs/outputs via `add_discrete_input` /
`add_discrete_output`. Discrete data is serialized as
Expand Down
2 changes: 1 addition & 1 deletion philote_mdo/general/discipline.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def add_option(self, name, type):
the name of the option being added
type : string
the data type of the option. acceptable types are 'bool', 'int',
'float'
'float', 'str', 'dict'
"""
self.options_list[name] = type

Expand Down
2 changes: 2 additions & 0 deletions philote_mdo/general/discipline_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def get_available_options(self):
type_str = "float"
if val == data.kString:
type_str = "str"
if val == data.kStruct:
type_str = "dict"
self.options_list[name] = type_str

def send_options(self, options):
Expand Down
2 changes: 2 additions & 0 deletions philote_mdo/general/discipline_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ def GetAvailableOptions(self, request, context):
type = data.kDouble
elif val == "str":
type = data.kString
elif val == "dict":
type = data.kStruct
else:
raise ValueError(
"Invalid value for discipline option '{}'".format(name)
Expand Down
8 changes: 4 additions & 4 deletions philote_mdo/generated/data_pb2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
_runtime_version.ValidateProtobufRuntimeVersion(_runtime_version.Domain.PUBLIC, 5, 27, 2, '', 'data.proto')
_sym_db = _symbol_database.Default()
from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ndata.proto\x12\x07philote\x1a\x1cgoogle/protobuf/struct.proto"}\n\x14DisciplineProperties\x12\x12\n\ncontinuous\x18\x01 \x01(\x08\x12\x16\n\x0edifferentiable\x18\x02 \x01(\x08\x12\x1a\n\x12provides_gradients\x18\x03 \x01(\x08\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0f\n\x07version\x18\x05 \x01(\t"#\n\rStreamOptions\x12\x12\n\nnum_double\x18\x01 \x01(\x03"?\n\x0bOptionsList\x12\x0f\n\x07options\x18\x01 \x03(\t\x12\x1f\n\x04type\x18\x02 \x03(\x0e2\x11.philote.DataType"=\n\x11DisciplineOptions\x12(\n\x07options\x18\x01 \x01(\x0b2\x17.google.protobuf.Struct"c\n\x10VariableMetaData\x12#\n\x04type\x18\x01 \x01(\x0e2\x15.philote.VariableType\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05shape\x18\x04 \x03(\x03\x12\r\n\x05units\x18\x05 \x01(\t"@\n\x10PartialsMetaData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07subname\x18\x02 \x01(\t\x12\r\n\x05shape\x18\x03 \x03(\x03"u\n\x05Array\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07subname\x18\x02 \x01(\t\x12\r\n\x05start\x18\x03 \x01(\x03\x12\x0b\n\x03end\x18\x04 \x01(\x03\x12#\n\x04type\x18\x05 \x01(\x0e2\x15.philote.VariableType\x12\x0c\n\x04data\x18\x06 \x03(\x01"l\n\x10DiscreteVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x04type\x18\x02 \x01(\x0e2\x15.philote.VariableType\x12%\n\x05value\x18\x03 \x01(\x0b2\x16.google.protobuf.Value"q\n\x0fVariableMessage\x12$\n\ncontinuous\x18\x01 \x01(\x0b2\x0e.philote.ArrayH\x00\x12-\n\x08discrete\x18\x02 \x01(\x0b2\x19.philote.DiscreteVariableH\x00B\t\n\x07payload*9\n\x08DataType\x12\t\n\x05kBool\x10\x00\x12\x08\n\x04kInt\x10\x01\x12\x0b\n\x07kDouble\x10\x02\x12\x0b\n\x07kString\x10\x03*m\n\x0cVariableType\x12\n\n\x06kInput\x10\x00\x12\x12\n\x0ekDiscreteInput\x10\x01\x12\r\n\tkResidual\x10\x02\x12\x0b\n\x07kOutput\x10\x03\x12\x13\n\x0fkDiscreteOutput\x10\x04\x12\x0c\n\x08kPartial\x10\x05B\x11\n\x0forg.philote.mdob\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ndata.proto\x12\x07philote\x1a\x1cgoogle/protobuf/struct.proto"}\n\x14DisciplineProperties\x12\x12\n\ncontinuous\x18\x01 \x01(\x08\x12\x16\n\x0edifferentiable\x18\x02 \x01(\x08\x12\x1a\n\x12provides_gradients\x18\x03 \x01(\x08\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x0f\n\x07version\x18\x05 \x01(\t"#\n\rStreamOptions\x12\x12\n\nnum_double\x18\x01 \x01(\x03"?\n\x0bOptionsList\x12\x0f\n\x07options\x18\x01 \x03(\t\x12\x1f\n\x04type\x18\x02 \x03(\x0e2\x11.philote.DataType"=\n\x11DisciplineOptions\x12(\n\x07options\x18\x01 \x01(\x0b2\x17.google.protobuf.Struct"c\n\x10VariableMetaData\x12#\n\x04type\x18\x01 \x01(\x0e2\x15.philote.VariableType\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05shape\x18\x04 \x03(\x03\x12\r\n\x05units\x18\x05 \x01(\t"@\n\x10PartialsMetaData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07subname\x18\x02 \x01(\t\x12\r\n\x05shape\x18\x03 \x03(\x03"u\n\x05Array\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07subname\x18\x02 \x01(\t\x12\r\n\x05start\x18\x03 \x01(\x03\x12\x0b\n\x03end\x18\x04 \x01(\x03\x12#\n\x04type\x18\x05 \x01(\x0e2\x15.philote.VariableType\x12\x0c\n\x04data\x18\x06 \x03(\x01"l\n\x10DiscreteVariable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12#\n\x04type\x18\x02 \x01(\x0e2\x15.philote.VariableType\x12%\n\x05value\x18\x03 \x01(\x0b2\x16.google.protobuf.Value"q\n\x0fVariableMessage\x12$\n\ncontinuous\x18\x01 \x01(\x0b2\x0e.philote.ArrayH\x00\x12-\n\x08discrete\x18\x02 \x01(\x0b2\x19.philote.DiscreteVariableH\x00B\t\n\x07payload*F\n\x08DataType\x12\t\n\x05kBool\x10\x00\x12\x08\n\x04kInt\x10\x01\x12\x0b\n\x07kDouble\x10\x02\x12\x0b\n\x07kString\x10\x03\x12\x0b\n\x07kStruct\x10\x04*m\n\x0cVariableType\x12\n\n\x06kInput\x10\x00\x12\x12\n\x0ekDiscreteInput\x10\x01\x12\r\n\tkResidual\x10\x02\x12\x0b\n\x07kOutput\x10\x03\x12\x13\n\x0fkDiscreteOutput\x10\x04\x12\x0c\n\x08kPartial\x10\x05B\x11\n\x0forg.philote.mdob\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'data_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals['DESCRIPTOR']._loaded_options = None
_globals['DESCRIPTOR']._serialized_options = b'\n\x0forg.philote.mdo'
_globals['_DATATYPE']._serialized_start = 856
_globals['_DATATYPE']._serialized_end = 913
_globals['_VARIABLETYPE']._serialized_start = 915
_globals['_VARIABLETYPE']._serialized_end = 1024
_globals['_DATATYPE']._serialized_end = 926
_globals['_VARIABLETYPE']._serialized_start = 928
_globals['_VARIABLETYPE']._serialized_end = 1037
_globals['_DISCIPLINEPROPERTIES']._serialized_start = 53
_globals['_DISCIPLINEPROPERTIES']._serialized_end = 178
_globals['_STREAMOPTIONS']._serialized_start = 180
Expand Down
2 changes: 2 additions & 0 deletions philote_mdo/generated/data_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class DataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
kInt: _ClassVar[DataType]
kDouble: _ClassVar[DataType]
kString: _ClassVar[DataType]
kStruct: _ClassVar[DataType]

class VariableType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
Expand All @@ -25,6 +26,7 @@ kBool: DataType
kInt: DataType
kDouble: DataType
kString: DataType
kStruct: DataType
kInput: VariableType
kDiscreteInput: VariableType
kResidual: VariableType
Expand Down
2 changes: 2 additions & 0 deletions philote_mdo/openmdao/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def declare_options(opt_list, options):
opt_type = float
elif type_str == "str":
opt_type = str
elif type_str == "dict":
opt_type = dict

options.declare(name, types=opt_type)

Expand Down
45 changes: 45 additions & 0 deletions tests/test_discipline_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,51 @@ def test_get_available_options(self, mock_discipline_stub):
}
self.assertEqual(instance.options_list, expected_options_list)

@patch("philote_mdo.generated.disciplines_pb2_grpc.DisciplineServiceStub")
def test_get_available_options_with_dict_type(self, mock_discipline_stub):
"""
Tests that get_available_options correctly maps kStruct to 'dict'.
"""
mock_channel = Mock()
mock_stub = mock_discipline_stub.return_value

instance = DisciplineClient(channel=mock_channel)

mock_options = MagicMock()
mock_options.options = ["config", "flag"]
mock_options.type = [data.kStruct, data.kBool]
instance._disc_stub.GetAvailableOptions.return_value = mock_options

instance.get_available_options()

expected_options_list = {
"config": "dict",
"flag": "bool",
}
self.assertEqual(instance.options_list, expected_options_list)

def test_send_options_with_nested_dict(self):
"""
Tests that send_options correctly serializes nested dict values via Struct.
"""
mock_stub = Mock()
mock_channel = Mock()
mock_channel.stub.return_value = mock_stub

client = DisciplineClient(channel=mock_channel)
client._disc_stub = mock_stub

options = {
"config": {"solver": "newton", "tol": 1e-6},
"name": "test",
}

client.send_options(options)

expected_proto_options = data.DisciplineOptions()
expected_proto_options.options.update(options)
mock_stub.SetOptions.assert_called_once_with(expected_proto_options)

def test_send_options(self):
# mock gRPC stub and channel
mock_stub = Mock()
Expand Down
50 changes: 50 additions & 0 deletions tests/test_discipline_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,56 @@ def test_process_inputs(self):
self.assertEqual(flat_inputs["x"].tolist(), [1.0, 2.0, 3.0, 4.0, 5.0, 0.0])
self.assertEqual(flat_outputs["f"].tolist(), [0.1, 0.2, 0.0])

def test_get_available_options_with_dict_type(self):
"""
Tests that GetAvailableOptions correctly maps dict options to kStruct.
"""
server = DisciplineServer()

request_mock = Mock()
context_mock = None

server._discipline = Discipline()
server._discipline.options_list = {
"config": "dict",
"flag": "bool",
}

results = server.GetAvailableOptions(request_mock, context_mock)

expected_options = ["config", "flag"]
expected_types = [data.kStruct, data.kBool]

self.assertEqual(results.options, expected_options)
self.assertEqual(results.type, expected_types)

def test_set_options_with_nested_dict(self):
"""
Tests that SetOptions correctly passes nested dict values through.
"""
server = DisciplineServer()

request_mock = Mock()
context_mock = Mock()

# set nested dict options in the request
request_mock.options = {
"config": {"solver": "newton", "tol": 1e-6, "nested": {"a": 1}},
"name": "test",
}

discipline_mock = Mock()
server._discipline = discipline_mock

server.SetOptions(request_mock, context_mock)

server._discipline.set_options.assert_called_once_with(
{
"config": {"solver": "newton", "tol": 1e-6, "nested": {"a": 1}},
"name": "test",
}
)

def test_get_available_options_invalid_type_raises_error(self):
"""
Tests that GetAvailableOptions raises ValueError for invalid option types.
Expand Down
20 changes: 20 additions & 0 deletions tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ def test_get_available_options_with_str_type(self):
# The method should complete without error and return options
self.assertIsNotNone(result)

def test_get_available_options_with_dict_type(self):
"""
Test GetAvailableOptions with dict option type (covers kStruct mapping).
"""
server = DisciplineServer()
discipline = Mock()

discipline.options_list = {"config": "dict"}

server.attach_discipline(discipline)

request = Mock()
context = Mock()

result = server.GetAvailableOptions(request, context)

self.assertIsNotNone(result)
self.assertEqual(list(result.options), ["config"])
self.assertEqual(list(result.type), [data.kStruct])

def test_get_available_options_with_invalid_type(self):
"""
Test GetAvailableOptions with invalid option type (covers lines 100-103).
Expand Down
Loading