Skip to content

Commit ef83646

Browse files
committed
P2: vectorise _solve_w, fix CPU/GPU portability, translate FR→EN
Performance - rens.py: pre-compute base = x@C1.T + u@D12.T + bv outside the per-neuron loop; divide by lam_diag once; drop w.clone() and v tensor - renode.py: same pattern – pre-compute base = x@C1.T + u@D12.T + bv, remove w.clone(), drop unused v tensor CPU/GPU device portability - implicit.py: torch.eye(2*nx) → device=self.X.device in both forward paths; torch.Tensor(torch.randn(...)) → torch.randn(...) - hinf.py (linear): self.Ix = torch.eye(nx) → register_buffer so .to('cuda') moves it automatically - lipschitz.py: all torch.eye/zeros in forward() now carry device=self.A.device - lyap.py: torch.eye(A.size(0)) → device=A.device in both Lyapunov classes - regularizations.py: torch.eye(mat.shape[0]) → device=mat.device - integrators.py: remove mutable default args torch.zeros(1)/torch.Tensor(1); default to None and lazily create zero tensor from u_batch device/dtype; applies to RK4Simulator, RK45Simulator and Sim_discrete Housekeeping - .gitignore: exclude docs/_build/, docs/_static/, docs/_sources/, docs/*.html - translate all remaining French comments/docstrings to English across utils/data.py, utils/misc.py, preprocessing/scalers.py, linalg/utils.py, integrators/integrators.py, train/train.py, linearization/feedbacklin.py, layers/dd.py, tests/models/linearization/test_flnssm.py https://claude.ai/code/session_017mH337zq1P8SP5zCakWnn8
1 parent 4874880 commit ef83646

17 files changed

Lines changed: 162 additions & 153 deletions

File tree

‎.gitignore‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,12 @@ build/
3131
/data/tiny-imagenet-200/
3232

3333
# Ignore saved pytorch models
34-
*.pt
34+
*.pt
35+
36+
# Ignore compiled documentation
37+
docs/_build/
38+
docs/_static/
39+
docs/_sources/
40+
docs/*.html
41+
docs/*.js
42+
docs/*.inv

‎ctrlnmod/integrators/integrators.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ def __repr__(self):
8787
def __str__(self) -> str:
8888
return f"RK4_{str(self.ss_model)}"
8989

90-
def forward(self, u_batch, x0_batch=torch.zeros(1), d_batch=None):
90+
def forward(self, u_batch, x0_batch=None, d_batch=None):
9191
X_sim_list = []
9292
Y_sim_list = []
93-
x_step = x0_batch
93+
x_step = x0_batch if x0_batch is not None else torch.zeros(
94+
u_batch.shape[0], self.ss_model.nx, device=u_batch.device, dtype=u_batch.dtype)
9495

9596
with P.cached(), self.ss_model._frame_cache.cache_frame():
9697
for i, u_step in enumerate(u_batch.split(1, dim=1)):
@@ -143,17 +144,18 @@ def __repr__(self):
143144
def __str__(self) -> str:
144145
return f"RK45_{str(self.ss_model)}"
145146

146-
def forward(self, u_batch, x0_batch=torch.zeros(1), d_batch=None):
147+
def forward(self, u_batch, x0_batch=None, d_batch=None):
147148
X_sim_list = []
148149
Y_sim_list = []
149-
x_step = x0_batch
150+
x_step = x0_batch if x0_batch is not None else torch.zeros(
151+
u_batch.shape[0], self.ss_model.nx, device=u_batch.device, dtype=u_batch.dtype)
150152

151153
with P.cached(), self.ss_model._frame_cache.cache_frame():
152154
for i, u_step in enumerate(u_batch.split(1, dim=1)):
153155
u_step = u_step.squeeze(1)
154156
d_step = d_batch[:, i, :] if d_batch is not None else None
155157
X_sim_list.append(x_step)
156-
158+
157159
dt = self.ts
158160
k1, y_step = self.ss_model(u_step, x_step, d_step)
159161
Y_sim_list.append(y_step)
@@ -164,7 +166,7 @@ def forward(self, u_batch, x0_batch=torch.zeros(1), d_batch=None):
164166
k5, _ = self.ss_model(u_step, x_step + dt * (439*k1/216 - 8*k2 + 3680*k3/513 - 845*k4/4104), d_step)
165167
k6, _ = self.ss_model(u_step, x_step + dt * (-8*k1/27 + 2*k2 - 3544*k3/2565 + 1859*k4/4104 - 11*k5/40), d_step)
166168

167-
# Solution d'ordre 5
169+
# 5th-order solution
168170
dx = dt * (16*k1/135 + 6656*k3/12825 + 28561*k4/56430 - 9*k5/50 + 2*k6/55)
169171
x_step = x_step + dx
170172

@@ -205,10 +207,11 @@ def clone(self):
205207
copy.load_state_dict(self.state_dict())
206208
return copy
207209

208-
def forward(self, u_batch, x0_batch=torch.Tensor(1)):
210+
def forward(self, u_batch, x0_batch=None):
209211
X_sim_list = []
210212
Y_sim_list = []
211-
x_step = x0_batch
213+
x_step = x0_batch if x0_batch is not None else torch.zeros(
214+
u_batch.shape[0], self.ss_model.nx, device=u_batch.device, dtype=u_batch.dtype)
212215
for u_step in u_batch.split(1, dim=1):
213216
u_step = u_step.squeeze(1)
214217
X_sim_list += [x_step]

‎ctrlnmod/layers/dd.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def forward(self, M):
2121
dQ = torch.diag(Q)
2222
delta_Q = self.act(torch.sum(torch.abs(Q), dim=1) - dQ - torch.abs(dQ))
2323

24-
DQ = torch.diag(delta_Q) # (Une) distance à l'ensemble DD+
24+
DQ = torch.diag(delta_Q) # A distance to the DD+ set
2525

2626
return DQ
2727

‎ctrlnmod/linalg/utils.py‎

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def schur(matrix, dim_A, dim_B, dim_C):
135135
expected_rows = dim_A + dim_C
136136
expected_cols = dim_A + dim_B
137137
if matrix.shape[0] < expected_rows or matrix.shape[1] < expected_cols:
138-
raise ValueError("Les dimensions fournies sont incompatibles avec la taille de la matrice.")
138+
raise ValueError("Provided dimensions are incompatible with the matrix size.")
139139

140140
# Blocks extraction
141141
A = matrix[:dim_A, :dim_A]
@@ -479,23 +479,23 @@ def solve_riccati_torch(A: torch.Tensor,
479479
H[:nx, nx:] = H_12
480480
H[nx:, :nx] = H_21
481481

482-
# Calcul des valeurs et vecteurs propres
483-
# Note: torch.linalg.eig retourne les valeurs complexes même pour matrices réelles
482+
# Compute eigenvalues and eigenvectors
483+
# Note: torch.linalg.eig returns complex values even for real matrices
484484
eigvals, eigvecs = torch.linalg.eig(H)
485-
486-
# Conversion en valeurs réelles pour le tri
485+
486+
# Extract real parts for sorting
487487
real_parts = eigvals.real
488-
489-
# Tri des valeurs propres par partie réelle
488+
489+
# Sort eigenvalues by real part (ascending)
490490
sorted_indices = torch.argsort(real_parts)
491491
eigvals = eigvals[sorted_indices]
492492
eigvecs = eigvecs[:, sorted_indices]
493-
494-
# Vérification du nombre de valeurs propres stables
493+
494+
# Check number of stable eigenvalues
495495
n_stable = torch.sum(real_parts[sorted_indices] < -tol).item()
496-
496+
497497
if n_stable != nx:
498-
# Ajuster le seuil si nécessaire
498+
# Adjust threshold if needed
499499
alternative_tol = abs(real_parts[nx-1].item()) * 10
500500
logger.debug(f"Adjusting tolerance from {tol} to {alternative_tol}")
501501
stable_indices = real_parts < alternative_tol
@@ -516,39 +516,39 @@ def solve_riccati_torch(A: torch.Tensor,
516516
X = stable_eigvecs[:nx, :]
517517
Y = stable_eigvecs[nx:, :]
518518

519-
# Vérifier le conditionnement de X
519+
# Check conditioning of X
520520
try:
521521
cond_X = torch.linalg.cond(X).item()
522522
except:
523523
cond_X = float('inf')
524-
525-
if cond_X > 1e12: # seuil arbitraire
524+
525+
if cond_X > 1e12: # arbitrary threshold
526526
logger.warning(f"Warning: X is poorly conditioned (cond = {cond_X:.2e})")
527-
527+
528528
try:
529-
# Utiliser la pseudo-inverse si X est mal conditionné
529+
# Use pseudoinverse when X is ill-conditioned
530530
if cond_X > 1e12:
531-
# Calcul manuel de la pseudo-inverse pour plus de contrôle
531+
# Manual pseudoinverse via SVD for numerical control
532532
U, S, Vh = torch.linalg.svd(X)
533533
S_pinv = torch.where(S > tol * S[0], 1/S, torch.zeros_like(S))
534534
X_pinv = (Vh.T.conj() * S_pinv.unsqueeze(0)) @ U.T.conj()
535535
P = Y @ X_pinv
536536
else:
537537
P = Y @ torch.linalg.inv(X)
538538

539-
# Prendre la partie réelle et symétriser
539+
# Take real part and symmetrise
540540
P = P.real
541541
P = (P + P.T)/2
542-
543-
# Vérifier que P est définie positive
542+
543+
# Verify that P is positive definite
544544
try:
545545
L = torch.linalg.cholesky(P)
546546
is_positive = True
547547
except:
548548
is_positive = False
549549
logger.warning("Warning: P is not positive definite")
550550

551-
# Calculer le résidu
551+
# Compute the Riccati residual
552552
riccati_residual = (A.T @ P + P @ A +
553553
gamma_sq_inv * P @ B @ B.T @ P +
554554
C.T @ C)

‎ctrlnmod/lmis/lipschitz.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,20 @@ def forward(self) -> Tuple[Tensor, ...]:
100100
# Making Lambda diagonal
101101
Lambda = torch.diag(self.Lambda_vec)
102102

103-
M11 = self.lip**2 * torch.eye(self.n_in)
103+
dev = self.A.device
104+
M11 = self.lip**2 * torch.eye(self.n_in, device=dev)
104105
M12 = - self.B.T @ Lambda
105-
M13 = torch.zeros((self.n_in, self.n_out))
106+
M13 = torch.zeros((self.n_in, self.n_out), device=dev)
106107
M22 = 2 * self.beta * Lambda - Lambda @ self.A - self.A.T @ Lambda
107108
M23 = - self.C.T
108-
M33 = torch.eye(self.n_out)
109+
M33 = torch.eye(self.n_out, device=dev)
109110

110111
M1 = torch.cat([M11, M12, M13], dim = 1)
111112
M2 = torch.cat([M12.T, M22, M23], dim = 1)
112113
M3 = torch.cat([M13.T, M23.T, M33], dim = 1)
113114

114115
M = torch.cat([M1, M2, M3], dim = 0)
115-
return M + self.epsilon*torch.eye(M.shape[0]), Lambda
116+
return M + self.epsilon*torch.eye(M.shape[0], device=dev), Lambda
116117

117118

118119
@classmethod

‎ctrlnmod/lmis/lyap.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, A: Tensor, alpha: float) -> None:
2727
super().__init__()
2828
self.A = Parameter(A)
2929
self.alpha = alpha
30-
self.P = Parameter(torch.eye(A.size(0)))
30+
self.P = Parameter(torch.eye(A.size(0), device=A.device))
3131

3232
def forward(self) -> Tensor:
3333
"""
@@ -93,7 +93,7 @@ def __init__(self, A: Tensor, alpha: float) -> None:
9393
super().__init__()
9494
self.A = Parameter(A)
9595
self.alpha = alpha
96-
self.P = Parameter(torch.eye(A.size(0)))
96+
self.P = Parameter(torch.eye(A.size(0), device=A.device))
9797

9898
def _symP(self):
9999
return 0.5 * (self.P + self.P.T)

‎ctrlnmod/models/ssmodels/continuous/general/renode.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ def forward(self, u: torch.Tensor, x: torch.Tensor) -> Tuple[torch.Tensor, torch
8888
y = x @ C2.T + w @ D21.T + self.by.T
8989
return dx, y
9090

91-
def _solve_w(self, x: torch.Tensor, u: torch.Tensor, C1: torch.Tensor,
91+
def _solve_w(self, x: torch.Tensor, u: torch.Tensor, C1: torch.Tensor,
9292
D11: torch.Tensor, D12: torch.Tensor) -> torch.Tensor:
9393
nb = x.shape[0]
94-
w = torch.zeros(nb, self.nq, device=self.device)
95-
v = torch.zeros(nb, self.nq, device=self.device)
96-
94+
# Pre-compute input contributions once via batch matmuls
95+
base = x @ C1.T + u @ D12.T + self.bv.squeeze(-1) # (nb, nq)
96+
w = torch.zeros(nb, self.nq, device=x.device)
9797
for k in range(self.nq):
98-
v[:, k] = x @ C1[k, :] + w.clone() @ D11[k, :] + u @ D12[k, :] + self.bv[k]
99-
w[:, k] = self.act(v[:, k])
98+
v_k = base[:, k] + w @ D11[k, :]
99+
w[:, k] = self.act(v_k)
100100
return w
101101

102102
def check(self) -> Tuple[bool, dict]:

‎ctrlnmod/models/ssmodels/continuous/general/rens.py‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,28 @@ def forward(self, x: torch.Tensor, u: torch.Tensor) -> tuple[torch.Tensor, torch
9797

9898
def _solve_w(self, x: torch.Tensor, u: torch.Tensor) -> torch.Tensor:
9999
"""
100-
Solve for the internal non-linear state w in the acyclic case
100+
Solve for the internal non-linear state w in the acyclic case.
101+
102+
The recurrence is strictly lower-triangular (D11 is strictly lower-triangular
103+
after the constrained parametrisation), so each v_k depends only on w[:k].
104+
We pre-compute the input contributions once via batch matmuls and amortise
105+
the per-neuron cost to a single row-vector dot product.
101106
102107
Args:
103-
x (torch.Tensor): Current state.
104-
u (torch.Tensor): Input.
108+
x (torch.Tensor): Current state, shape (batch, nx).
109+
u (torch.Tensor): Input, shape (batch, nu).
105110
106111
Returns:
107-
torch.Tensor: Solved internal non-linear state.
112+
torch.Tensor: Solved internal non-linear state, shape (batch, nq).
108113
"""
109114
nb = x.shape[0]
110-
w = torch.zeros(nb, self.nq, device=self.device)
111-
v = torch.zeros(nb, self.nq, device=self.device)
115+
lam_diag = torch.diagonal(self.Lambda) # (nq,)
116+
# Pre-compute contributions that are constant w.r.t. w (one matmul each)
117+
base = (x @ self.C1.T + u @ self.D12.T + self.bv.squeeze(-1)) / lam_diag # (nb, nq)
118+
w = torch.zeros(nb, self.nq, device=x.device)
112119
for k in range(self.nq):
113-
v[:, k] = (1 / self.Lambda[k, k]) * (x @ self.C1[k, :] +
114-
w.clone() @ self.D11[k, :] + u @ self.D12[k, :] + self.bv[k])
115-
w[:, k] = self.act(v[:, k].clone())
120+
v_k = base[:, k] + (w @ self.D11[k, :]) / lam_diag[k]
121+
w[:, k] = self.act(v_k)
116122
return w
117123

118124

‎ctrlnmod/models/ssmodels/continuous/linear/hinf.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(self, nu: int, ny: int, nx: int, gamma: float, alpha: float = 0.0,
4747

4848

4949
self.gamma = gamma
50-
self.Ix = torch.eye(nx)
50+
self.register_buffer('Ix', torch.eye(nx))
5151
self.eps = epsilon
5252
self.param = param
5353
self.alpha = alpha
@@ -305,7 +305,7 @@ def __init__(self, nu: int, ny: int, nx: int, nd: int, gamma: float, alpha: floa
305305

306306

307307
self.gamma = gamma
308-
self.Ix = torch.eye(nx)
308+
self.register_buffer('Ix', torch.eye(nx))
309309
self.eps = epsilon
310310
self.param = param
311311
self.alpha = alpha

‎ctrlnmod/models/ssmodels/continuous/linear/implicit.py‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def __init__(self, input_dim: int, output_dim: int, nx, strSavpath=os.getcwd())
2121
self.nx = nx
2222
self.ny = output_dim
2323

24-
self.E = Parameter(torch.Tensor(torch.randn(self.nx, self.nx)))
24+
self.E = Parameter(torch.randn(self.nx, self.nx))
2525
self.F = nn.Linear(self.nx, self.nx, bias=False)
2626
self.B = nn.Linear(self.nu, self.nx, bias=False)
2727
self.C = nn.Linear(self.nx, self.ny, bias=False)
@@ -72,17 +72,16 @@ def __init__(self, input_dim: int, output_dim: int, nx, strSavpath=os.getcwd())
7272
self.nx = nx
7373
self.ny = output_dim
7474

75-
self.X = Parameter(torch.Tensor(torch.randn(2 * self.nx, 2 * self.nx)))
76-
self.Y = Parameter(torch.Tensor(torch.randn(self.nx, self.nx)))
75+
self.X = Parameter(torch.randn(2 * self.nx, 2 * self.nx))
76+
self.Y = Parameter(torch.randn(self.nx, self.nx))
7777
self.epsilon = 1e-4
7878
self.B = nn.Linear(self.nu, self.nx, bias=False)
7979
self.C = nn.Linear(self.nx, self.ny, bias=False)
80-
# self.config = config
8180
self.str_savepath = strSavpath
8281

8382
def forward(self, u, x):
8483
nx = self.nx
85-
H = self.X @ self.X.T + self.epsilon * torch.eye(2 * self.nx)
84+
H = self.X @ self.X.T + self.epsilon * torch.eye(2 * self.nx, device=self.X.device)
8685
H11 = H[:nx, :nx]
8786
P = H[nx:, nx:]
8887
F = H[nx:, :nx]
@@ -135,17 +134,16 @@ def __init__(self, input_dim: int, output_dim: int, nx, alpha: float = 1.0, strS
135134
self.ny = output_dim
136135
self.alpha = alpha
137136

138-
self.X = Parameter(torch.Tensor(torch.randn(2 * self.nx, 2 * self.nx)))
139-
self.Y = Parameter(torch.Tensor(torch.randn(self.nx, self.nx)))
137+
self.X = Parameter(torch.randn(2 * self.nx, 2 * self.nx))
138+
self.Y = Parameter(torch.randn(self.nx, self.nx))
140139
self.epsilon = 1e-4
141140
self.B = nn.Linear(self.nu, self.nx, bias=False)
142141
self.C = nn.Linear(self.nx, self.ny, bias=False)
143-
# self.config = config
144142
self.str_savepath = strSavpath
145143

146144
def forward(self, u, x):
147145
nx = self.nx
148-
H = self.X @ self.X.T + self.epsilon * torch.eye(2 * self.nx)
146+
H = self.X @ self.X.T + self.epsilon * torch.eye(2 * self.nx, device=self.X.device)
149147
H11 = H[:nx, :nx]
150148
P = H[nx:, nx:]
151149
F = H[nx:, :nx]

0 commit comments

Comments
 (0)