Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Add remove_variables method to EquationSystem #1321

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/porepy/numerics/ad/equation_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,32 @@ def create_variables(

return merged_variable

def remove_variables(self, variables: VariableList) -> None:
keileg marked this conversation as resolved.
Show resolved Hide resolved
"""Removes variables from the system.
keileg marked this conversation as resolved.
Show resolved Hide resolved
The variables are removed from the system and the DOFs are reordered.

Parameters:
variables: List of variables to remove. Variables can be given as a list of
variables, mixed-dimensional variables, or variable names (strings).

Raises:
ValueError: If a variable is not known to the system.

"""
variables = self._parse_variable_type(variables)
for var in variables:
if var.id not in self._variables:
raise ValueError(
f"Variable {var.name} (ID: {var.id}) not known to the system."
)
# Remove the variable from the system. _variables, _variable_dof_type and
# _variable_numbers are indexed by variable id.
del self._variables[var.id]
self._variable_dof_type.pop(var.id)
self._variable_numbers.pop(var.id)
# Update the variable clustering. This also updates _variable_num_dofs.
self._cluster_dofs_gridwise()

def update_variable_tags(
self,
tags: dict[str, Any],
Expand Down
78 changes: 76 additions & 2 deletions tests/numerics/ad/test_equation_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* test_schur_complement: Assemble a subsystem, using a Schur complement reduction.

"""

import numpy as np
import pytest
import scipy.sparse as sps
Expand Down Expand Up @@ -89,7 +90,9 @@ def test_evaluate_variables():
ad_array_prev_timestep = var_prev_timestep.value_and_jacobian(eq_system)
assert isinstance(ad_array_prev_timestep, pp.ad.AdArray)
assert np.allclose(ad_array_prev_timestep.val, 1)
assert np.allclose(ad_array_prev_timestep.jac.toarray(), np.zeros(known_jac.shape))
assert np.allclose(
ad_array_prev_timestep.jac.toarray(), np.zeros(known_jac.shape)
)

# Use the ad machinery to define the difference between the current and previous
# time step. This should give an AdArray with the same value as that obtained by
Expand Down Expand Up @@ -196,6 +199,77 @@ def test_variable_creation():
)


@pytest.mark.parametrize("variable_to_be_removed", ["var_1", "var_2", "var_3", None])
def test_remove_variables(variable_to_be_removed):
keileg marked this conversation as resolved.
Show resolved Hide resolved
"""Test that removing one of three md-variables from an EquationSystem works as
expected.

The test generates a MixedDimensionalGrid and defines some variables on it. The test
then removes one of the variables, and checks that the remaining variables are
correctly stored in the EquationSystem.

Parameters:
variable_to_be_removed (str): The name of the variable to be removed. If None, no
variable is removed.
"""
mdg, _ = square_with_orthogonal_fractures("cartesian", {"cell_size": 0.5}, [1])

equation_system = pp.ad.EquationSystem(mdg)
# Define the number of variables per grid item. Faces are included just to test that
# # this also works.
num_dof_per_cell, num_dof_per_face = 1, 2
dof_info_sd = {"cells": num_dof_per_cell, "faces": num_dof_per_face}
dof_info_intf = {"cells": num_dof_per_cell}

# Create variables on subdomain and interface.
subdomains = mdg.subdomains()
single_subdomain = mdg.subdomains(dim=mdg.dim_max())

interfaces = mdg.interfaces()

# Define one variable on all subdomains, one on a single subdomain, and one on all
# interfaces (there is only one interface in this grid).
var_1 = equation_system.create_variables(
"var_1", dof_info_sd, subdomains=subdomains
)
var_2 = equation_system.create_variables(
"var_2", dof_info_sd, subdomains=single_subdomain
)
var_3 = equation_system.create_variables(
"var_3", dof_info_intf, interfaces=interfaces
)

if variable_to_be_removed:
var_to_remove = equation_system.md_variable(variable_to_be_removed)
equation_system.remove_variables([var_to_remove])
# Check that the EquationSystem does not contain the removed variable anymore.
for field in ["_variables", "_variable_numbers", "_variable_dof_type"]:
assert variable_to_be_removed not in getattr(equation_system, field).keys()
# Check that trying to remove the variable again raises an error.
with pytest.raises(ValueError):
equation_system.remove_variables([var_to_remove])
else:
equation_system.remove_variables([])

# Identify remaining subvariables. This allows direct comparison with
# equation_system.variables.
remaining_vars = []
for var in [var_1, var_2, var_3]:
if var.name == variable_to_be_removed:
continue
remaining_vars.extend(var.sub_vars)

remaining_dofs = np.hstack(
[equation_system.dofs_of([var]) for var in remaining_vars]
)

# Check that the number of dofs is correct and that the dofs form a continuous
# range.
assert np.allclose(np.sort(remaining_dofs), np.arange(equation_system.num_dofs()))
# Check that the number of variables is correct.
assert len(equation_system.variables) == len(remaining_vars)


def test_variable_tags():
"""Test that variables can be tagged, and that the tags are correctly propagated
to the underlying atomic variables.
Expand Down Expand Up @@ -1138,7 +1212,7 @@ def test_assemble(setup, equation_variables):
b_sub_only_rhs = sys_man.assemble(
evaluate_jacobian=False, equations=eq_names, variables=var_names
)

# Check that the residual vector is the same regardless of whether the Jacobian
# is evaluated or not.
assert np.allclose(b_sub, b_sub_only_rhs)
Expand Down