Skip to content

Commit ce736ad

Browse files
GiovanniCanalindem0
authored andcommitted
separate residuals and losses logic
1 parent 1a0688e commit ce736ad

15 files changed

Lines changed: 122 additions & 88 deletions

pina/_src/condition/condition_interface.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def create_dataloader(
5757
"""
5858

5959
@abstractmethod
60-
def evaluate(self, batch, solver, loss):
60+
def evaluate(self, batch, solver):
6161
"""
6262
Evaluate the residual of the condition on the given batch using the
6363
solver.
@@ -75,8 +75,6 @@ def evaluate(self, batch, solver, loss):
7575
pass and compute the residual. The solver provides access to the
7676
model and its parameters, which may be necessary for evaluating the
7777
condition residual.
78-
:param torch.nn.Module loss: The non-aggregating loss function used to
79-
compare the condition residual against its reference value.
8078
:return: The non-aggregated residual tensor.
8179
:rtype: torch.Tensor | LabelTensor
8280
"""

pina/_src/condition/data_condition.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def store_data(self, **kwargs):
8585

8686
return _DataManager(**data_dict)
8787

88-
def evaluate(self, batch, solver, loss):
88+
def evaluate(self, batch, solver):
8989
"""
9090
Evaluate the residual of the condition on the given batch using the
9191
solver.
@@ -103,13 +103,10 @@ def evaluate(self, batch, solver, loss):
103103
pass and compute the residual. The solver provides access to the
104104
model and its parameters, which may be necessary for evaluating the
105105
condition residual.
106-
:param torch.nn.Module loss: The non-aggregating loss function used to
107-
compare the condition residual against its reference value.
108106
:return: The non-aggregated residual tensor.
109107
:rtype: torch.Tensor | LabelTensor
110108
"""
111-
output_ = solver.forward(batch["input"])
112-
return loss(output_, torch.zeros_like(output_))
109+
return solver.forward(batch["input"])
113110

114111
@property
115112
def conditional_variables(self):

pina/_src/condition/domain_equation_condition.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def store_data(self, **kwargs):
7575
setattr(self, "domain", kwargs.get("domain"))
7676
setattr(self, "equation", kwargs.get("equation"))
7777

78-
def evaluate(self, batch, solver, loss):
78+
def evaluate(self, batch, solver):
7979
"""
8080
Evaluate the residual of the condition on the given batch using the
8181
solver.
@@ -93,8 +93,6 @@ def evaluate(self, batch, solver, loss):
9393
pass and compute the residual. The solver provides access to the
9494
model and its parameters, which may be necessary for evaluating the
9595
condition residual.
96-
:param torch.nn.Module loss: The non-aggregating loss function used to
97-
compare the condition residual against its reference value.
9896
:raises NotImplementedError: Always raised since any domain-equation
9997
condition is transformed into an input-equation condition before
10098
evaluation, and the residual is computed using the input-equation

pina/_src/condition/input_equation_condition.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def equation(self, value):
109109
check_consistency(value, self._avail_equation_cls)
110110
self._equation = value
111111

112-
def evaluate(self, batch, solver, loss):
112+
def evaluate(self, batch, solver):
113113
"""
114114
Evaluate the residual of the condition on the given batch using the
115115
solver.
@@ -127,15 +127,11 @@ def evaluate(self, batch, solver, loss):
127127
pass and compute the residual. The solver provides access to the
128128
model and its parameters, which may be necessary for evaluating the
129129
condition residual.
130-
:param torch.nn.Module loss: The non-aggregating loss function used to
131-
compare the condition residual against its reference value.
132130
:return: The non-aggregated residual tensor.
133131
:rtype: LabelTensor
134132
"""
135133
# Compute residuals
136134
samples = batch["input"].requires_grad_(True)
137-
residual = self.equation.residual(
135+
return self.equation.residual(
138136
samples, solver.forward(samples), solver._params
139137
)
140-
141-
return loss(residual, torch.zeros_like(residual))

pina/_src/condition/input_target_condition.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def store_data(self, **kwargs):
8383
"""
8484
return _DataManager(**kwargs)
8585

86-
def evaluate(self, batch, solver, loss):
86+
def evaluate(self, batch, solver):
8787
"""
8888
Evaluate the residual of the condition on the given batch using the
8989
solver.
@@ -101,12 +101,10 @@ def evaluate(self, batch, solver, loss):
101101
pass and compute the residual. The solver provides access to the
102102
model and its parameters, which may be necessary for evaluating the
103103
condition residual.
104-
:param torch.nn.Module loss: The non-aggregating loss function used to
105-
compare the condition residual against its reference value.
106104
:return: The non-aggregated residual tensor.
107105
:rtype: torch.Tensor | LabelTensor
108106
"""
109-
return loss(solver.forward(batch["input"]), batch["target"])
107+
return solver.forward(batch["input"]) - batch["target"]
110108

111109
@property
112110
def input(self):

pina/_src/condition/time_series_condition.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,32 +170,32 @@ def _unroll(self, data, n_windows, unroll_length, randomize):
170170

171171
return torch.stack(windows, dim=1)
172172

173-
def evaluate(self, batch, solver, loss):
173+
def evaluate(self, batch, solver):
174174
"""
175175
Evaluate the residual of the condition on the given batch using the
176176
solver.
177177
178-
This method computes the non-aggregated, element-wise residual of the
179-
condition. A forward pass of the solver's model is performed on the
180-
input samples, and the condition residual is evaluated accordingly.
178+
This method computes the per-step residuals through autoregressive
179+
unrolling. A forward pass of the solver's model is performed at each
180+
time step, and the per-step residuals (predicted - target) are
181+
returned as a stacked tensor.
181182
182-
The returned tensor is not reduced, preserving the per-sample residual
183-
values.
183+
The returned tensor preserves all per-step residual values without
184+
reduction or loss aggregation.
184185
185186
:param dict batch: The batch containing the data required by the
186187
condition evaluation.
187188
:param SolverInterface solver: The solver used to perform the forward
188189
pass and compute the residual. The solver provides access to the
189190
model and its parameters, which may be necessary for evaluating the
190191
condition residual.
191-
:param torch.nn.Module loss: The non-aggregating loss function used to
192-
compare the condition residual against its reference value.
193192
:raises ValueError: If the input tensor in the batch has less than 4
194193
dimensions.
195-
:return: The non-aggregated residual tensor.
194+
:return: The stacked per-step residual tensor of shape
195+
[time_steps - 1, trajectories, windows, *features].
196196
:rtype: torch.Tensor | LabelTensor
197197
"""
198-
# Raise error if input tensor does not have at least4 dimensions
198+
# Raise error if input tensor does not have at least 4 dimensions
199199
if batch["input"].dim() < 4:
200200
raise ValueError(
201201
"The provided input tensor must have at least 4 dimensions:"
@@ -206,9 +206,9 @@ def evaluate(self, batch, solver, loss):
206206
# Copy the kwargs to avoid modifying the original settings
207207
kwargs = solver._kwargs.copy()
208208

209-
# Extract the initial state and initialize the list of step-wise losses
209+
# Extract the initial state and initialize the step-wise residuals list
210210
current_state = batch["input"][:, :, 0]
211-
losses = []
211+
residuals = []
212212

213213
# Iterate over the time steps
214214
for step in range(1, batch["input"].shape[2]):
@@ -218,23 +218,16 @@ def evaluate(self, batch, solver, loss):
218218
output = solver.forward(processed_input)
219219
predicted_state = solver.postprocess_step(output, **kwargs)
220220

221-
# Retrieve the target and compute the step-wise loss
221+
# Retrieve the target and compute the step-wise residual
222222
target_state = batch["input"][:, :, step]
223-
step_loss = loss(predicted_state, target_state, **kwargs)
224-
losses.append(step_loss)
223+
step_residual = predicted_state - target_state
224+
residuals.append(step_residual)
225225

226226
# Update the current state for the next iteration
227227
current_state = predicted_state
228228

229-
# Stack the step-wise losses
230-
step_losses = torch.stack(losses).as_subclass(torch.Tensor)
231-
232-
# Compute adaptive weights and aggregate the step-wise losses
233-
with torch.no_grad():
234-
name = getattr(self, "name", None) or "default"
235-
weights = solver._get_weights(name, step_losses)
236-
237-
return solver.aggregation_strategy(step_losses * weights)
229+
# Stack the step-wise residuals
230+
return torch.stack(residuals).as_subclass(torch.Tensor)
238231

239232
@property
240233
def input(self):

pina/_src/solver/autoregressive_solver.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,45 @@ def postprocess_step(self, predicted_state, **kwargs):
216216
"""
217217
return predicted_state
218218

219+
def optimization_cycle(self, batch):
220+
"""
221+
Compute one reduced, aggregated loss per condition in the batch.
222+
223+
For TimeSeriesCondition, this method evaluates the condition to obtain
224+
per-step residuals, applies the pointwise loss function to each step,
225+
computes adaptive weights based on the step-wise losses, and returns
226+
the aggregated weighted loss.
227+
228+
:param list[tuple[str, dict]] batch: A batch of data. Each element is a
229+
tuple containing a condition name and a dictionary of points.
230+
:return: The reduced, aggregated losses for all conditions.
231+
:rtype: dict[str, torch.Tensor]
232+
"""
233+
condition_losses = {}
234+
235+
for condition_name, data in batch:
236+
condition = self.problem.conditions[condition_name]
237+
condition_data = dict(data)
238+
239+
# Evaluate condition to get per-step residuals
240+
self.step_residuals = condition.evaluate(condition_data, self)
241+
242+
# Apply the loss function to each step-wise residual
243+
step_losses = self._loss_fn(
244+
self.step_residuals, torch.zeros_like(self.step_residuals)
245+
)
246+
247+
# Compute adaptive weights and aggregate the step-wise losses
248+
with torch.no_grad():
249+
name = condition_name or "default"
250+
weights = self._get_weights(name, step_losses)
251+
252+
# Aggregate using the configured strategy
253+
aggregated_loss = self.aggregation_strategy(step_losses * weights)
254+
condition_losses[condition_name] = aggregated_loss
255+
256+
return condition_losses
257+
219258
def predict(self, initial_state, n_steps, **kwargs):
220259
"""
221260
Generate predictions by recursively calling the model's forward.

pina/_src/solver/multi_model_simple_solver.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,24 @@ class MultiModelSimpleSolver(BaseSolver):
3030
\quad i = 1, \dots, N_{\rm ensemble}
3131
3232
During the optimization cycle each model's prediction is evaluated against
33-
the condition independently, and the resulting per-model losses are
34-
averaged to form the aggregated condition loss:
33+
the condition independently, the residual is converted into a pointwise
34+
loss, and the resulting per-model losses are averaged to form the
35+
aggregated condition loss:
3536
3637
.. math::
3738
\mathcal{L}_{\rm condition} = \frac{1}{N_{\rm ensemble}}
3839
\sum_{i=1}^{N_{\rm ensemble}} \mathcal{L}_i
3940
4041
The per-condition workflow is:
4142
42-
1. evaluate the condition for each model and obtain non-aggregated
43-
loss tensors;
44-
2. apply the configured reduction to each per-model tensor;
45-
3. average the reduced per-model losses into a single scalar for
46-
the condition;
47-
4. return the per-condition losses, which are aggregated by the
48-
inherited solver machinery through the configured weighting.
43+
1. evaluate the condition for each model and obtain non-aggregated
44+
residual tensors;
45+
2. apply the pointwise loss and the configured reduction to each
46+
per-model tensor;
47+
3. average the reduced per-model losses into a single scalar for the
48+
condition;
49+
4. return the per-condition losses, which are aggregated by the
50+
inherited solver machinery through the configured weighting.
4951
"""
5052

5153
accepted_conditions_types = (
@@ -191,7 +193,7 @@ def forward(self, x, model_idx=None):
191193
:rtype: LabelTensor | torch.Tensor
192194
"""
193195
if model_idx is not None:
194-
return self.models[model_idx].forward(x)
196+
return self.models[model_idx](x)
195197
return torch.stack(
196198
[self.forward(x, idx) for idx in range(self.num_models)],
197199
)
@@ -259,10 +261,16 @@ def optimization_cycle(self, batch):
259261
self.problem.output_variables,
260262
)
261263

262-
loss_tensor = condition.evaluate(
263-
condition_data, self, self._loss_fn
264+
# Store the residual tensor
265+
self.residual_tensor = condition.evaluate(condition_data, self)
266+
267+
# Compute the per-sample loss tensor
268+
loss_tensor = self._loss_fn(
269+
self.residual_tensor, torch.zeros_like(self.residual_tensor)
264270
)
265271
self.forward = original_forward
272+
273+
# Apply reduction and store the result
266274
per_model_losses.append(self._apply_reduction(loss_tensor))
267275

268276
condition_losses[condition_name] = torch.stack(

pina/_src/solver/single_model_simple_solver.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ class SingleModelSimpleSolver(BaseSolver):
2222
2323
The solver orchestrates a uniform workflow for all conditions in the batch:
2424
25-
1. evaluate the condition and obtain a non-aggregated loss tensor;
26-
2. apply a reduction to obtain a scalar loss for that condition;
25+
1. evaluate the condition and obtain a non-aggregated residual tensor;
26+
2. apply the pointwise loss and a reduction to obtain a scalar loss for
27+
that condition;
2728
4. return the per-condition losses, which are aggregated by the inherited
2829
solver machinery through the configured weighting.
2930
"""
@@ -99,12 +100,19 @@ def optimization_cycle(self, batch):
99100
condition = self.problem.conditions[condition_name]
100101
condition_data = dict(data)
101102

102-
condition_loss_tensor = condition.evaluate(
103-
condition_data, self, self._loss_fn
103+
# Store the residual tensor
104+
self.residual_tensor = condition.evaluate(condition_data, self)
105+
106+
# Compute the per-sample loss tensor
107+
loss_tensor = self._loss_fn(
108+
self.residual_tensor, torch.zeros_like(self.residual_tensor)
104109
)
110+
111+
# Apply reduction and store the result
105112
condition_losses[condition_name] = self._apply_reduction(
106-
condition_loss_tensor
113+
loss_tensor
107114
)
115+
108116
return condition_losses
109117

110118
def _apply_reduction(self, value):

tests/test_condition/test_data_condition.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,8 @@ def test_evaluate(case, use_lt, conditional_variables):
382382
}
383383

384384
# Evaluate the condition and compute the expected value
385-
loss = condition.evaluate(batch, solver, loss_fn)
386-
output_ = solver.forward(batch["input"])
387-
expected = loss_fn(output_, torch.zeros_like(output_))
385+
loss = condition.evaluate(batch, solver)
386+
expected = solver.forward(batch["input"])
388387

389388
# Assert that the evaluated loss is correct
390389
assert torch.allclose(loss, expected)

0 commit comments

Comments
 (0)