Custom: Module with two outputs

The creation of a module with two outputs is demonstrated

Some modules may need to return two outputs (for instance, eigenvalues and eigenvectors, or absolute value and angle). Implementation of a module that returns two values requires some special handling for the sensitivity function.

  9 import pymoto as pym
 10
 11
 12 class TwoOutputs(pym.Module):
 13     """ This module has two inputs and two outputs """
 14     def __call__(self, x1, x2):
 15         print(f'[{type(self).__name__}]Do my response calculation')
 16         # Store data, which might be needed for the sensitivity calculation
 17         self.x1 = x1
 18         self.x2 = x2
 19
 20         # Calculate two response values
 21         y1 = x1 * x2
 22         y2 = x1 + x2
 23
 24         # Return the results
 25         return y1, y2
 26
 27     def _sensitivity(self, df_dy1, df_dy2):
 28         """ This function calculate the (backward) sensitivity.
 29         It should handle None (zero sensitivity) as incoming adjoint variable. If both are None, the sensitivity
 30         will not be called.
 31         """
 32         print(f'[{type(self).__name__}]Do my sensitivity calculation')
 33
 34         # Calculate the gradients with chain-rule
 35         # First initialize sensitivities with the correct size containing all zeros
 36         df_dx1 = self.x1 * 0  # The sensitivity df/dx1 is the same size as x1 (in case of a vector/matrix)
 37         df_dx2 = self.x2 * 0
 38
 39         # In case the data of x1 and x2 were not stored, it could still be obtained here by directly accessing the state
 40         # of the input signals.
 41         also_x1 = self.sig_in[0].state
 42         assert also_x1 == self.x1
 43         also_x2 = self.sig_in[1].state
 44         assert also_x2 == self.x2
 45
 46         # If the sensitivity of the output signal is empty, it is None. So we only need to do calculations whenever it
 47         # is not None. In case both sensitivities of the output signals are None, this function won't be called.
 48         if df_dy1 is not None:
 49             df_dx1 += df_dy1*self.x2
 50             df_dx2 += df_dy1*self.x1
 51
 52         if df_dy2 is not None:
 53             df_dx1 += df_dy2
 54             df_dx2 += df_dy2
 55
 56         # Return the results
 57         return df_dx1, df_dx2
 58
 59
 60 if __name__ == "__main__":
 61     print(__doc__)
 62     print("_" * 80)
 63     print("-- Module setup")
 64
 65     # Create signals for the inputs. The argument is the 'tag' of the signal, which is optional.
 66     # The tag of the signal can be seen as its name, which can be useful for printing and debugging
 67     x1 = pym.Signal("x1", 2.0)
 68
 69     # Also create a second input signal (as our module has two inputs)
 70     x2 = pym.Signal("x2", 3.0)
 71
 72     print(f"\nState initialized to {x1.tag} = {x1.state}, {x2.tag} = {x2.state}")
 73
 74     # The module is instantiated using the constructor. In this case there is not initialization defined, so no
 75     # arguments are passed.
 76     print("Create Module:")
 77     my_module = TwoOutputs()  # Module with two outputs
 78
 79     print("\n-- Connect module and run forward analysis:")
 80     y1, y2 = my_module(x1, x2)
 81     y1.tag, y2.tag = 'y1', 'y2'  # Set a name for the output signals
 82
 83     # The state of the output signal can be accessed using `state` again
 84     print(f"The result: {y1.tag} = {y1.state}, {y2.tag} = {y2.state}")
 85
 86     print("\n-- Sensitivity analysis by back-propagation")
 87     # Calculate sensitivities
 88     print("\nSeed dy1/dy1 = 1.0, so we can calculate dy1/dx1 and dy1/dx2")
 89     # An initial 'seed' sensitivity of the response you're interested in needs to be set. We can do this by setting
 90     # the `sensitivity` property
 91     y1.sensitivity = 1.0
 92     my_module.sensitivity()
 93     # The sensitivities of the input signals can now be accessed by <Signal>.sensitivity
 94     print(f"dy1/d{x1.tag} = {x1.sensitivity}")
 95     print(f"dy1/d{x2.tag} = {x2.sensitivity}")
 96
 97     # If we also want to calculate the sensitivities for the second output, we first need to reset the sensitivities
 98     print("\nReset sensitivities")
 99     my_module.reset()
100     assert y1.sensitivity is None  # The sensitivity of y1 is now cleared; also those of x1 and x2
101
102     print("\nSeed dy2/dy2 = 1.0, so we can calculate dy2/dx1 and dy2/dx2")
103     y2.sensitivity = 1.0
104     my_module.sensitivity()
105     print(f"dy2/d{x1.tag} = {x1.sensitivity}")
106     print(f"dy2/d{x2.tag} = {x2.sensitivity}")
107
108     # You can always check your module with finite differencing
109     pym.finite_difference([x1, x2], [y1, y2], random=False)

Gallery generated by Sphinx-Gallery