Skip to content
Open
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
24 changes: 24 additions & 0 deletions docs/api/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,27 @@ Fields and Elements
:member-order: bysource
:members:
:exclude-members: log

Assigned value dictionaries
---------------------------

A model instance's field values can be converted to a dictionary with
:meth:`condor.fields.FieldValues.dict_of`. By default, each key is the field
element's backend expression, so the result can be used for substitution::

import condor as co
from condor.backend import operators as ops

class Sum(co.ExplicitSystem):
x = input()
y = input()
output.total = x + y

result = Sum(x=2, y=3)
substitutions = result.input.dict_of()
evaluated = ops.substitute(Sum.total.backend_repr, substitutions)

``result.input.dict_of("name")`` instead returns ``{"x": 2, "y": 3}``.
The dictionary is new, but its values reference the assigned objects directly,
including arrays and symbolic expressions. Use the existing ``asdict()`` method
when an independent, recursively copied mapping is required.
27 changes: 27 additions & 0 deletions src/condor/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ def asdict(self):
datacalss"""
return dc.asdict(self)

def dict_of(self, attr="backend_repr"):
"""Return assigned values keyed by an attribute of each field element.

Parameters
----------
attr : str, optional
Element attribute to use as the key. The default, ``backend_repr``,
produces a mapping for :func:`condor.backend.operators.substitute`.
Use ``"name"`` for a name-keyed mapping. Keys must be hashable;
repeated keys follow the usual dictionary last-value-wins behavior.

Returns
-------
dict
A new dictionary in field-element order. Values are taken directly
from this instance, preserving array shapes and symbolic objects
without copying. Unlike :meth:`asdict`, this is a shallow mapping.

Raises
------
AttributeError
If an element does not have the requested attribute.
TypeError
If a key is not hashable.
"""
return {getattr(elem, attr): getattr(self, elem.name) for elem in self.field}

def flatten(self):
"""turn the bound values of this field instance into a single symbol -- may be
numeric or in the backend representation (symbol class)"""
Expand Down
92 changes: 92 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import casadi
import numpy as np
import pytest

import condor
from condor.backend import operators


def test_create_from():
Expand Down Expand Up @@ -53,3 +58,90 @@ class Sys(condor.ExplicitSystem):
sys = Sys(a=1, b=-2)
assert dict(**sys.input) == {"a": 1, "b": -2}
assert dict(**sys.output) == {"c": -1}


@pytest.fixture
def value_system():
class ValueSystem(condor.ExplicitSystem):
x = input()
vector = input(shape=(2, 1))
matrix = input(shape=(2, 2))

output.result = x + vector[0] + matrix[0, 0]

return ValueSystem


def test_field_values_dict_of_symbols(value_system):
instance = value_system(x=2, vector=np.array([3, 5]), matrix=np.eye(2))
values = instance.input.dict_of()

assert list(values) == value_system.input.list_of("backend_repr")
for element in value_system.input:
assert values[element.backend_repr] is getattr(instance.input, element.name)
assert len(values) == 3


def test_field_values_dict_of_names(value_system):
instance = value_system(x=2, vector=np.array([3, 5]), matrix=np.eye(2))
values = instance.input.dict_of("name")

assert list(values) == ["x", "vector", "matrix"]
assert values["x"] == instance.input.x
assert values["vector"] is instance.input.vector
assert values["matrix"] is instance.input.matrix
assert values["vector"].shape == instance.input.vector.shape
assert values["matrix"].shape == (2, 2)
# The existing conversion keeps its independent deep-copy semantics.
copied = instance.input.asdict()
assert copied["matrix"] is not instance.input.matrix
np.testing.assert_array_equal(copied["matrix"], values["matrix"])


def test_field_values_dict_of_symbolic_values(value_system):
symbolic = value_system.input.dataclass_of()
values = symbolic.dict_of()

for element in value_system.input:
assert values[element.backend_repr] is element.backend_repr


def test_field_values_dict_of_substitution(value_system):
instance = value_system(x=2, vector=np.array([3, 5]), matrix=np.eye(2))
expression = value_system.result.backend_repr
evaluated = operators.substitute(expression, instance.input.dict_of())

assert float(casadi.evalf(evaluated)) == pytest.approx(instance.result.item())


def test_field_values_dict_of_output(value_system):
instance = value_system(x=2, vector=np.array([3, 5]), matrix=np.eye(2))
result = instance.output.dict_of()

assert len(result) == 1
assert result[value_system.result.backend_repr] is instance.output.result


def test_field_values_dict_of_empty_field():
class Constant(condor.ExplicitSystem):
output.value = 1

instance = Constant()
assert instance.input.dict_of() == {}
assert instance.input.dict_of("name") == {}


def test_field_values_dict_of_invalid_attribute(value_system):
values = value_system.input.dataclass_of()
with pytest.raises(AttributeError, match="no_such_attribute"):
values.dict_of("no_such_attribute")


def test_field_values_dict_of_returns_new_mapping(value_system):
values = value_system.input.dataclass_of()
first = values.dict_of()
second = values.dict_of()
first.clear()

assert len(second) == 3
assert len(values.dict_of()) == 3