-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathren.py
More file actions
207 lines (166 loc) · 8.31 KB
/
Copy pathren.py
File metadata and controls
207 lines (166 loc) · 8.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import OrderedDict
class ContractiveREN(nn.Module):
"""
Acyclic contractive recurrent equilibrium network, following the paper:
"Recurrent equilibrium networks: Flexible dynamic models with guaranteed
stability and robustness, Revay M et al. ."
The mathematical model of RENs relies on an implicit layer embedded in a recurrent layer.
The model is described as,
[ E . x_t+1 ] = [ F B_1 B_2 ] [ x_t ] + [ b_x ]
[ Λ . v_t ] = [ C_1 D_11 D_12 ] [ w_t ] + [ b_w ]
[ y_t ] = [ C_2 D_21 D_22 ] [ u_t ] + [ b_u ]
where E is an invertible matrix and Λ is a positive-definite diagonal matrix. The model parameters
are then {E, Λ , F, B_i, C_i, D_ij, b} which form a convex set according to the paper.
NOTE: REN has input "u", output "y", and internal state "x". When used in closed-loop,
the REN input "u" would be the noise reconstruction ("w") and the REN output ("y")
would be the input to the plant. The internal state of the REN ("x") should not be mistaken
with the internal state of the plant.
"""
def __init__(
self, dim_in: int, dim_out: int, dim_internal: int,
dim_nl: int, internal_state_init = None, initialization_std: float = 0.5,
pos_def_tol: float = 0.001, contraction_rate_lb: float = 1.0
):
"""
Args:
dim_in (int): Input (u) dimension.
dim_out (int): Output (y) dimension.
dim_internal (int): Internal state (x) dimension. This state evolves with contraction properties.
dim_nl (int): Dimension of the input ("v") and ouput ("w") of the nonlinear static block.
initialization_std (float, optional): Weight initialization. Set to 0.1 by default.
internal_state_init (torch.Tensor or None, optional): Initial condition for the internal state. Defaults to 0 when set to None.
epsilon (float, optional): Positive and negligible scalar to force positive definite matrices.
contraction_rate_lb (float, optional): Lower bound on the contraction rate. Defaults to 1.
"""
super().__init__()
# set dimensions
self.dim_in = dim_in
self.dim_out = dim_out
self.dim_internal = dim_internal
self.dim_nl = dim_nl
# set functionalities
self.contraction_rate_lb = contraction_rate_lb
# auxiliary elements
self.epsilon = pos_def_tol
# initialize internal state
if internal_state_init is None:
self.register_buffer('x', torch.zeros(1, 1, dim_internal))
else:
self.register_buffer('x', internal_state_init.reshape(1, 1, dim_internal))
self.register_buffer('init_x', self.x.detach().clone())
# define matrices shapes
# auxiliary matrices
self.X_shape = (2 * self.dim_internal + self.dim_nl, 2 * self.dim_internal + self.dim_nl)
self.Y_shape = (self.dim_internal, self.dim_internal)
# nn state dynamics
self.B2_shape = (self.dim_internal, self.dim_in)
# nn output
self.C2_shape = (self.dim_out, self.dim_internal)
self.D21_shape = (self.dim_out, self.dim_nl)
self.D22_shape = (self.dim_out, self.dim_in)
# v signal
self.D12_shape = (self.dim_nl, self.dim_in)
# define trainable params
self.training_param_names = ['X', 'Y', 'B2', 'C2', 'D21', 'D22', 'D12']
self._init_trainable_params(initialization_std)
# mask
self.register_buffer('eye_mask_H', torch.eye(2 * self.dim_internal + self.dim_nl))
self.register_buffer('eye_mask_w', torch.eye(self.dim_nl))
# update non-trainable model params
self._update_model_param()
def _update_model_param(self):
"""
Update non-trainable matrices according to the REN formulation to preserve contraction.
"""
# dependent params
H = torch.matmul(self.X.T, self.X) + self.epsilon * self.eye_mask_H
h1, h2, h3 = torch.split(H, [self.dim_internal, self.dim_nl, self.dim_internal], dim=0)
H11, H12, H13 = torch.split(h1, [self.dim_internal, self.dim_nl, self.dim_internal], dim=1)
H21, H22, _ = torch.split(h2, [self.dim_internal, self.dim_nl, self.dim_internal], dim=1)
H31, H32, H33 = torch.split(h3, [self.dim_internal, self.dim_nl, self.dim_internal], dim=1)
P = H33
# nn state dynamics
self.F = H31
self.B1 = H32
# nn output
self.E = 0.5 * (H11 + self.contraction_rate_lb * P + self.Y - self.Y.T)
self.E_inv = self.E.inverse()
# v signal for strictly acyclic REN
self.Lambda = 0.5 * torch.diag(H22)
self.D11 = -torch.tril(H22, diagonal=-1)
self.C1 = -H21
def forward(self, u_in):
"""
Forward pass of REN.
Args:
u_in (torch.Tensor): Input with the size of (batch_size, 1, self.dim_in).
Return:
y_out (torch.Tensor): Output with (batch_size, 1, self.dim_out).
"""
batch_size = u_in.shape[0]
w = torch.zeros(batch_size, 1, self.dim_nl, device=u_in.device)
# update each row of w using Eq. (8) with a lower triangular D11
for i in range(self.dim_nl):
# v is element i of v with dim (batch_size, 1)
v = F.linear(self.x, self.C1[i, :]) + F.linear(w, self.D11[i, :]) + F.linear(u_in, self.D12[i,:])
w = w + (self.eye_mask_w[i, :] * torch.tanh(v / self.Lambda[i])).reshape(batch_size, 1, self.dim_nl)
# compute next state using Eq. 18
self.x = F.linear(F.linear(self.x, self.F) + F.linear(w, self.B1) + F.linear(u_in, self.B2), self.E_inv)
# compute output
y_out = F.linear(self.x, self.C2) + F.linear(w, self.D21) + F.linear(u_in, self.D22)
return y_out
def reset(self):
self.x = self.init_x # reset the REN state to the initial value
# init trainable params
def _init_trainable_params(self, initialization_std):
for training_param_name in self.training_param_names: # name of one of the training params, e.g., X
# read the defined shapes of the selected training param, e.g., X_shape
shape = getattr(self, training_param_name + '_shape')
# define the selected param (e.g., self.X) as nn.Parameter
setattr(self, training_param_name, nn.Parameter((torch.randn(*shape) * initialization_std)))
# setters and getters
def get_parameter_shapes(self):
param_dict = OrderedDict(
(name, getattr(self, name).shape) for name in self.training_param_names
)
return param_dict
def get_named_parameters(self):
param_dict = OrderedDict(
(name, getattr(self, name)) for name in self.training_param_names
)
return param_dict
def run(self, u_sequence):
"""
Runs the ContractiveREN model over a sequence of inputs.
Args:
u_sequence (torch.Tensor): batch of input sequences of shape (batch_size, horizon, dim_in).
Returns:
y_sequence (torch.Tensor): batch of output sequences of shape (batch_size, horizon, dim_out).
"""
# 1. Reset the internal state of the model
self.reset()
# Update the REN parameters ONCE before the trajectory starts.
self._update_model_param()
batch_size, horizon, dim_in = u_sequence.shape
y_list = []
# 2. Iterate over the time horizon
for t in range(horizon):
# Extract input at time t, keeping dimensions (batch_size, 1, dim_in)
u_t = u_sequence[:, t:t + 1, :]
# Forward pass updates the internal state 'model.x' automatically
y_t = self.forward(u_t)
y_list.append(y_t)
# 3. Concatenate outputs along the time dimension
y_sequence = torch.cat(y_list, dim=1)
return y_sequence
def __call__(self, u_sequence):
"""
Args:
u_sequence (torch.Tensor): batch of input sequences of shape (batch_size, horizon, dim_in).
Returns:
y_sequence (torch.Tensor): batch of output sequences of shape (batch_size, horizon, dim_out).
"""
return self.run(u_sequence)