make_complete for incomplete multi-index sets produces strange results for lp-degree not 1.0 or inf.
I was trying to synchronize the optimizer_refac
branch with the current dev
branch and found the following issue that might be related to the core functionality of Minterpy
.
The issue
Below is a reproducible example where a term of higher-degree exponents is added to a downward complete, lexicographically sorted multi-index set then made complete:
import numpy as np
import minterpy as mp
# A complete multi-index set
dim = 2
poly_degree = 3
lp_degree = 2.0
exps = mp.MultiIndexSet.from_degree(dim, poly_degree, 2.0).exponents
print(exps)
# [[0 0]
# [1 0]
# [2 0]
# [3 0]
# [0 1]
# [1 1]
# [2 1]
# [0 2]
# [1 2]
# [2 2]
# [0 3]]
# Insert an arbitrary (unique) term
incomplete_exps = np.insert(exps, 7, np.array([4, 1]), axis=0)
print(incomplete_exps)
# [[0 0]
# [1 0]
# [2 0]
# [3 0]
# [0 1]
# [1 1]
# [2 1]
# [4 1]
# [0 2]
# [1 2]
# [2 2]
# [0 3]]
# The multi-index set becomes incomplete
assert not mp.core.utils.is_lexicographically_complete(incomplete_exps)
# Make the index complete
completed_exps = mp.core.utils.make_complete(incomplete_exps)
print(completed_exps)
# [[0 0]
# [1 0]
# [2 0]
# [3 0]
# [4 0] # Strange repeated index
# [4 0] # Strange repeated index
# [0 1]
# [1 1]
# [2 1]
# [3 1]
# [4 1]
# [0 2]
# [1 2]
# [2 2]
# [3 2]
# [0 3]
# [1 3]
# [2 3]
# [0 4] # Strange repeated index
# [0 4] # Strange repeated index
# [1 4]]
# Now the exponents are not lexicographical ordered
assert mp.jit_compiled_utils.have_lexicographical_ordering(completed_exps)
I find this is a strange behavior of the function make_complete
.
There are repeated exponents and the complete set is not lexicographically sorted.
Note that per implementation of have_lexicographical_ordering
, duplicate entries are not allowed.
I would expect the next polynomial degree that contains the term [4, 1]
for lp-degree 2.0 is actually degree 5:
# This will fail
assert np.allclose(completed_exps, mp.MultiIndexSet.from_degree(2, 5, 2).exponents)
While the task of adding term may be quite specific to the optimizer in the optimiser_refac
branch,
the issue is still related how things implemented in the core side of Minterpy
(see below).
Cause
To make a multi-index set complete the function mp.core.utils.make_complete()
relies on two other functions:
-
mp.core.utils._get_poly_degree()
: Get the polynomial degree for a given multi-index set and for a given lp-degree, regardless of whether it is downward complete or not. -
mp.core.utils.get_exponent_matrix()
: Create the exponents matrix for a given dimension, degree, and lp-degree.
I think the issue comes from how the polynomial degree is computed from a given multi-index set, especially for lp-degree not 1.0 or inf before it is passed to core.utils.get_exponent_matrix()
function.
poly_degree = mp.core.utils._get_poly_degree(incomplete_exps, lp_degree)
print(poly_degree)
# 4.123105625617661
new_exps = mp.core.utils.get_exponent_matrix(dim, poly_degree, lp_degree)
print(new_exps)
# [[0 0]
# [1 0]
# [2 0]
# [3 0]
# [4 0] # Strange repeated index
# [4 0] # Strange repeated index
# [0 1]
# [1 1]
# [2 1]
# [3 1]
# [4 1]
# [0 2]
# [1 2]
# [2 2]
# [3 2]
# [0 3]
# [1 3]
# [2 3]
# [0 4] # Strange repeated index
# [0 4] # Strange repeated index
# [1 4]]
I don't think it makes much sense to have a non-whole number for the polynomial degree.
In the case of lp-degrees not 1.0 or inf, it is possible to get a non-whole number as polynomial degree based on the current implementation of _get_poly_degree()
.
Furthermore, the function get_exponent_matrix()
behaves strangely when being passed a non-whole number as the polynomial degree.
Note that this is also inconsistent with the upper-level constructor for MultiIndexSet
, i.e., MultiIndexSet.from_degree()
where non-integer numbers are not allowed for the poly_degree
(NOTE: in that function, however, I guess the restriction is too hard that it throws an error for any floats).
Test coverage
While there is a test for make_complete
, it does not cover this issue because the test logic works by removing (not adding) a term to an already complete multi-index set.
The incomplete set, when pass to _get_poly_degree()
will have the same polynomial degree as the one used to create the initial index.
Adding an arbitrary term to a multi-index set (possibly outside the original degree) will generally result in a non-whole number and should throw an error using the current implementation.
(Possible) Solutions
There are two possible straightforward solutions:
- round up (via, say, a ceiling function) the results of
_get_poly_degree()
(before it returns). This is also saying that the polynomial degree of a given multi-index set can't be a non-whole number regardless of the lp-degree. - Another solution is to put guardrail on the
get_exponent_matrix()
side in order to enforce the integer requirement (at least a whole-number requirement) for thepoly_degree
argument.
Any of these two will solve the issue, but they are not exclusive.
If it really doesn't make any sense to create an exponent matrix (via get_exponent_matrix
) with a non-whole number as poly_degree
then put a guardrail also there.
I'm working on this solution and expand the test a bit; I will push it soon.