Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the new `VariableMessage` wrapper. The OpenMDAO bindings
(`RemoteExplicitComponent`, `RemoteImplicitComponent`) automatically
discover and forward discrete variables.
- Added comprehensive input validation and error handling across the
framework. Introduces custom exception classes (`PhiloteValidationError`,
`PhiloteServerError`), parameter validation in discipline base classes
(`add_input`, `add_output`, `add_option`, `declare_partials`), proper
gRPC error propagation via `context.abort()` with appropriate status
codes in all server RPC methods, and client-side input validation with
gRPC error wrapping (#46).

### Bug Fixes

- Fixed bare `except` to `except ImportError` in `examples/__init__.py`.
- Fixed missing space in `RemoteImplicitComponent` error message
("will notbe" -> "will not be").
- Fixed `SellarMDA` promoted-input ambiguity that newer OpenMDAO releases
reject during `final_setup`. The `x` and `z` defaults were being set on
the inner `cycle` subgroup, but `obj_cmp` promoted the same variables
Expand Down
45 changes: 45 additions & 0 deletions philote_mdo/general/discipline.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
# therein. The DoD does not exercise any editorial, security, or other
# control over the information you may find at these locations.
import philote_mdo.generated.data_pb2 as data
from philote_mdo.utils.validation import (
validate_name,
validate_shape,
validate_units,
validate_option_type,
PhiloteValidationError,
)


class Discipline:
Expand Down Expand Up @@ -71,6 +78,12 @@ def add_option(self, name, type):
the data type of the option. acceptable types are 'bool', 'int',
'float', 'str', 'dict'
"""
validate_name(name, "add_option")
validate_option_type(type, name)
if name in self.options_list:
raise PhiloteValidationError(
f"add_option: option '{name}' is already defined."
)
self.options_list[name] = type

def add_input(self, name, shape=(1,), units=""):
Expand All @@ -86,6 +99,13 @@ def add_input(self, name, shape=(1,), units=""):
units : string
the unit definition for the input variable
"""
validate_name(name, "add_input")
validate_shape(shape, "add_input")
validate_units(units, "add_input")
if any(v.name == name and v.type == data.VariableType.kInput for v in self._var_meta):
raise PhiloteValidationError(
f"add_input: input '{name}' is already defined."
)
meta = data.VariableMetaData()
meta.type = data.VariableType.kInput
meta.name = name
Expand All @@ -107,6 +127,14 @@ def add_discrete_input(self, name, default=None):
default : object, optional
the default value for the discrete input
"""
validate_name(name, "add_discrete_input")
if any(
v.name == name and v.type == data.VariableType.kDiscreteInput
for v in self._discrete_var_meta
):
raise PhiloteValidationError(
f"add_discrete_input: discrete input '{name}' is already defined."
)
meta = data.VariableMetaData()
meta.type = data.VariableType.kDiscreteInput
meta.name = name
Expand All @@ -126,6 +154,14 @@ def add_discrete_output(self, name, default=None):
default : object, optional
the default value for the discrete output
"""
validate_name(name, "add_discrete_output")
if any(
v.name == name and v.type == data.VariableType.kDiscreteOutput
for v in self._discrete_var_meta
):
raise PhiloteValidationError(
f"add_discrete_output: discrete output '{name}' is already defined."
)
meta = data.VariableMetaData()
meta.type = data.VariableType.kDiscreteOutput
meta.name = name
Expand All @@ -144,6 +180,13 @@ def add_output(self, name, shape=(1,), units=""):
units : string
the unit definition for the output variable
"""
validate_name(name, "add_output")
validate_shape(shape, "add_output")
validate_units(units, "add_output")
if any(v.name == name and v.type == data.VariableType.kOutput for v in self._var_meta):
raise PhiloteValidationError(
f"add_output: output '{name}' is already defined."
)
out_meta = data.VariableMetaData()
out_meta.type = data.VariableType.kOutput
out_meta.name = name
Expand All @@ -164,6 +207,8 @@ def declare_partials(self, func, var):
"""
Defines partials that will be determined using the analysis server.
"""
validate_name(func, "declare_partials (func)")
validate_name(var, "declare_partials (var)")
self._partials_meta += [data.PartialsMetaData(name=func, subname=var)]

def initialize(self):
Expand Down
20 changes: 17 additions & 3 deletions philote_mdo/general/discipline_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
import philote_mdo.generated.disciplines_pb2_grpc as disc
import philote_mdo.utils as utils
from philote_mdo.general.discipline_server import _python_to_value, _value_to_python
from philote_mdo.utils.validation import (
PhiloteValidationError,
validate_is_dict,
validate_numpy_array,
)


class DisciplineClient:
Expand Down Expand Up @@ -114,6 +119,7 @@ def send_options(self, options):
-------
None
"""
validate_is_dict(options, "send_options")
proto_options = data.DisciplineOptions()
proto_options.options.update(options)
self._disc_stub.SetOptions(proto_options)
Expand Down Expand Up @@ -158,6 +164,14 @@ def _assemble_input_messages(
Both continuous and discrete inputs are wrapped in ``VariableMessage``
envelopes.
"""
validate_is_dict(inputs, "_assemble_input_messages (inputs)")
for input_name, value in inputs.items():
validate_numpy_array(value, input_name)
if outputs is not None:
validate_is_dict(outputs, "_assemble_input_messages (outputs)")
for output_name, value in outputs.items():
validate_numpy_array(value, output_name)

messages = []

# Continuous inputs
Expand Down Expand Up @@ -251,7 +265,7 @@ def _recover_outputs(self, responses):
if len(arr.data) > 0:
flat_outputs[arr.name][b:e] = arr.data
else:
raise ValueError(
raise PhiloteValidationError(
"Expected continuous variables, but array is empty."
)

Expand Down Expand Up @@ -289,7 +303,7 @@ def _recover_residuals(self, responses):
if len(arr.data) > 0:
flat_residuals[arr.name][b:e] = arr.data
else:
raise ValueError(
raise PhiloteValidationError(
"Expected continuous variables, but array is empty."
)

Expand Down Expand Up @@ -336,7 +350,7 @@ def _recover_partials(self, responses):
if len(arr.data) > 0:
flat_p[(arr.name, arr.subname)][b:e] = arr.data
else:
raise ValueError(
raise PhiloteValidationError(
"Expected continuous outputs for the "
"partials, but array was empty."
)
Expand Down
85 changes: 54 additions & 31 deletions philote_mdo/general/discipline_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
# the linked websites, of the information, products, or services contained
# therein. The DoD does not exercise any editorial, security, or other
# control over the information you may find at these locations.
import grpc
import numpy as np

import philote_mdo.generated.data_pb2 as data
import philote_mdo.generated.disciplines_pb2_grpc as disc
from google.protobuf.empty_pb2 import Empty
from google.protobuf import struct_pb2
from philote_mdo.utils import PairDict, get_flattened_view
from philote_mdo.utils.validation import PhiloteValidationError


class DisciplineServer(disc.DisciplineService):
Expand Down Expand Up @@ -85,48 +87,69 @@ def GetAvailableOptions(self, request, context):
"""
RPC that gets the names and types of all available discipline options.
"""
opts_dict = self._discipline.options_list
opts = data.OptionsList()

for name, val in opts_dict.items():
opts.options.append(name)

# assign the correct data type
if val == "bool":
type = data.kBool
elif val == "int":
type = data.kInt
elif val == "float":
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)
)
try:
opts_dict = self._discipline.options_list
opts = data.OptionsList()

for name, val in opts_dict.items():
opts.options.append(name)

# assign the correct data type
if val == "bool":
type = data.kBool
elif val == "int":
type = data.kInt
elif val == "float":
type = data.kDouble
elif val == "str":
type = data.kString
elif val == "dict":
type = data.kStruct
else:
raise PhiloteValidationError(
"Invalid value for discipline option '{}'".format(name)
)

opts.type.append(type)
opts.type.append(type)

return opts
return opts
except PhiloteValidationError as e:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
except Exception as e:
context.abort(
grpc.StatusCode.INTERNAL, f"GetAvailableOptions failed: {e}"
)

def SetOptions(self, request, context):
"""
RPC that sets the discipline options.
"""
options = request.options
self._discipline.set_options(options)
return Empty()
try:
options = request.options
self._discipline.set_options(options)
return Empty()
except PhiloteValidationError as e:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
except Exception as e:
context.abort(
grpc.StatusCode.INTERNAL, f"SetOptions failed: {e}"
)

def Setup(self, request, context):
"""
RPC that runs the setup function
"""
self._discipline._clear_data()
self._discipline.setup()
self._discipline.setup_partials()
return Empty()
try:
self._discipline._clear_data()
self._discipline.setup()
self._discipline.setup_partials()
return Empty()
except PhiloteValidationError as e:
context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
except Exception as e:
context.abort(
grpc.StatusCode.INTERNAL, f"Setup failed: {e}"
)

def GetVariableDefinitions(self, request, context):
"""
Expand Down Expand Up @@ -237,7 +260,7 @@ def process_inputs(
elif arr.type == data.VariableType.kOutput:
flat_outputs[arr.name][b : e + 1] = arr.data
else:
raise ValueError(
raise PhiloteValidationError(
"Expected continuous variables but arrays were"
" empty for variable %s." % (arr.name)
)
Expand Down
35 changes: 24 additions & 11 deletions philote_mdo/general/explicit_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# control over the information you may find at these locations.
import grpc
from philote_mdo.general.discipline_client import DisciplineClient
from philote_mdo.utils.validation import PhiloteServerError, validate_is_dict
import philote_mdo.generated.disciplines_pb2_grpc as disc


Expand Down Expand Up @@ -59,11 +60,17 @@ def run_compute(self, inputs, discrete_inputs=None):
Continuous outputs, or (continuous outputs, discrete outputs) when
the server returns discrete output data.
"""
messages = self._assemble_input_messages(
inputs, discrete_inputs=discrete_inputs
)
responses = self._expl_stub.ComputeFunction(iter(messages))
return self._recover_outputs(responses)
validate_is_dict(inputs, "run_compute (inputs)")
try:
messages = self._assemble_input_messages(
inputs, discrete_inputs=discrete_inputs
)
responses = self._expl_stub.ComputeFunction(iter(messages))
return self._recover_outputs(responses)
except grpc.RpcError as e:
raise PhiloteServerError(
f"Server error during run_compute: {e.details()}"
) from e

def run_compute_partials(self, inputs, discrete_inputs=None):
"""
Expand All @@ -77,10 +84,16 @@ def run_compute_partials(self, inputs, discrete_inputs=None):
discrete_inputs : dict, optional
Discrete input values.
"""
messages = self._assemble_input_messages(
inputs, discrete_inputs=discrete_inputs
)
responses = self._expl_stub.ComputeGradient(iter(messages))
partials = self._recover_partials(responses)
validate_is_dict(inputs, "run_compute_partials (inputs)")
try:
messages = self._assemble_input_messages(
inputs, discrete_inputs=discrete_inputs
)
responses = self._expl_stub.ComputeGradient(iter(messages))
partials = self._recover_partials(responses)

return partials
return partials
except grpc.RpcError as e:
raise PhiloteServerError(
f"Server error during run_compute_partials: {e.details()}"
) from e
Loading
Loading