1

I want an object with inputs and outputs, with all outputs calculated only when needed (some outputs may depend only on a partial set of inputs, etc.), as in a Labview VI. There can be cascading relationships between outputs, e.g. the result of the calculation of output_1 depends on the result of the calculation of output_0.

Let's consider this oversimplified example (where I am conscious that output_0 and output_1 could be calculated in a single procedure, but I aim to more complex applications of course):

from traits.api import HasTraits, Int, Property, cached_property

class A(HasTraits):
    input_0 = Int
    output_0 = Property(depends_on='input_0')
    output_1 = Property(depends_on='output_0')

    @cached_property
    def _get_output_0(self):
        print('_get_output_0')
        return self.input_0**2

    @cached_property
    def _get_output_1(self):
        print('_get_output_1')
        return self.output_0**2

a = A(input_0=3)

The problem here is that, as output_1 is declared to depend on output_0, the value of output_0 has to be calculated every time input_0 changes (which is understandable), i.e. we have '_get_output_0' printed when executing the above code.

It is possible to have a more consistent behavior using Events, as in:

from traits.api import HasTraits, Int, Property, cached_property, Event
from traitsui.api import View

class A(HasTraits):
    input_0 = Int
    output_0 = Property(depends_on='input_0')
    output_1 = Property(depends_on='output_0_changed')  # <--- depends on Event now ---
    output_0_changed = Event

    @cached_property
    def _get_output_0(self):
        print('_get_output_0')
        self.output_0_changed = True    #  <----- Event fired ----------
        return self.input_0**2

    @cached_property
    def _get_output_1(self):
        print('_get_output_1')
        return self.output_0**2

    traits_view = View('input_0', 'output_0', 'output_1')

a = A(input_0=3)

Hurray! The calculation of output_0 is no more fired. Asking for output_0 or output_1 in any order will have only the relevant calculations launched. So far, so good...

Unfortunately, issuing

a.configure_traits()

and changing the value of input_0 in the dialog box will get into an infinite recursion of alternative calculations of output_0 and output_1. The traits.api doc states about on_trait_change that If the current thread is the UI thread, the notifications are executed immediately. Here we are dealing with Property's, but it should be something similar, iow the output_0_changed notification is executed before the new value is set, hence the recursive calls between output_1 and output_0

So far, I did not find any solution to get rid of this recursion. Any idea?

Yves Surrel
  • 193
  • 1
  • 10

1 Answers1

0

A partial solution is to add an _output_0 trait and make output_1 depend on _output_0:

from traits.api import HasTraits, Int, Property, cached_property
from traitsui.api import View

class A(HasTraits):
    _output_0 = Int
    input_0 = Int
    output_0 = Property(depends_on='input_0')
    output_1 = Property(depends_on='_output_0')

    @cached_property
    def _get_output_0(self):
        print('_get_output_0')
        self._output_0 = self.input_0**2
        return self._output_0

    @cached_property
    def _get_output_1(self):
        print('_get_output_1')
        return self.output_0**2

    traits_view = View('input_0', 'output_0', 'output_1')

a = A(input_0=3)

However, if the configuration dialog is opened, changing the input make _get_output_0 been called twice, once before _get_output_1 and once after...

Yves Surrel
  • 193
  • 1
  • 10