Skip to content

Commit 726463f

Browse files
authored
Remove cvxpy for weight minimization, fix test values and refactor tests codes (#120)
* Add pyproject.toml * Remove cvxpy dep * Use scipy L-BFGS-B for matrix optimization, remove cvxpy * Correclty set the bounds for example set in optimization * Remove abs tolerance of 0.5 * Add news * Fix tests for computing real root * Refactor test_contianers test * Refactor test for test factorizers * Refactor test optimizers * Refactor test polynomial * Refactor subroutines test * Fix typo in news * Remove usage of input in test_containers.py
1 parent fb8db49 commit 726463f

11 files changed

+635
-539
lines changed

news/cvxpy.rst

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
**Added:**
2+
3+
* L-BFGS-B method in scipy for weight optimization in def get_weights
4+
* Support for Python 3.13
5+
6+
**Changed:**
7+
8+
* <news item>
9+
10+
**Deprecated:**
11+
12+
* <news item>
13+
14+
**Removed:**
15+
16+
* cvxpy dependency for linear weight optimization in def get_weights
17+
* Support for Python 3.10
18+
19+
**Fixed:**
20+
21+
* Absolute tolerance for updated weighted matrix from 0.5 to 1e-05 in test_subroutines.py
22+
23+
**Security:**
24+
25+
* <news item>

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ maintainers = [
1414
description = "A python package implementing the stretched NMF algorithm."
1515
keywords = ['diffpy', 'PDF']
1616
readme = "README.rst"
17-
requires-python = ">=3.10, <3.13"
17+
requires-python = ">=3.11, <3.14"
1818
classifiers = [
1919
'Development Status :: 4 - Beta',
2020
'Environment :: Console',

requirements/conda.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
numpy
22
scipy
3-
cvxpy
43
diffpy.utils
54
numdifftools

requirements/pip.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
numpy
22
scipy
3-
cvxpy
43
diffpy.utils
54
numdifftools

src/diffpy/snmf/optimizers.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import cvxpy
21
import numpy as np
2+
from scipy.optimize import minimize
33

44

55
def get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound):
@@ -36,20 +36,19 @@ def get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound
3636
input set. Has length C
3737
3838
"""
39+
3940
stretched_component_gram_matrix = np.asarray(stretched_component_gram_matrix)
4041
linear_coefficient = np.asarray(linear_coefficient)
4142
upper_bound = np.asarray(upper_bound)
4243
lower_bound = np.asarray(lower_bound)
4344

44-
problem_size = max(linear_coefficient.shape)
45-
solution_variable = cvxpy.Variable(problem_size)
46-
47-
objective = cvxpy.Minimize(
48-
linear_coefficient.T @ solution_variable
49-
+ 0.5 * cvxpy.quad_form(solution_variable, stretched_component_gram_matrix)
50-
)
51-
constraints = [lower_bound <= solution_variable, solution_variable <= upper_bound]
45+
# Set dynamic bounds based on the size of the linear coefficient
46+
bounds = [(lower_bound, upper_bound) for _ in range(len(linear_coefficient))]
47+
initial_guess = np.zeros_like(linear_coefficient)
5248

53-
cvxpy.Problem(objective, constraints).solve()
49+
# Find optimal weights of linear coefficients
50+
def obj_func(y):
51+
return linear_coefficient.T @ y + 0.5 * y.T @ stretched_component_gram_matrix @ y
5452

55-
return solution_variable.value
53+
result = minimize(obj_func, initial_guess, method="L-BFGS-B", bounds=bounds)
54+
return result.x

src/diffpy/snmf/polynomials.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22

33

4-
def rooth(linear_coefficient, constant_term):
4+
def compute_root(linear_coefficient, constant_term):
55
"""
66
Returns the largest real root of x^3+(linear_coefficient) * x + constant_term. If there are no real roots
77
return 0.
@@ -19,7 +19,6 @@ def rooth(linear_coefficient, constant_term):
1919
The largest real root of x^3+(linear_coefficient) * x + constant_term if roots are real, else
2020
return 0 array
2121
22-
2322
"""
2423
linear_coefficient = np.asarray(linear_coefficient)
2524
constant_term = np.asarray(constant_term)

tests/test_containers.py

+101-80
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,59 @@
33

44
from diffpy.snmf.containers import ComponentSignal
55

6-
tas = [
7-
(
8-
[np.arange(10), 3, 0, [6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71], 0.25],
9-
[
10-
[6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0],
11-
[0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0],
12-
[0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0],
13-
],
14-
),
15-
(
16-
[np.arange(5), 10, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], 1.25],
17-
[
18-
[-11.47, -10.8444, -9.1322, -16.633, -20.6760],
19-
[0, -0.50048, -3.31904, 40.9824, -112.1792],
20-
[0, 0.800768, 5.310464, -65.57184, 179.48672],
21-
],
22-
),
23-
(
24-
[np.arange(5), 2, 0, [-11.47, -10.688, -8.095, -29.44, 14.38], 0.88],
25-
[
26-
[-11.47, -10.3344, -13.9164, -11.5136, 0],
27-
[0, -3.3484, 55.1265, -169.7572, 0],
28-
[0, 7.609997, -125.2876, 385.81189, 0],
29-
],
30-
),
31-
(
32-
[np.arange(10), 1, 2, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.88],
33-
[
34-
[1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0],
35-
[0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0],
36-
[0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0],
37-
],
38-
),
39-
(
40-
[
6+
7+
@pytest.mark.parametrize(
8+
"grid, number_of_signals, id_number, iq, stretching_factor, expected",
9+
[
10+
(
11+
np.arange(10),
12+
3,
13+
0,
14+
[6.55, 0.357, 8.49, 9.33, 6.78, 7.57, 7.43, 3.92, 6.55, 1.71],
15+
0.25,
16+
[
17+
[6.55, 6.78, 6.55, 0, 0, 0, 0, 0, 0, 0],
18+
[0, 14.07893122, 35.36478086, 0, 0, 0, 0, 0, 0, 0],
19+
[0, -19.92049156, 11.6931482, 0, 0, 0, 0, 0, 0, 0],
20+
],
21+
),
22+
(
23+
np.arange(5),
24+
10,
25+
0,
26+
[-11.47, -10.688, -8.095, -29.44, 14.38],
27+
1.25,
28+
[
29+
[-11.47, -10.8444, -9.1322, -16.633, -20.6760],
30+
[0, -0.50048, -3.31904, 40.9824, -112.1792],
31+
[0, 0.800768, 5.310464, -65.57184, 179.48672],
32+
],
33+
),
34+
(
35+
np.arange(5),
36+
2,
37+
0,
38+
[-11.47, -10.688, -8.095, -29.44, 14.38],
39+
0.88,
40+
[
41+
[-11.47, -10.3344, -13.9164, -11.5136, 0],
42+
[0, -3.3484, 55.1265, -169.7572, 0],
43+
[0, 7.609997, -125.2876, 385.81189, 0],
44+
],
45+
),
46+
(
47+
np.arange(10),
48+
1,
49+
2,
50+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
51+
0.88,
52+
[
53+
[1, 2.1364, 3.2727, 4.4091, 5.5455, 6.6818, 7.8182, 8.9545, 0, 0],
54+
[0, -1.29, -2.58, -3.87, -5.165, -6.45, -7.74, -9.039, 0, 0],
55+
[0, 2.93, 5.869, 8.084, 11.739, 14.674, 17.608, 20.5437, 0, 0],
56+
],
57+
),
58+
(
4159
np.arange(14),
4260
100,
4361
3,
@@ -58,53 +76,56 @@
5876
2.7960,
5977
],
6078
0.55,
61-
],
62-
[
63-
[-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0],
64-
[0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0],
65-
[0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0],
66-
],
67-
),
68-
(
69-
[np.arange(11), 20, 4, [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5], 0.987],
70-
[
71-
[0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0],
72-
[0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0],
73-
[0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0],
74-
],
75-
),
76-
(
77-
[np.arange(9), 15, 3, [-1, -2, -3, -4, -5, -6, -7, -8, -9], -0.4],
78-
[[-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]],
79-
),
80-
]
81-
82-
83-
@pytest.mark.parametrize("tas", tas)
84-
def test_apply_stretch(tas):
85-
component = ComponentSignal(tas[0][0], tas[0][1], tas[0][2])
86-
component.iq = tas[0][3]
87-
component.stretching_factors[0] = tas[0][4]
79+
[
80+
[-2.9384, -1.9769, 0.9121, 0.6314, 0.8622, -2.4239, -0.2302, 1.9281, 0, 0, 0, 0, 0, 0],
81+
[0, 2.07933, 38.632, 18.3748, 43.07305, -61.557, 26.005, -73.637, 0, 0, 0, 0, 0, 0],
82+
[0, -7.56, -140.480, -66.81, -156.6293, 223.84, -94.564, 267.7734, 0, 0, 0, 0, 0, 0],
83+
],
84+
),
85+
(
86+
np.arange(11),
87+
20,
88+
4,
89+
[0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5],
90+
0.987,
91+
[
92+
[0, 0.2533, 0.5066, 0.7599, 1.0132, 1.2665, 1.5198, 1.7730, 2.0263, 2.2796, 0],
93+
[0, -0.2566, -0.5132, -0.7699, -1.0265, -1.2831, -1.5398, -1.7964, -2.0530, -2.3097, 0],
94+
[0, 0.5200, 1.0400, 1.56005, 2.08007, 2.6000, 3.1201, 3.6401, 4.1601, 4.6801, 0],
95+
],
96+
),
97+
(
98+
np.arange(9),
99+
15,
100+
3,
101+
[-1, -2, -3, -4, -5, -6, -7, -8, -9],
102+
-0.4,
103+
[[-1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]],
104+
),
105+
],
106+
)
107+
def test_apply_stretch(grid, number_of_signals, id_number, iq, stretching_factor, expected):
108+
component = ComponentSignal(grid, number_of_signals, id_number)
109+
component.iq = iq
110+
component.stretching_factors[0] = stretching_factor
88111
actual = component.apply_stretch(0)
89-
expected = tas[1]
90112
np.testing.assert_allclose(actual, expected, rtol=1e-01)
91113

92114

93-
taw = [
94-
([np.arange(5), 2, 0, [0, 1, 2, 3, 4], 0.5], [0, 0.5, 1, 1.5, 2]),
95-
([np.arange(5), 20, 2, [0, -1, -2, -3, -4], 0.25], [0, -0.25, -0.5, -0.75, -1]),
96-
([np.arange(40), 200, 4, np.arange(0, 10, 0.25), 0.3], np.arange(0, 10, 0.25) * 0.3),
97-
([np.arange(1), 10, 2, [10.5, 11.5, -10.5], 0], [0, 0, 0]),
98-
([[-12, -10, -15], 5, 2, [-0.5, -1, -1.2], 0.9], [-0.45, -0.9, -1.08]),
99-
([[-12, -10, -15], 5, 2, [0, 0, 0], 0.9], [0, 0, 0]),
100-
]
101-
102-
103-
@pytest.mark.parametrize("taw", taw)
104-
def test_apply_weight(taw):
105-
component = ComponentSignal(taw[0][0], taw[0][1], taw[0][2])
106-
component.iq = np.array(taw[0][3])
107-
component.weights[0] = taw[0][4]
115+
@pytest.mark.parametrize(
116+
"grid, number_of_signals, id_number, iq, weight, expected",
117+
[
118+
(np.arange(5), 2, 0, [0, 1, 2, 3, 4], 0.5, [0, 0.5, 1, 1.5, 2]),
119+
(np.arange(5), 20, 2, [0, -1, -2, -3, -4], 0.25, [0, -0.25, -0.5, -0.75, -1]),
120+
(np.arange(40), 200, 4, np.arange(0, 10, 0.25), 0.3, np.arange(0, 10, 0.25) * 0.3),
121+
(np.arange(1), 10, 2, [10.5, 11.5, -10.5], 0, [0, 0, 0]),
122+
([-12, -10, -15], 5, 2, [-0.5, -1, -1.2], 0.9, [-0.45, -0.9, -1.08]),
123+
([-12, -10, -15], 5, 2, [0, 0, 0], 0.9, [0, 0, 0]),
124+
],
125+
)
126+
def test_apply_weight(grid, number_of_signals, id_number, iq, weight, expected):
127+
component = ComponentSignal(grid, number_of_signals, id_number)
128+
component.iq = np.array(iq)
129+
component.weights[0] = weight
108130
actual = component.apply_weight(0)
109-
expected = taw[1]
110-
np.testing.assert_allclose(actual, expected, rtol=1e-01)
131+
np.testing.assert_allclose(actual, expected, rtol=1e-04)

tests/test_factorizers.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33

44
from diffpy.snmf.factorizers import lsqnonneg
55

6-
tl = [
7-
([[[1, 0], [1, 0], [0, 1]], [2, 1, 1]], [1.5, 1.0]),
8-
([[[2, 3], [1, 2], [0, 0]], [7, 7, 2]], [0, 2.6923]),
9-
([[[3, 2, 4, 1]], [3.2]], [0, 0, 0.8, 0]),
10-
([[[-0.4, 0], [0, 0], [-9, -18]], [-2, -3, -4.9]], [0.5532, 0]),
11-
([[[-0.1, -0.2], [-0.8, -0.9]], [0, 0]], [0, 0]),
12-
([[[0, 0], [0, 0]], [10, 10]], [0, 0]),
13-
([[[2], [1], [-4], [-0.3]], [6, 4, 0.33, -5]], 0.767188240872451),
14-
]
156

16-
17-
@pytest.mark.parametrize("tl", tl)
18-
def test_lsqnonneg(tl):
19-
actual = lsqnonneg(tl[0][0], tl[0][1])
20-
expected = tl[1]
7+
@pytest.mark.parametrize(
8+
"stretched_component_matrix, target_signal, expected",
9+
[
10+
([[1, 0], [1, 0], [0, 1]], [2, 1, 1], [1.5, 1.0]),
11+
([[2, 3], [1, 2], [0, 0]], [7, 7, 2], [0, 2.6923]),
12+
([[3, 2, 4, 1]], [3.2], [0, 0, 0.8, 0]),
13+
([[-0.4, 0], [0, 0], [-9, -18]], [-2, -3, -4.9], [0.5532, 0]),
14+
([[-0.1, -0.2], [-0.8, -0.9]], [0, 0], [0, 0]),
15+
([[0, 0], [0, 0]], [10, 10], [0, 0]),
16+
([[2], [1], [-4], [-0.3]], [6, 4, 0.33, -5], 0.767188240872451),
17+
],
18+
)
19+
def test_lsqnonneg(stretched_component_matrix, target_signal, expected):
20+
actual = lsqnonneg(stretched_component_matrix, target_signal)
2121
np.testing.assert_array_almost_equal(actual, expected, decimal=4)

tests/test_optimizers.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22

33
from diffpy.snmf.optimizers import get_weights
44

5-
tm = [
6-
([[[1, 0], [0, 1]], [1, 1], [0, 0], [1, 1]], [0, 0]),
7-
([[[1, 0], [0, 1]], [1, 1], -1, 1], [-1, -1]),
8-
([[[1.75, 0], [0, 1.5]], [1, 1.2], -1, 1], [-0.571428571428571, -0.8]),
9-
([[[0.75, 0.2], [0.2, 0.75]], [-0.1, -0.2], -1, 1], [0.066985645933014, 0.248803827751196]),
10-
([[[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, 1, 1], -10, 12], [-1.5, -2, -1.5]),
11-
([[[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, -1, -1], -10, 12], [0, 1, 1]),
12-
([[[4, 0, 0, 0], [0, 3, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]], [-2, -3, -4, -1], 0, 1000], [0.5, 1, 2, 1]),
13-
]
145

15-
16-
@pytest.mark.parametrize("tm", tm)
17-
def test_get_weights(tm):
18-
expected = tm[1]
19-
actual = get_weights(tm[0][0], tm[0][1], tm[0][2], tm[0][3])
6+
@pytest.mark.parametrize(
7+
"stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound, expected",
8+
[
9+
([[1, 0], [0, 1]], [1, 1], 0, 0, [0, 0]),
10+
([[1, 0], [0, 1]], [1, 1], -1, 1, [-1, -1]),
11+
([[1.75, 0], [0, 1.5]], [1, 1.2], -1, 1, [-0.571428571428571, -0.8]),
12+
([[0.75, 0.2], [0.2, 0.75]], [-0.1, -0.2], -1, 1, [0.066985645933014, 0.248803827751196]),
13+
([[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, 1, 1], -10, 12, [-1.5, -2, -1.5]),
14+
([[2, -1, 0], [-1, 2, -1], [0, -1, 2]], [1, -1, -1], -10, 12, [0, 1, 1]),
15+
([[4, 0, 0, 0], [0, 3, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]], [-2, -3, -4, -1], 0, 1000, [0.5, 1, 2, 1]),
16+
],
17+
)
18+
def test_get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound, expected):
19+
actual = get_weights(stretched_component_gram_matrix, linear_coefficient, lower_bound, upper_bound)
2020
assert actual == pytest.approx(expected, rel=1e-4, abs=1e-6)

0 commit comments

Comments
 (0)