Note
Go to the end to download the full example code.
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)