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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
source = philote_mdo

[report]
# Fail if total coverage drops below 95%
fail_under = 95

# Exclude generated protobuf files from coverage reports
exclude_lines =
pragma: no cover
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- 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
`google.protobuf.Value` (supporting scalars, lists, and nested
structures) and multiplexed alongside continuous `Array` chunks in
the new `VariableMessage` wrapper. The OpenMDAO bindings
(`RemoteExplicitComponent`, `RemoteImplicitComponent`) automatically
discover and forward discrete variables.

### Bug Fixes

- Fixed bare `except` to `except ImportError` in `examples/__init__.py`.
- 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 All @@ -24,6 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Documentation & Infrastructure

- Added Codecov configuration (`codecov.yml`) requiring 95% coverage on
both project total and patch (new/changed lines).
- Added `fail_under = 95` to `.coveragerc` for local coverage enforcement.
- Marked unreachable import guards and defensive branches with
`pragma: no cover`.
- Updated installation instructions to reflect PyPI install option.
- Added documentation for implicit disciplines.
- Added documentation for OpenMDAO clients
Expand Down
10 changes: 10 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
coverage:
status:
project:
default:
target: 95%
threshold: 0%
patch:
default:
target: 95%
threshold: 0%
2 changes: 1 addition & 1 deletion philote_mdo/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@
try:
import openmdao.api
from .sellar import SellarGroup
except:
except ImportError: # pragma: no cover
pass
42 changes: 42 additions & 0 deletions philote_mdo/general/discipline.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def __init__(self):
# variable metadata
self._var_meta = []

# discrete variable metadata (name → default value)
self._discrete_var_meta = []

# partials metadata
self._partials_meta = []

Expand Down Expand Up @@ -90,6 +93,44 @@ def add_input(self, name, shape=(1,), units=""):
meta.units = units
self._var_meta += [meta]

def add_discrete_input(self, name, default=None):
"""
Define a discrete input.

Discrete inputs can hold any value that is representable as a
``google.protobuf.Value`` (scalars, lists, or nested dicts).

Parameters
----------
name : string
the name of the discrete input variable
default : object, optional
the default value for the discrete input
"""
meta = data.VariableMetaData()
meta.type = data.VariableType.kDiscreteInput
meta.name = name
self._discrete_var_meta += [meta]

def add_discrete_output(self, name, default=None):
"""
Define a discrete output.

Discrete outputs can hold any value that is representable as a
``google.protobuf.Value`` (scalars, lists, or nested dicts).

Parameters
----------
name : string
the name of the discrete output variable
default : object, optional
the default value for the discrete output
"""
meta = data.VariableMetaData()
meta.type = data.VariableType.kDiscreteOutput
meta.name = name
self._discrete_var_meta += [meta]

def add_output(self, name, shape=(1,), units=""):
"""
Defines a continuous output.
Expand Down Expand Up @@ -179,4 +220,5 @@ def _clear_data(self):
This function is invoked from the Setup function of the server.
"""
self._var_meta = []
self._discrete_var_meta = []
self._partials_meta = []
158 changes: 114 additions & 44 deletions philote_mdo/general/discipline_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import philote_mdo.generated.data_pb2 as data
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


class DisciplineClient:
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(self, channel):

# variable and partials metadata
self._var_meta = []
self._discrete_var_meta = []
self._partials_meta = []

# list of available options
Expand Down Expand Up @@ -123,9 +125,18 @@ def run_setup(self):
def get_variable_definitions(self):
"""
Requests the input and output metadata from the server.

Both continuous and discrete variable metadata are stored in their
respective lists.
"""
for message in self._disc_stub.GetVariableDefinitions(empty.Empty()):
self._var_meta += [message]
if message.type in (
data.VariableType.kDiscreteInput,
data.VariableType.kDiscreteOutput,
):
self._discrete_var_meta += [message]
else:
self._var_meta += [message]

def get_partials_definitions(self):
"""
Expand All @@ -135,69 +146,120 @@ def get_partials_definitions(self):
if message.name not in self._partials_meta:
self._partials_meta += [message]

def _assemble_input_messages(self, inputs, outputs=None):
def _assemble_input_messages(
self, inputs, outputs=None, discrete_inputs=None, discrete_outputs=None
):
"""
Assembles the messages for transmitting the input variables to the
server.

Both continuous and discrete inputs are wrapped in ``VariableMessage``
envelopes.
"""
messages = []

# Continuous inputs
for input_name, value in inputs.items():
for b, e in utils.get_chunk_indices(
value.size, self._stream_options.num_double
):
messages += [
data.Array(
name=input_name,
start=b,
end=e - 1,
type=data.VariableType.kInput,
data=value.ravel()[b:e],
data.VariableMessage(
continuous=data.Array(
name=input_name,
start=b,
end=e - 1,
type=data.VariableType.kInput,
data=value.ravel()[b:e],
)
)
]

# Continuous outputs (for implicit disciplines)
if outputs:
for output_name, value in outputs.items():
for b, e in utils.get_chunk_indices(
value.size, self._stream_options.num_double
):
messages += [
data.Array(
name=output_name,
start=b,
end=e - 1,
type=data.VariableType.kOutput,
data=value.ravel()[b:e],
data.VariableMessage(
continuous=data.Array(
name=output_name,
start=b,
end=e - 1,
type=data.VariableType.kOutput,
data=value.ravel()[b:e],
)
)
]

# Discrete inputs
if discrete_inputs:
for name, value in discrete_inputs.items():
messages += [
data.VariableMessage(
discrete=data.DiscreteVariable(
name=name,
type=data.VariableType.kDiscreteInput,
value=_python_to_value(value),
)
)
]

# Discrete outputs (for implicit disciplines)
if discrete_outputs:
for name, value in discrete_outputs.items():
messages += [
data.VariableMessage(
discrete=data.DiscreteVariable(
name=name,
type=data.VariableType.kDiscreteOutput,
value=_python_to_value(value),
)
)
]

return messages

def _recover_outputs(self, responses):
"""
Recovers the outputs from the stream of responses.

Returns both continuous outputs and discrete outputs.
"""
outputs = {}
flat_outputs = {}
discrete_outputs = {}

# preallocate
# preallocate continuous outputs
for out in self._var_meta:
if out.type == data.kOutput:
name = out.name
outputs[name] = np.zeros(out.shape)
flat_outputs[name] = utils.get_flattened_view(outputs[name])

for message in responses:
if message.type == data.kOutput:
b = message.start
e = message.end + 1
if len(message.data) > 0:
flat_outputs[message.name][b:e] = message.data
else:
raise ValueError(
"Expected continuous variables, but array is empty."
)
variant = message.WhichOneof("payload")

if variant == "continuous":
arr = message.continuous
if arr.type == data.kOutput:
b = arr.start
e = arr.end + 1
if len(arr.data) > 0:
flat_outputs[arr.name][b:e] = arr.data
else:
raise ValueError(
"Expected continuous variables, but array is empty."
)

elif variant == "discrete":
dv = message.discrete
if dv.type == data.VariableType.kDiscreteOutput:
discrete_outputs[dv.name] = _value_to_python(dv.value)

if discrete_outputs:
return outputs, discrete_outputs
return outputs

def _recover_residuals(self, responses):
Expand All @@ -215,15 +277,19 @@ def _recover_residuals(self, responses):
flat_residuals[name] = utils.get_flattened_view(residuals[name])

for message in responses:
if message.type == data.kResidual:
b = message.start
e = message.end + 1
if len(message.data) > 0:
flat_residuals[message.name][b:e] = message.data
else:
raise ValueError(
"Expected continuous variables, but array is empty."
)
variant = message.WhichOneof("payload")

if variant == "continuous":
arr = message.continuous
if arr.type == data.kResidual:
b = arr.start
e = arr.end + 1
if len(arr.data) > 0:
flat_residuals[arr.name][b:e] = arr.data
else:
raise ValueError(
"Expected continuous variables, but array is empty."
)

return residuals

Expand Down Expand Up @@ -257,16 +323,20 @@ def _recover_partials(self, responses):
)

for message in responses:
b = message.start
e = message.end + 1

if message.type == data.kPartial:
if len(message.data) > 0:
flat_p[(message.name, message.subname)][b:e] = message.data
else:
raise ValueError(
"Expected continuous outputs for the "
"partials, but array was empty."
)
variant = message.WhichOneof("payload")

if variant == "continuous":
arr = message.continuous
b = arr.start
e = arr.end + 1

if arr.type == data.kPartial:
if len(arr.data) > 0:
flat_p[(arr.name, arr.subname)][b:e] = arr.data
else:
raise ValueError(
"Expected continuous outputs for the "
"partials, but array was empty."
)

return partials
Loading
Loading