From d66647ecc364f27a5ab931ac3bc05b102af57203 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Sun, 3 May 2026 18:52:11 +0200 Subject: [PATCH 01/11] refactor is_linear_expression function --- sympde/expr/expr.py | 127 +++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 46202054..0eeac3dd 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -127,7 +127,7 @@ def __new__(cls, arguments, expr, **options): args = _sanitize_arguments(arguments, is_linear=True) - if not is_linear_expression(expr, args, integral=False): + if not is_linear_expression(expr, args): msg = '> Expression is not linear' raise UnconsistentLinearExpressionError(msg) @@ -739,71 +739,86 @@ def linearize(form, fields, trials=None): return BilinearForm((trials, tests), bilinear_expr) #============================================================================== -def is_linear_expression(expr, args, integral=True, debug=True): - """checks if an expression is linear with respect to the given arguments.""" - # ... - left_args = [] - right_args = [] +def is_linear_expression(expr, args, debug=True): + """ + Checks if expression is linear with respect to ``args``: + + 1. Additivity: f(x + y) = f(x) + f(y) + 2. Homogeneity: f(alpha * x) = alpha * f(x) + + Parameters + ---------- + expr + Symbolic expression to test. + args : iterable + Each argument must be ScalarFunction or VectorFunction. + debug : bool, optional + Print diagnostic info if a check fails. + + Returns + ------- + bool + True if the expression is linear with respect to all given arguments, False otherwise. + """ + + x_args = [] + y_args = [] + # create 2 independent copies (x and y) of every original argument for arg in args: - tag = random_string( 4 ) + tag = random_string(4) if isinstance(arg, ScalarFunction): - left = ScalarFunction(arg.space, name='l_' + tag) - right = ScalarFunction(arg.space, name='r_' + tag) + x = ScalarFunction(arg.space, name='x_' + tag) + y = ScalarFunction(arg.space, name='y_' + tag) elif isinstance(arg, VectorFunction): - left = VectorFunction(arg.space, name='l_' + tag) - right = VectorFunction(arg.space, name='r_' + tag) + x = VectorFunction(arg.space, name='x_' + tag) + y = VectorFunction(arg.space, name='y_' + tag) else: raise TypeError('argument must be a {Scalar|Vector}Function') - left_args += [left] - right_args += [right] - # ... - - # ... check addition - newargs = [left + right for left, right in zip(left_args, right_args)] - - newexpr = expr.subs(zip(args, newargs)) - left_expr = expr.subs(zip(args, left_args)) - right_expr = expr.subs(zip(args, right_args)) - - a = newexpr - b = left_expr + right_expr - - if not( (a-b).expand() == 0 or a.expand() == b.expand()): - # TODO use a warning or exception? - if debug: - print('Failed to assert addition property') - print('{} != {}'.format(a.expand(), b.expand())) - return False - - # ... - - # ... check multiplication - tag = random_string( 4 ) - coeff = Constant('alpha_' + tag) - - newexpr = expr - for arg, left in zip(args, left_args): - newarg = coeff * left - newexpr = newexpr.subs(arg, newarg) - - atoms = list(newexpr.atoms(BasicOperator)) + x_args.append(x) + y_args.append(y) + + # --------------------------------------------------------------------------- + # check addition property: f(x + y) = f(x) + f(y) + summed_args = [x + y for x, y in zip(x_args, y_args)] + expr_at_x = expr.subs(zip(args, x_args)) # f(x) + expr_at_y = expr.subs(zip(args, y_args)) # f(y) + expr_at_sum = expr.subs(zip(args, summed_args)) # f(x + y) + expected_sum = expr_at_x + expr_at_y # = f(x) + f (y) + + if (expr_at_sum - expected_sum).expand() != 0: + expr1 = expr_at_sum.expand() + expr2 = expected_sum.expand() + if expr1 != expr2: + if debug: + print('Failed to assert addition property') + print('{} != {}'.format(expr1, expr2)) + return False + + # --------------------------------------------------------------------------- + # check multiplication property: f(alpha * x) = alpha * f(x) + alpha = Constant(f"alpha_{random_string(4)}") + + scaled_x_args = [alpha * x for x in x_args] + expr_at_scaled_x = expr.subs(zip(args, scaled_x_args)) + + atoms = list(expr_at_scaled_x.atoms(BasicOperator)) subs = [e.func(*e.args, evaluate=True) for e in atoms] - newexpr = newexpr.subs(zip(atoms, subs)) - - - left_expr = expr.subs(list(zip(args, left_args))) - left_expr = coeff * left_expr - if not( (newexpr-left_expr).expand() == 0 or newexpr.expand()==left_expr.expand()): - # TODO use a warning or exception? - if debug: - print('Failed to assert multiplication property') - print('{} != {}'.format(newexpr, left_expr)) - return False - # ... + expr_at_scaled_x = expr_at_scaled_x.subs(zip(atoms, subs)) + + scaled_expr = alpha * expr_at_x + + if (expr_at_scaled_x - scaled_expr).expand() != 0: + expr1 = expr_at_scaled_x.expand() + expr2 = scaled_expr.expand() + if expr1 != expr2: + if debug: + print('Failed to assert multiplication property') + print('{} != {}'.format(expr1, expr2)) + return False return True From 62262812644152eede66adba3fc27b536abddd29 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Tue, 14 Jul 2026 17:06:42 +0200 Subject: [PATCH 02/11] change version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7189fb20..63fe7a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sympde" -version = "0.19.2" +version = "0.19.3" description = "Symbolic calculus for partial differential equations (and variational forms)" readme = "README.rst" requires-python = ">= 3.9" From e49a7b4457c54c46e7e065ebdd0faf7716cfe91f Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Tue, 14 Jul 2026 17:34:06 +0200 Subject: [PATCH 03/11] change version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 63fe7a31..7189fb20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sympde" -version = "0.19.3" +version = "0.19.2" description = "Symbolic calculus for partial differential equations (and variational forms)" readme = "README.rst" requires-python = ">= 3.9" From 85fba96b02a2d4559700ab06e8bfecdcb2e31a0d Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Tue, 14 Jul 2026 17:36:49 +0200 Subject: [PATCH 04/11] change version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7189fb20..63fe7a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sympde" -version = "0.19.2" +version = "0.19.3" description = "Symbolic calculus for partial differential equations (and variational forms)" readme = "README.rst" requires-python = ">= 3.9" From 320ebaaf6af61d4b34c4341d662920ab531c2fee Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Thu, 16 Jul 2026 18:00:00 +0200 Subject: [PATCH 05/11] added type of expr --- sympde/expr/expr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 0eeac3dd..1f29a36d 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -748,7 +748,7 @@ def is_linear_expression(expr, args, debug=True): Parameters ---------- - expr + expr : Expr Symbolic expression to test. args : iterable Each argument must be ScalarFunction or VectorFunction. @@ -760,6 +760,7 @@ def is_linear_expression(expr, args, debug=True): bool True if the expression is linear with respect to all given arguments, False otherwise. """ + assert isinstance(expr, Expr) x_args = [] y_args = [] From 661c442b026bb2c6b96cf107451dc16c89f29ac0 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Thu, 16 Jul 2026 18:33:43 +0200 Subject: [PATCH 06/11] error messages --- sympde/expr/expr.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 1f29a36d..e8ad0f24 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -795,8 +795,13 @@ def is_linear_expression(expr, args, debug=True): expr2 = expected_sum.expand() if expr1 != expr2: if debug: - print('Failed to assert addition property') - print('{} != {}'.format(expr1, expr2)) + print(r"Failed to assert addition property `f(x + y) = f(x) + f(y)`, where:") + print() + print('f(x + y) =') + print(expr1) + print() + print('f(x) + f(y) =') + print(expr2) return False # --------------------------------------------------------------------------- @@ -817,8 +822,13 @@ def is_linear_expression(expr, args, debug=True): expr2 = scaled_expr.expand() if expr1 != expr2: if debug: - print('Failed to assert multiplication property') - print('{} != {}'.format(expr1, expr2)) + print(r"Failed to assert multiplication property `f(alpha * x) = alpha * f(x)`, where:") + print() + print('f(alpha * x) =') + print(expr1) + print() + print('alpha * f(x) =') + print(expr2) return False return True From d87e74c4e8db2fbb9fa893fae92bee7d51c7fc1a Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Thu, 16 Jul 2026 19:36:33 +0200 Subject: [PATCH 07/11] improved docstring --- sympde/expr/expr.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index e8ad0f24..87561ac5 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -741,16 +741,22 @@ def linearize(form, fields, trials=None): #============================================================================== def is_linear_expression(expr, args, debug=True): """ - Checks if expression is linear with respect to ``args``: + Checks if expression is linear with respect to each argument in``args``: 1. Additivity: f(x + y) = f(x) + f(y) 2. Homogeneity: f(alpha * x) = alpha * f(x) + In this notation, x and y represent independent copies of the same argument. In general, f may have an + arbitrary number of arguments. + Parameters ---------- expr : Expr Symbolic expression to test. args : iterable + Arguments with respect to which expr is tested for linearity. + Only one instance of each argument needs to be provided; the function internally creates + the independent copies required for the additivity and homogeneity checks. Each argument must be ScalarFunction or VectorFunction. debug : bool, optional Print diagnostic info if a check fails. From 16b18c787b124887943ea333ade0a74194806fdf Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Thu, 16 Jul 2026 19:39:40 +0200 Subject: [PATCH 08/11] formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- sympde/expr/expr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 87561ac5..a3c2f184 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -793,8 +793,8 @@ def is_linear_expression(expr, args, debug=True): summed_args = [x + y for x, y in zip(x_args, y_args)] expr_at_x = expr.subs(zip(args, x_args)) # f(x) expr_at_y = expr.subs(zip(args, y_args)) # f(y) - expr_at_sum = expr.subs(zip(args, summed_args)) # f(x + y) - expected_sum = expr_at_x + expr_at_y # = f(x) + f (y) + expr_at_sum = expr.subs(zip(args, summed_args)) # f(x + y) + expected_sum = expr_at_x + expr_at_y # f(x) + f(y) if (expr_at_sum - expected_sum).expand() != 0: expr1 = expr_at_sum.expand() From 04040e112ee032ca50b012320bcd6c79cb564267 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Fri, 17 Jul 2026 14:05:19 +0200 Subject: [PATCH 09/11] reduce line length --- sympde/expr/expr.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index a3c2f184..1b935ed8 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -755,8 +755,9 @@ def is_linear_expression(expr, args, debug=True): Symbolic expression to test. args : iterable Arguments with respect to which expr is tested for linearity. - Only one instance of each argument needs to be provided; the function internally creates - the independent copies required for the additivity and homogeneity checks. + Only one instance of each argument needs to be provided; the function + internally creates the independent copies required for the additivity + and homogeneity checks. Each argument must be ScalarFunction or VectorFunction. debug : bool, optional Print diagnostic info if a check fails. @@ -764,7 +765,8 @@ def is_linear_expression(expr, args, debug=True): Returns ------- bool - True if the expression is linear with respect to all given arguments, False otherwise. + True if the expression is linear with respect to all given arguments, + False otherwise. """ assert isinstance(expr, Expr) @@ -788,7 +790,7 @@ def is_linear_expression(expr, args, debug=True): x_args.append(x) y_args.append(y) - # --------------------------------------------------------------------------- + # ------------------------------------------------------------------------ # check addition property: f(x + y) = f(x) + f(y) summed_args = [x + y for x, y in zip(x_args, y_args)] expr_at_x = expr.subs(zip(args, x_args)) # f(x) @@ -810,7 +812,7 @@ def is_linear_expression(expr, args, debug=True): print(expr2) return False - # --------------------------------------------------------------------------- + # ------------------------------------------------------------------------ # check multiplication property: f(alpha * x) = alpha * f(x) alpha = Constant(f"alpha_{random_string(4)}") From 6127f0ee9be7d418b433bfb3b8eab0eaabd3cd76 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Fri, 17 Jul 2026 14:06:11 +0200 Subject: [PATCH 10/11] Update docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- sympde/expr/expr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index 1b935ed8..a61ca8c9 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -741,13 +741,14 @@ def linearize(form, fields, trials=None): #============================================================================== def is_linear_expression(expr, args, debug=True): """ - Checks if expression is linear with respect to each argument in``args``: + Checks if expression is linear with respect to each argument in ``args``: 1. Additivity: f(x + y) = f(x) + f(y) 2. Homogeneity: f(alpha * x) = alpha * f(x) - In this notation, x and y represent independent copies of the same argument. In general, f may have an - arbitrary number of arguments. + In general, `f` may have an arbitrary number of arguments: `x` and `y` + represent independent copies of the same list of arguments. + Parameters ---------- From f2c35056c80ad3decc162488e6f7a5eb5c6f8737 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Fri, 17 Jul 2026 14:30:31 +0200 Subject: [PATCH 11/11] reduce line length --- sympde/expr/expr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sympde/expr/expr.py b/sympde/expr/expr.py index a61ca8c9..67557467 100644 --- a/sympde/expr/expr.py +++ b/sympde/expr/expr.py @@ -804,7 +804,8 @@ def is_linear_expression(expr, args, debug=True): expr2 = expected_sum.expand() if expr1 != expr2: if debug: - print(r"Failed to assert addition property `f(x + y) = f(x) + f(y)`, where:") + print(r"Failed to assert addition property " + r"`f(x + y) = f(x) + f(y)`, where:") print() print('f(x + y) =') print(expr1) @@ -831,7 +832,8 @@ def is_linear_expression(expr, args, debug=True): expr2 = scaled_expr.expand() if expr1 != expr2: if debug: - print(r"Failed to assert multiplication property `f(alpha * x) = alpha * f(x)`, where:") + print(r"Failed to assert multiplication property " + r"`f(alpha * x) = alpha * f(x)`, where:") print() print('f(alpha * x) =') print(expr1)