Skip to content

Commit 254ec8e

Browse files
fix advection (#581)
1 parent e1d1331 commit 254ec8e

File tree

2 files changed

+204
-33
lines changed

2 files changed

+204
-33
lines changed

pina/operator.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -275,44 +275,42 @@ def fast_laplacian(output_, input_, components, d, method="std"):
275275
def fast_advection(output_, input_, velocity_field, components, d):
276276
"""
277277
Perform the advection operation on the ``output_`` with respect to the
278-
``input``. This operator support vector-valued functions with multiple input
279-
coordinates.
278+
``input``. This operator supports vector-valued functions with multiple
279+
input coordinates.
280280
281281
Unlike ``advection``, this function performs no internal checks on input and
282282
output tensors. The user is required to specify both ``components`` and
283283
``d`` as lists of strings. It is designed to enhance computation speed.
284284
285285
:param LabelTensor output_: The output tensor on which the advection is
286-
computed.
286+
computed. It includes both the velocity and the quantity to be advected.
287287
:param LabelTensor input_: the input tensor with respect to which advection
288288
is computed.
289-
:param str velocity_field: The name of the output variable used as velocity
290-
field. It must be chosen among the output labels.
289+
:param list[str] velocity_field: The name of the output variables used as
290+
velocity field. It must be chosen among the output labels.
291291
:param list[str] components: The names of the output variables for which to
292292
compute the advection. It must be a subset of the output labels.
293293
:param list[str] d: The names of the input variables with respect to which
294294
the advection is computed. It must be a subset of the input labels.
295295
:return: The computed advection tensor.
296-
:rtype: torch.Tensor
296+
:rtype: LabelTensor
297297
"""
298298
# Add a dimension to the velocity field for following operations
299299
velocity = output_.extract(velocity_field).unsqueeze(-1)
300300

301-
# Remove the velocity field from the components
302-
filter_components = [c for c in components if c != velocity_field]
303-
304301
# Compute the gradient
305302
grads = fast_grad(
306-
output_=output_, input_=input_, components=filter_components, d=d
303+
output_=output_, input_=input_, components=components, d=d
307304
)
308305

309306
# Reshape into [..., len(filter_components), len(d)]
310-
tmp = grads.reshape(*output_.shape[:-1], len(filter_components), len(d))
307+
tmp = grads.reshape(*output_.shape[:-1], len(components), len(d))
311308

312309
# Transpose to [..., len(d), len(filter_components)]
313310
tmp = tmp.transpose(-1, -2)
314311

315-
return (tmp * velocity).sum(dim=tmp.tensor.ndim - 2)
312+
adv = (tmp * velocity).sum(dim=tmp.tensor.ndim - 2)
313+
return LabelTensor(adv, labels=[f"adv_{c}" for c in components])
316314

317315

318316
def grad(output_, input_, components=None, d=None):
@@ -425,15 +423,16 @@ def laplacian(output_, input_, components=None, d=None, method="std"):
425423
def advection(output_, input_, velocity_field, components=None, d=None):
426424
"""
427425
Perform the advection operation on the ``output_`` with respect to the
428-
``input``. This operator support vector-valued functions with multiple input
429-
coordinates.
426+
``input``. This operator supports vector-valued functions with multiple
427+
input coordinates.
430428
431429
:param LabelTensor output_: The output tensor on which the advection is
432-
computed.
430+
computed. It includes both the velocity and the quantity to be advected.
433431
:param LabelTensor input_: the input tensor with respect to which advection
434432
is computed.
435-
:param str velocity_field: The name of the output variable used as velocity
433+
:param velocity_field: The name of the output variables used as velocity
436434
field. It must be chosen among the output labels.
435+
:type velocity_field: str | list[str]
437436
:param components: The names of the output variables for which to compute
438437
the advection. It must be a subset of the output labels.
439438
If ``None``, all output variables are considered. Default is ``None``.
@@ -444,18 +443,29 @@ def advection(output_, input_, velocity_field, components=None, d=None):
444443
:type d: str | list[str]
445444
:raises TypeError: If the input tensor is not a LabelTensor.
446445
:raises TypeError: If the output tensor is not a LabelTensor.
447-
:raises RuntimeError: If the velocity field is not in the output labels.
446+
:raises RuntimeError: If the velocity field is not a subset of the output
447+
labels.
448+
:raises RuntimeError: If the dimensionality of the velocity field does not
449+
match that of the input tensor.
448450
:return: The computed advection tensor.
449-
:rtype: torch.Tensor
451+
:rtype: LabelTensor
450452
"""
451453
components, d = _check_values(
452454
output_=output_, input_=input_, components=components, d=d
453455
)
454456

455-
# Check if velocity field is present in the output labels
456-
if velocity_field not in output_.labels:
457+
# Map velocity_field to a list if it is a string
458+
if isinstance(velocity_field, str):
459+
velocity_field = [velocity_field]
460+
461+
# Check if all the velocity_field labels are present in the output labels
462+
if not all(vi in output_.labels for vi in velocity_field):
463+
raise RuntimeError("Velocity labels missing from output tensor.")
464+
465+
# Check if the velocity has the same dimensionality as the input tensor
466+
if len(velocity_field) != len(d):
457467
raise RuntimeError(
458-
f"Velocity {velocity_field} is not present in the output labels."
468+
"Velocity dimensionality does not match input dimensionality."
459469
)
460470

461471
return fast_advection(

tests/test_operator.py

Lines changed: 173 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,22 +296,183 @@ def test_laplacian(f):
296296
laplacian(output_=output_, input_=input_, components=["a", "b", "c"])
297297

298298

299-
def test_advection():
299+
def test_advection_scalar():
300300

301-
# Define input and output
301+
# Define 3-dimensional input
302302
input_ = torch.rand((20, 3), requires_grad=True)
303303
input_ = LabelTensor(input_, ["x", "y", "z"])
304-
output_ = LabelTensor(input_**2, ["u", "v", "c"])
305304

306-
# Define the velocity field
307-
velocity = output_.extract(["c"])
305+
# Define 3-dimensional velocity field and quantity to be advected
306+
velocity = torch.rand((20, 3), requires_grad=True)
307+
field = torch.sum(input_**2, dim=-1, keepdim=True)
308+
309+
# Combine velocity and field into a LabelTensor
310+
labels = ["ux", "uy", "uz", "c"]
311+
output_ = LabelTensor(torch.cat((velocity, field), dim=1), labels)
312+
313+
# Compute the pina advection
314+
components = ["c"]
315+
pina_adv = advection(
316+
output_=output_,
317+
input_=input_,
318+
velocity_field=["ux", "uy", "uz"],
319+
components=components,
320+
d=["x", "y", "z"],
321+
)
322+
323+
# Compute the true advection
324+
grads = 2 * input_
325+
true_adv = torch.sum(grads * velocity, dim=grads.ndim - 1, keepdim=True)
326+
327+
# Check the shape, labels, and value of the advection
328+
assert pina_adv.shape == (*output_.shape[:-1], len(components))
329+
assert pina_adv.labels == ["adv_c"]
330+
assert torch.allclose(pina_adv, true_adv)
331+
332+
# Should fail if input not a LabelTensor
333+
with pytest.raises(TypeError):
334+
advection(
335+
output_=output_,
336+
input_=input_.tensor,
337+
velocity_field=["ux", "uy", "uz"],
338+
)
339+
340+
# Should fail if output not a LabelTensor
341+
with pytest.raises(TypeError):
342+
advection(
343+
output_=output_.tensor,
344+
input_=input_,
345+
velocity_field=["ux", "uy", "uz"],
346+
)
347+
348+
# Should fail for non-existent input labels
349+
with pytest.raises(RuntimeError):
350+
advection(
351+
output_=output_,
352+
input_=input_,
353+
d=["x", "a"],
354+
velocity_field=["ux", "uy", "uz"],
355+
)
356+
357+
# Should fail for non-existent output labels
358+
with pytest.raises(RuntimeError):
359+
advection(
360+
output_=output_,
361+
input_=input_,
362+
components=["a", "b", "c"],
363+
velocity_field=["ux", "uy", "uz"],
364+
)
365+
366+
# Should fail if velocity_field labels are not present in the output labels
367+
with pytest.raises(RuntimeError):
368+
advection(
369+
output_=output_,
370+
input_=input_,
371+
velocity_field=["ux", "uy", "nonexistent"],
372+
components=["c"],
373+
)
374+
375+
# Should fail if velocity_field dimensionality does not match input tensor
376+
with pytest.raises(RuntimeError):
377+
advection(
378+
output_=output_,
379+
input_=input_,
380+
velocity_field=["ux", "uy"],
381+
components=["c"],
382+
)
383+
384+
385+
def test_advection_vector():
308386

309-
# Compute the true advection and the pina advection
310-
pina_advection = advection(
311-
output_=output_, input_=input_, velocity_field="c"
387+
# Define 3-dimensional input
388+
input_ = torch.rand((20, 3), requires_grad=True)
389+
input_ = LabelTensor(input_, ["x", "y", "z"])
390+
391+
# Define 3-dimensional velocity field
392+
velocity = torch.rand((20, 3), requires_grad=True)
393+
394+
# Define 2-dimensional field to be advected
395+
field_1 = torch.sum(input_**2, dim=-1, keepdim=True)
396+
field_2 = torch.sum(input_**3, dim=-1, keepdim=True)
397+
398+
# Combine velocity and field into a LabelTensor
399+
labels = ["ux", "uy", "uz", "c1", "c2"]
400+
output_ = LabelTensor(
401+
torch.cat((velocity, field_1, field_2), dim=1), labels
402+
)
403+
404+
# Compute the pina advection
405+
components = ["c1", "c2"]
406+
pina_adv = advection(
407+
output_=output_,
408+
input_=input_,
409+
velocity_field=["ux", "uy", "uz"],
410+
components=components,
411+
d=["x", "y", "z"],
312412
)
313-
true_advection = velocity * 2 * input_.extract(["x", "y"])
314413

315-
# Check the shape of the advection
316-
assert pina_advection.shape == (*output_.shape[:-1], output_.shape[-1] - 1)
317-
assert torch.allclose(pina_advection, true_advection)
414+
# Compute the true gradients of the fields "c1", "c2"
415+
grads1 = 2 * input_
416+
grads2 = 3 * input_**2
417+
418+
# Compute the true advection for each field
419+
true_adv1 = torch.sum(grads1 * velocity, dim=grads1.ndim - 1, keepdim=True)
420+
true_adv2 = torch.sum(grads2 * velocity, dim=grads2.ndim - 1, keepdim=True)
421+
true_adv = torch.cat((true_adv1, true_adv2), dim=-1)
422+
423+
# Check the shape, labels, and value of the advection
424+
assert pina_adv.shape == (*output_.shape[:-1], len(components))
425+
assert pina_adv.labels == ["adv_c1", "adv_c2"]
426+
assert torch.allclose(pina_adv, true_adv)
427+
428+
# Should fail if input not a LabelTensor
429+
with pytest.raises(TypeError):
430+
advection(
431+
output_=output_,
432+
input_=input_.tensor,
433+
velocity_field=["ux", "uy", "uz"],
434+
)
435+
436+
# Should fail if output not a LabelTensor
437+
with pytest.raises(TypeError):
438+
advection(
439+
output_=output_.tensor,
440+
input_=input_,
441+
velocity_field=["ux", "uy", "uz"],
442+
)
443+
444+
# Should fail for non-existent input labels
445+
with pytest.raises(RuntimeError):
446+
advection(
447+
output_=output_,
448+
input_=input_,
449+
d=["x", "a"],
450+
velocity_field=["ux", "uy", "uz"],
451+
)
452+
453+
# Should fail for non-existent output labels
454+
with pytest.raises(RuntimeError):
455+
advection(
456+
output_=output_,
457+
input_=input_,
458+
components=["a", "b", "c"],
459+
velocity_field=["ux", "uy", "uz"],
460+
)
461+
462+
# Should fail if velocity_field labels are not present in the output labels
463+
with pytest.raises(RuntimeError):
464+
advection(
465+
output_=output_,
466+
input_=input_,
467+
velocity_field=["ux", "uy", "nonexistent"],
468+
components=["c"],
469+
)
470+
471+
# Should fail if velocity_field dimensionality does not match input tensor
472+
with pytest.raises(RuntimeError):
473+
advection(
474+
output_=output_,
475+
input_=input_,
476+
velocity_field=["ux", "uy"],
477+
components=["c"],
478+
)

0 commit comments

Comments
 (0)