From 0357c8972a6531461ceae37430dab0dc52192452 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:45:30 +0100 Subject: [PATCH] Map assigned field values by element attributes Signed-off-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> --- docs/api/fields.rst | 24 ++++++++++++ src/condor/fields.py | 27 +++++++++++++ tests/test_fields.py | 92 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/docs/api/fields.rst b/docs/api/fields.rst index 73ccbb39..816dcf54 100644 --- a/docs/api/fields.rst +++ b/docs/api/fields.rst @@ -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. diff --git a/src/condor/fields.py b/src/condor/fields.py index 67c00d3b..ab85b5c5 100644 --- a/src/condor/fields.py +++ b/src/condor/fields.py @@ -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)""" diff --git a/tests/test_fields.py b/tests/test_fields.py index 3610c4b1..acba0500 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,4 +1,9 @@ +import casadi +import numpy as np +import pytest + import condor +from condor.backend import operators def test_create_from(): @@ -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