Skip to content

Commit a508707

Browse files
authored
Nick/fortran ordering (#373)
* Internal: Try new link checker * Internal: Add codespell and fix typos. * Internal: See if codespell precommit finds config. * Internal: Found config. Now enable reading it * MATLAB: Add initial support for more matlab support. * DEV: Updating contributing doc for more details on adding a tutorial * Fix internal calls to avoid fortran warnings in tutorials * Update one tutorial with "F" order to avoid warnings. * Check doctests for Fortran ordering warnings * Add tests to verify dense tensor ops return arrays with the correct memory layout: * Only required fixing mttkrp and double * Add a note about memory layout at top level high visibility areas * Add small utility for memory layout management * TTENSOR: Propagate order * Assumes core ordering is correct * KTENSOR: Propagate order * TENMAT: Propagate order * SPTENSOR: Propagate order * Ensure fortran order for data returned but not index arrays * SPTENMAT: Propagate order * Doesn't return arrays so minimal changes needed? * SUMTENSOR: Propagate order * Mostly calls the ops from its parts * Plumb through order printout * Remove global warnings filter * See if anything blows up Closes #368
1 parent 7e97fea commit a508707

24 files changed

+870
-406
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ CP_ALS:
5656
Final f = 7.508253e-01
5757
```
5858

59+
### Memory layout
60+
For historical reasons we use Fortran memory layouts, where numpy by default uses C.
61+
This is relevant for indexing. In the future we hope to extend support for both.
62+
```python
63+
>>> import numpy as np
64+
>>> c_order = np.arange(8).reshape((2,2,2))
65+
>>> f_order = np.arange(8).reshape((2,2,2), order="F")
66+
>>> print(c_order[0,1,1])
67+
3
68+
>>> print(f_order[0,1,1])
69+
6
70+
```
71+
5972
<!-- markdown-link-check-disable -->
6073
### Getting Help
6174
- [Documentation](https://pyttb.readthedocs.io)

conftest.py

+131
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
# U.S. Government retains certain rights in this software.
55

66
import numpy
7+
import numpy as np
78

89
# content of conftest.py
910
import pytest
1011

1112
import pyttb
13+
import pyttb as ttb
1214

1315

1416
@pytest.fixture(autouse=True)
@@ -17,6 +19,12 @@ def add_packages(doctest_namespace): # noqa: D103
1719
doctest_namespace["ttb"] = pyttb
1820

1921

22+
@pytest.fixture(params=[{"order": "F"}, {"order": "C"}])
23+
def memory_layout(request):
24+
"""Test C and F memory layouts."""
25+
return request.param
26+
27+
2028
def pytest_addoption(parser): # noqa: D103
2129
parser.addoption(
2230
"--packaging",
@@ -30,3 +38,126 @@ def pytest_addoption(parser): # noqa: D103
3038
def pytest_configure(config): # noqa: D103
3139
if not config.option.packaging:
3240
config.option.markexpr = "not packaging"
41+
42+
43+
@pytest.fixture()
44+
def sample_tensor_2way(): # noqa: D103
45+
data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
46+
shape = (2, 3)
47+
params = {"data": data, "shape": shape}
48+
tensorInstance = ttb.tensor(data, shape)
49+
return params, tensorInstance
50+
51+
52+
@pytest.fixture()
53+
def sample_tensor_3way(): # noqa: D103
54+
data = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0])
55+
shape = (2, 3, 2)
56+
params = {"data": np.reshape(data, np.array(shape), order="F"), "shape": shape}
57+
tensorInstance = ttb.tensor(data, shape)
58+
return params, tensorInstance
59+
60+
61+
@pytest.fixture()
62+
def sample_ndarray_1way(): # noqa: D103
63+
shape = (16,)
64+
ndarrayInstance = np.reshape(np.arange(1, 17), shape, order="F")
65+
params = {"data": ndarrayInstance, "shape": shape}
66+
return params, ndarrayInstance
67+
68+
69+
@pytest.fixture()
70+
def sample_ndarray_2way(): # noqa: D103
71+
shape = (4, 4)
72+
ndarrayInstance = np.reshape(np.arange(1, 17), shape, order="F")
73+
params = {"data": ndarrayInstance, "shape": shape}
74+
return params, ndarrayInstance
75+
76+
77+
@pytest.fixture()
78+
def sample_ndarray_4way(): # noqa: D103
79+
shape = (2, 2, 2, 2)
80+
ndarrayInstance = np.reshape(np.arange(1, 17), shape, order="F")
81+
params = {"data": ndarrayInstance, "shape": shape}
82+
return params, ndarrayInstance
83+
84+
85+
@pytest.fixture()
86+
def sample_tenmat_4way(): # noqa: D103
87+
shape = (4, 4)
88+
data = np.reshape(np.arange(1, 17), shape, order="F")
89+
tshape = (2, 2, 2, 2)
90+
rdims = np.array([0, 1])
91+
cdims = np.array([2, 3])
92+
tenmatInstance = ttb.tenmat()
93+
tenmatInstance.tshape = tshape
94+
tenmatInstance.rindices = rdims.copy()
95+
tenmatInstance.cindices = cdims.copy()
96+
tenmatInstance.data = data.copy()
97+
params = {
98+
"data": data,
99+
"rdims": rdims,
100+
"cdims": cdims,
101+
"tshape": tshape,
102+
"shape": shape,
103+
}
104+
return params, tenmatInstance
105+
106+
107+
@pytest.fixture()
108+
def sample_tensor_4way(): # noqa: D103
109+
data = np.arange(1, 17)
110+
shape = (2, 2, 2, 2)
111+
params = {"data": np.reshape(data, np.array(shape), order="F"), "shape": shape}
112+
tensorInstance = ttb.tensor(data, shape)
113+
return params, tensorInstance
114+
115+
116+
@pytest.fixture()
117+
def sample_ktensor_2way(): # noqa: D103
118+
weights = np.array([1.0, 2.0])
119+
fm0 = np.array([[1.0, 2.0], [3.0, 4.0]])
120+
fm1 = np.array([[5.0, 6.0], [7.0, 8.0]])
121+
factor_matrices = [fm0, fm1]
122+
data = {"weights": weights, "factor_matrices": factor_matrices}
123+
ktensorInstance = ttb.ktensor(factor_matrices, weights)
124+
return data, ktensorInstance
125+
126+
127+
@pytest.fixture()
128+
def sample_ktensor_3way(): # noqa: D103
129+
rank = 2
130+
shape = (2, 3, 4)
131+
vector = np.arange(1, rank * sum(shape) + 1).astype(float)
132+
weights = 2 * np.ones(rank).astype(float)
133+
vector_with_weights = np.concatenate((weights, vector), axis=0)
134+
# vector_with_weights = vector_with_weights.reshape((len(vector_with_weights), 1))
135+
# ground truth
136+
fm0 = np.array([[1.0, 3.0], [2.0, 4.0]])
137+
fm1 = np.array([[5.0, 8.0], [6.0, 9.0], [7.0, 10.0]])
138+
fm2 = np.array([[11.0, 15.0], [12.0, 16.0], [13.0, 17.0], [14.0, 18.0]])
139+
factor_matrices = [fm0, fm1, fm2]
140+
data = {
141+
"weights": weights,
142+
"factor_matrices": factor_matrices,
143+
"vector": vector,
144+
"vector_with_weights": vector_with_weights,
145+
"shape": shape,
146+
}
147+
ktensorInstance = ttb.ktensor(factor_matrices, weights)
148+
return data, ktensorInstance
149+
150+
151+
@pytest.fixture()
152+
def sample_ktensor_symmetric(): # noqa: D103
153+
weights = np.array([1.0, 1.0])
154+
fm0 = np.array(
155+
[[2.340431417384394, 4.951967353890655], [4.596069112758807, 8.012451489774961]]
156+
)
157+
fm1 = np.array(
158+
[[2.340431417384394, 4.951967353890655], [4.596069112758807, 8.012451489774961]]
159+
)
160+
factor_matrices = [fm0, fm1]
161+
data = {"weights": weights, "factor_matrices": factor_matrices}
162+
ktensorInstance = ttb.ktensor(factor_matrices, weights)
163+
return data, ktensorInstance

docs/source/index.rst

+12
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ algorithms for computing low-rank tensor models.
3939

4040
Getting Started
4141
===============
42+
For historical reasons we use Fortran memory layouts, where numpy by default uses C.
43+
This is relevant for indexing. In the future we hope to extend support for both.
44+
45+
.. code-block:: python
46+
47+
>>> import numpy as np
48+
>>> c_order = np.arange(8).reshape((2,2,2))
49+
>>> f_order = np.arange(8).reshape((2,2,2), order="F")
50+
>>> print(c_order[0,1,1])
51+
3
52+
>>> print(f_order[0,1,1])
53+
6
4254
4355
.. toctree::
4456
:maxdepth: 1

docs/source/tutorial/class_tensor.ipynb

+5-3
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@
857857
"outputs": [],
858858
"source": [
859859
"np.random.seed(0)\n",
860-
"A = ttb.tensor(np.floor(3 * np.random.rand(2, 2, 3))) # Generate some data.\n",
860+
"A = ttb.tensor(np.floor(3 * np.random.rand(2, 2, 3), order=\"F\")) # Generate some data.\n",
861861
"A.tenfun(lambda x: x + 1) # Increment every element of A by one."
862862
]
863863
},
@@ -882,12 +882,14 @@
882882
"outputs": [],
883883
"source": [
884884
"np.random.seed(0)\n",
885-
"C = ttb.tensor(np.floor(5 * np.random.rand(2, 2, 3))) # Create another tensor.\n",
885+
"C = ttb.tensor(\n",
886+
" np.floor(5 * np.random.rand(2, 2, 3), order=\"F\")\n",
887+
") # Create another tensor.\n",
886888
"\n",
887889
"\n",
888890
"def elementwise_mean(X):\n",
889891
" # finding mean for the columns\n",
890-
" return np.floor(np.mean(X, axis=0))\n",
892+
" return np.floor(np.mean(X, axis=0), order=\"F\")\n",
891893
"\n",
892894
"\n",
893895
"A.tenfun(elementwise_mean, B, C) # Elementwise means for A, B, and C."

pyttb/__init__.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ def ignore_warnings(ignore=True):
4040
warnings.simplefilter("default")
4141

4242

43-
ignore_warnings(True)
44-
45-
# Ruff inspection rules are too strict heres
43+
# Ruff inspection rules are too strict here
4644
__all__ = [ # noqa: PLE0604
4745
cp_als.__name__,
4846
cp_apr.__name__,

pyttb/cp_apr.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,9 @@ def tt_cp_apr_pdnr( # noqa: PLR0912,PLR0913,PLR0915
521521
if isinstance(input_tensor, ttb.tensor) and isSparse is False:
522522
# Data is not a sparse tensor.
523523
Pi = tt_calcpi_prowsubprob(input_tensor, M, rank, n, N, isSparse)
524-
X_mat = input_tensor.to_tenmat(np.array([n]), copy=False).data
524+
X_mat = input_tensor.to_tenmat(
525+
np.array([n], order=input_tensor.order), copy=False
526+
).data
525527

526528
num_rows = M.factor_matrices[n].shape[0]
527529
isRowNOTconverged = np.zeros((num_rows,))
@@ -876,7 +878,9 @@ def tt_cp_apr_pqnr( # noqa: PLR0912,PLR0913,PLR0915
876878
if not isinstance(input_tensor, ttb.sptensor) and not isSparse:
877879
# Data is not a sparse tensor.
878880
Pi = tt_calcpi_prowsubprob(input_tensor, M, rank, n, N, isSparse)
879-
X_mat = input_tensor.to_tenmat(np.array([n]), copy=False).data
881+
X_mat = input_tensor.to_tenmat(
882+
np.array([n], order=input_tensor.order), copy=False
883+
).data
880884

881885
num_rows = M.factor_matrices[n].shape[0]
882886
isRowNOTconverged = np.zeros((num_rows,))
@@ -1772,7 +1776,7 @@ def calculate_phi( # noqa: PLR0913
17721776
)
17731777
Phi[:, r] = Yr
17741778
else:
1775-
Xn = Data.to_tenmat(np.array([factorIndex]), copy=False).data
1779+
Xn = Data.to_tenmat(np.array([factorIndex], order=Data.order), copy=False).data
17761780
V = Model.factor_matrices[factorIndex].dot(Pi.transpose())
17771781
W = Xn / np.maximum(V, epsilon)
17781782
Y = W.dot(Pi)
@@ -1817,8 +1821,8 @@ def tt_loglikelihood(
18171821
np.sum(Data.vals * np.log(np.sum(A, axis=1))[:, None])
18181822
- np.sum(Model.factor_matrices[0])
18191823
)
1820-
dX = Data.to_tenmat(np.array([1]), copy=False).data
1821-
dM = Model.to_tenmat(np.array([1]), copy=False).data
1824+
dX = Data.to_tenmat(np.array([1], order=Data.order), copy=False).data
1825+
dM = Model.to_tenmat(np.array([1], order=Model.order), copy=False).data
18221826
f = 0
18231827
for i in range(dX.shape[0]):
18241828
for j in range(dX.shape[1]):

pyttb/ktensor.py

+33-9
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
np_to_python,
3838
parse_one_d,
3939
parse_shape,
40+
to_memory_order,
4041
tt_dimscheck,
4142
tt_ind2sub,
4243
)
@@ -74,7 +75,7 @@ class ktensor:
7475

7576
__slots__ = ("weights", "factor_matrices")
7677

77-
def __init__(
78+
def __init__( # noqa: PLR0912
7879
self,
7980
factor_matrices: Optional[Sequence[np.ndarray]] = None,
8081
weights: Optional[np.ndarray] = None,
@@ -147,7 +148,7 @@ def __init__(
147148

148149
# Empty constructor
149150
if factor_matrices is None and weights is None:
150-
self.weights = np.array([])
151+
self.weights = np.array([], order=self.order)
151152
self.factor_matrices: List[np.ndarray] = []
152153
return
153154

@@ -183,17 +184,30 @@ def __init__(
183184
)
184185
# make copy or use reference
185186
if copy:
186-
self.weights = weights.copy()
187+
self.weights = weights.copy(self.order)
187188
else:
188-
self.weights = weights
189+
if not self._matches_order(weights):
190+
logging.warning(
191+
f"Selected no copy, but input weights aren't {self.order} "
192+
"ordered so must copy."
193+
)
194+
self.weights = to_memory_order(weights, self.order)
189195
else:
190196
# create weights if not provided
191-
self.weights = np.ones(num_components)
197+
self.weights = np.ones(num_components, order=self.order)
192198

193199
# process factor_matrices
194200
if copy:
195-
self.factor_matrices = [fm.copy() for fm in factor_matrices]
201+
self.factor_matrices = [fm.copy(order=self.order) for fm in factor_matrices]
196202
else:
203+
if not all(self._matches_order(factor) for factor in factor_matrices):
204+
logging.warning(
205+
"Selected no copy, but input factor matrices aren't "
206+
f"{self.order} ordered so must copy."
207+
)
208+
factor_matrices = [
209+
to_memory_order(fm, self.order, copy=True) for fm in factor_matrices
210+
]
197211
if not isinstance(factor_matrices, list):
198212
logging.warning("Must provide factor matrices as list to avoid copy")
199213
factor_matrices = list(factor_matrices)
@@ -419,6 +433,14 @@ def order(self) -> Literal["F"]:
419433
"""Return the data layout of the underlying storage."""
420434
return "F"
421435

436+
def _matches_order(self, array: np.ndarray) -> bool:
437+
"""Check if provided array matches tensor memory layout."""
438+
if array.flags["C_CONTIGUOUS"] and self.order == "C":
439+
return True
440+
if array.flags["F_CONTIGUOUS"] and self.order == "F":
441+
return True
442+
return False
443+
422444
def arrange(
423445
self,
424446
weight_factor: Optional[int] = None,
@@ -924,7 +946,9 @@ def min_split_dims(dims: Tuple[int, ...]):
924946
data = (
925947
ttb.khatrirao(*self.factor_matrices[:i_split], reverse=True) * self.weights
926948
) @ ttb.khatrirao(*self.factor_matrices[i_split:], reverse=True).T
927-
return ttb.tensor(data, self.shape, copy=False)
949+
# Copy needed to ensure F order. Transpose above means both elements are
950+
# different layout. If originally in C order can save on this copy.
951+
return ttb.tensor(data, self.shape, copy=True)
928952

929953
def to_tenmat(
930954
self,
@@ -1237,7 +1261,7 @@ def mttkrp(
12371261
W = W * (self.factor_matrices[i].T @ U[i])
12381262

12391263
# Find each column of answer by multiplying columns of X.u{n} with weights
1240-
return self.factor_matrices[n] @ W
1264+
return to_memory_order(self.factor_matrices[n] @ W, self.order)
12411265

12421266
@property
12431267
def ncomponents(self) -> int:
@@ -1678,7 +1702,7 @@ def score(
16781702
# Compute all possible vector-vector congruences.
16791703

16801704
# Compute every pair for each mode
1681-
Cbig = ttb.tensor.from_function(np.zeros, (RA, RB, N))
1705+
Cbig = ttb.tensor(np.zeros((RA, RB, N), order=self.order))
16821706
for n in range(N):
16831707
Cbig[:, :, n] = np.abs(A.factor_matrices[n].T @ B.factor_matrices[n])
16841708

0 commit comments

Comments
 (0)