From c75cea4792632e6a80780c501adce4c7a7147497 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:38:11 -0500 Subject: [PATCH 1/2] Fix numpy copy as C --- conftest.py | 6 +++--- docs/source/tutorial/algorithm_gcp_opt.ipynb | 2 +- pyttb/gcp/fg_est.py | 4 ++-- pyttb/ktensor.py | 16 ++++++++-------- pyttb/pyttb_utils.py | 5 ++++- pyttb/sptenmat.py | 8 ++++---- pyttb/sptensor.py | 16 ++++++++-------- pyttb/tenmat.py | 14 +++++++------- pyttb/tensor.py | 12 ++++++------ tests/test_ktensor.py | 2 +- tests/test_sptensor.py | 2 +- 11 files changed, 45 insertions(+), 42 deletions(-) diff --git a/conftest.py b/conftest.py index 22417d76..2b532015 100644 --- a/conftest.py +++ b/conftest.py @@ -93,9 +93,9 @@ def sample_tenmat_4way(): # noqa: D103 cdims = np.array([2, 3]) tenmatInstance = ttb.tenmat() tenmatInstance.tshape = tshape - tenmatInstance.rindices = rdims.copy() - tenmatInstance.cindices = cdims.copy() - tenmatInstance.data = data.copy() + tenmatInstance.rindices = rdims.copy("K") + tenmatInstance.cindices = cdims.copy("K") + tenmatInstance.data = data.copy("K") params = { "data": data, "rdims": rdims, diff --git a/docs/source/tutorial/algorithm_gcp_opt.ipynb b/docs/source/tutorial/algorithm_gcp_opt.ipynb index f56fac50..1eebcea1 100644 --- a/docs/source/tutorial/algorithm_gcp_opt.ipynb +++ b/docs/source/tutorial/algorithm_gcp_opt.ipynb @@ -625,7 +625,7 @@ "outputs": [], "source": [ "# Use X and initial guess from the previous Poisson example\n", - "X_ztp = X.copy()\n", + "X_ztp = X.copy(\"K\")\n", "\n", "# Set seed for reproducibility\n", "np.random.seed(0)\n", diff --git a/pyttb/gcp/fg_est.py b/pyttb/gcp/fg_est.py index 4a3bb48c..4127fc0c 100644 --- a/pyttb/gcp/fg_est.py +++ b/pyttb/gcp/fg_est.py @@ -170,13 +170,13 @@ def estimate_helper( # After this pass, Zexp[k] = Hadarmard product of Uexp[0] through # Uexp[k-1] for k = 1,...,ndim Zexp = [np.empty(())] * ndim - Zexp[1] = Uexp[0].copy() + Zexp[1] = Uexp[0].copy("K") for k in range(2, ndim): Zexp[k] = Zexp[k - 1] * Uexp[k - 1] # After this pass, Zexp[k] = Hadarmard product of Uexcp[0] through # Uexp[d], except Uexp[k] for k = 0, ..., ndim - Zexp[0] = Uexp[ndim - 1].copy() + Zexp[0] = Uexp[ndim - 1].copy("K") for k in range(ndim - 2, 0, -1): Zexp[k] *= Zexp[0] Zexp[0] *= Uexp[k] diff --git a/pyttb/ktensor.py b/pyttb/ktensor.py index e4518391..9ee10c60 100644 --- a/pyttb/ktensor.py +++ b/pyttb/ktensor.py @@ -408,7 +408,7 @@ def from_vector(cls, data: np.ndarray, shape: Shape, contains_weights: bool): # extract weights from input vector if present if contains_weights: - weights = data[0:num_components].copy() + weights = data[0:num_components].copy("K") shift = num_components else: weights = np.ones(num_components) @@ -421,7 +421,7 @@ def from_vector(cls, data: np.ndarray, shape: Shape, contains_weights: bool): mend = num_components * sum(shape[0 : n + 1]) + shift # the following will match MATLAB output factor_matrix = np.reshape( - data[mstart:mend].copy(), (shape_n, num_components), order="F" + data[mstart:mend].copy("K"), (shape_n, num_components), order="F" ) factor_matrices.append(factor_matrix) @@ -860,7 +860,7 @@ def fixsigns(self, other: ktensor | None = None) -> ktensor: # noqa: PLR0912 # Sort the sign scores. sort_idx = np.argsort(sgn_score) - sort_sgn_score = sgn_score.copy()[sort_idx] + sort_sgn_score = sgn_score.copy("K")[sort_idx] # Determine the number of scores that should be flipped. breakpt = np.nonzero(sort_sgn_score < 0)[-1] @@ -1832,7 +1832,7 @@ def symmetrize(self) -> ktensor: weights[j] = -weights[j] V[:, [j]] = -V[:, [j]] - return ttb.ktensor([V.copy() for i in range(K.ndims)], weights) + return ttb.ktensor([V.copy("K") for i in range(K.ndims)], weights) def tolist(self, mode: int | None = None) -> list[np.ndarray]: """Convert :class:`pyttb.ktensor` to a list of factor matrices. @@ -2109,7 +2109,7 @@ def ttv( remdims = np.setdiff1d(range(self.ndims), dims) # Collapse dimensions that are being multiplied out - new_weights = self.weights.copy() + new_weights = self.weights.copy("K") for i, dim in enumerate(dims): new_weights = new_weights * ( self.factor_matrices[dim].T @ vector[vidx[i]].squeeze() @@ -2235,7 +2235,7 @@ def update(self, modes: OneDArray, data: np.ndarray) -> ktensor: endloc = loc + self.ncomponents if len(data) < endloc: assert False, "Data is too short" - self.weights = data[loc:endloc].copy() + self.weights = data[loc:endloc].copy("K") loc = endloc elif k < self.ndims: # update factor matrix @@ -2243,7 +2243,7 @@ def update(self, modes: OneDArray, data: np.ndarray) -> ktensor: if len(data) < endloc: assert False, "Data is too short" self.factor_matrices[k] = np.reshape( - data[loc:endloc].copy(), + data[loc:endloc].copy("K"), (self.shape[k], self.ncomponents), order=self.order, ) @@ -2385,7 +2385,7 @@ def line_plot(v, ax): self.normalize(normtype=norm, sort=True) # compute factor weights (and optionally normalize) - weights = self.weights.copy() + weights = self.weights.copy("K") weight_labels = [format(w, ".2e") for w in weights] if rel_weights: weights /= np.max(weights) diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py index 04cadd67..8db6293f 100644 --- a/pyttb/pyttb_utils.py +++ b/pyttb/pyttb_utils.py @@ -1020,7 +1020,10 @@ def to_memory_order( if copy: # This could be slightly optimized # in worst case two copies occur - array = array.copy() + if isinstance(array, np.ndarray): + array = array.copy("K") + else: + array = array.copy() if isinstance(array, sparse.coo_matrix): return array if order == "F": diff --git a/pyttb/sptenmat.py b/pyttb/sptenmat.py index 9b9911b7..fc25d9dc 100644 --- a/pyttb/sptenmat.py +++ b/pyttb/sptenmat.py @@ -112,9 +112,9 @@ def __init__( # noqa: PLR0913 rdims, cdims = gather_wrap_dims(n, rdims, cdims) # if rdims or cdims is empty, hstack will output an array of float not int if rdims.size == 0: - dims = cdims.copy() + dims = cdims.copy("K") elif cdims.size == 0: - dims = rdims.copy() + dims = rdims.copy("K") else: dims = np.hstack([rdims, cdims], dtype=int) assert len(dims) == n and (alldims == np.sort(dims)).all(), ( @@ -154,8 +154,8 @@ def __init__( # noqa: PLR0913 newvals = newvals[:, None] self.tshape = tshape - self.rdims = rdims.copy().astype(int) - self.cdims = cdims.copy().astype(int) + self.rdims = rdims.copy("K").astype(int) + self.cdims = cdims.copy("K").astype(int) self.subs = newsubs self.vals = newvals else: diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py index d1c7574e..7a1d503b 100644 --- a/pyttb/sptensor.py +++ b/pyttb/sptensor.py @@ -166,8 +166,8 @@ def __init__( raise ValueError("Values should be a column vector") if copy: - self.subs = subs.copy() - self.vals = vals.copy() + self.subs = subs.copy("K") + self.vals = vals.copy("K") self.shape = shape return self.subs = subs @@ -869,7 +869,7 @@ def to_sptenmat( return ttb.sptenmat( np.hstack([ridx, cidx], dtype=int), - self.vals.copy(), + self.vals.copy("K"), rdims.astype(int), cdims.astype(int), self.shape, @@ -1524,7 +1524,7 @@ def ones(self) -> sptensor: [0, 1] = 1.0 [1, 0] = 1.0 """ - oneVals = self.vals.copy() + oneVals = self.vals.copy("K") oneVals.fill(1) return ttb.sptensor(self.subs, oneVals, self.shape) @@ -2003,8 +2003,8 @@ def ttv( assert False, "Multiplicand is wrong size" # Multiply each value by the appropriate elements of the appropriate vector - newvals = self.vals.copy() - subs = self.subs.copy() + newvals = self.vals.copy("K") + subs = self.subs.copy("K") if subs.size == 0: # No nonzeros in tensor newsubs = np.array([], dtype=int) else: @@ -2117,11 +2117,11 @@ def __getitem__(self, item): # noqa: PLR0912, PLR0915 loc = self.subdims(region) # Handle slicing an sptensor with no entries if self.subs.size == 0: - subs = self.subs.copy() + subs = self.subs.copy("K") else: subs = self.subs[loc, :] if self.vals.size == 0: - vals = self.vals.copy() + vals = self.vals.copy("K") else: vals = self.vals[loc] diff --git a/pyttb/tenmat.py b/pyttb/tenmat.py index a1abcd21..467fd928 100644 --- a/pyttb/tenmat.py +++ b/pyttb/tenmat.py @@ -119,7 +119,7 @@ def __init__( # noqa: PLR0912 assert False, "tshape must be specified when data is 1d array." else: # make data a 2d array with shape (1, data.shape[0]), i.e., a row vector - data = np.reshape(data.copy(), (1, data.shape[0]), order=self.order) + data = np.reshape(data.copy("K"), (1, data.shape[0]), order=self.order) if len(data.shape) != 2: raise ValueError( @@ -152,9 +152,9 @@ def __init__( # noqa: PLR0912 # if rdims or cdims is empty, hstack will output an array of float not int if rdims.size == 0: - dims = cdims.copy() + dims = cdims.copy("K") elif cdims.size == 0: - dims = rdims.copy() + dims = rdims.copy("K") else: dims = np.hstack([rdims, cdims]) if not len(dims) == n or not (alldims == np.sort(dims)).all(): @@ -164,8 +164,8 @@ def __init__( # noqa: PLR0912 ) self.tshape = tshape - self.rindices = rdims.copy() - self.cindices = cdims.copy() + self.rindices = rdims.copy("K") + self.cindices = cdims.copy("K") if not copy and not self._matches_order(data): logging.warning( @@ -271,7 +271,7 @@ def to_tensor(self, copy: bool = True) -> ttb.tensor: order = np.hstack([self.rindices, self.cindices]) data = self.data if copy: - data = self.data.copy() + data = self.data.copy("K") data = np.reshape(data, np.array(shape)[order], order=self.order) if order.size > 1: if not copy: @@ -347,7 +347,7 @@ def double(self, immutable: bool = False) -> np.ndarray: if immutable: double.flags.writeable = False elif np.shares_memory(double, self.data): - double = double.copy() + double = double.copy("K") return double @property diff --git a/pyttb/tensor.py b/pyttb/tensor.py index 38cca47a..cfb4b176 100644 --- a/pyttb/tensor.py +++ b/pyttb/tensor.py @@ -766,9 +766,9 @@ def to_tenmat( rdims, cdims = gather_wrap_dims(n, rdims, cdims, cdims_cyclic) # if rdims or cdims is empty, hstack will output an array of float not int if rdims.size == 0: - dims = cdims.copy() + dims = cdims.copy("K") elif cdims.size == 0: - dims = rdims.copy() + dims = rdims.copy("K") else: dims = np.hstack([rdims, cdims]) if not len(dims) == n or not (alldims == np.sort(dims)).all(): @@ -1493,7 +1493,7 @@ def symmetrize( # noqa: PLR0912,PLR0915 if len(grps.shape) == 1: grps = np.array([grps]) - data = self.data.copy() + data = self.data.copy("K") # Use default newer faster version if version is None: @@ -1765,7 +1765,7 @@ def ttt( selfshape = tuple(np.array(self.shape)[selfdims]) if otherdims is None: - otherdims = selfdims.copy() + otherdims = selfdims.copy("K") elif isinstance(otherdims, int): otherdims = np.array([otherdims]) othershape = tuple(np.array(other.shape)[otherdims]) @@ -1852,7 +1852,7 @@ def ttv( assert False, "Multiplicand is wrong size" # Extract the data - c = self.data.copy() + c = self.data.copy("K") # Permute it so that the dimensions we're working with come last remdims = np.setdiff1d(np.arange(0, self.ndims), dims) @@ -1932,7 +1932,7 @@ def ttsv( dnew = skip_dim + 1 # Number of modes in result drem = d - dnew # Number of modes multiplied out - y = self.data.copy() + y = self.data.copy(order=self.order) for i in range(drem, 0, -1): yy = np.reshape(y, (sz ** (dnew + i - 1), sz), order=self.order) y = yy.dot(vector) diff --git a/tests/test_ktensor.py b/tests/test_ktensor.py index 40977118..2e44b5dd 100644 --- a/tests/test_ktensor.py +++ b/tests/test_ktensor.py @@ -129,7 +129,7 @@ def test_ktensor_from_vector(sample_ktensor_3way): assert np.array_equal(K1.factor_matrices[2], data["factor_matrices"][2]) # data as a row vector will work, but will be transposed - transposed_data = data["vector"].copy().reshape((1, len(data["vector"]))) + transposed_data = data["vector"].copy("K").reshape((1, len(data["vector"]))) K2 = ttb.ktensor.from_vector(transposed_data, data["shape"], False) assert np.array_equal(K2.weights, np.ones((2,))) assert np.array_equal(K2.factor_matrices[0], data["factor_matrices"][0]) diff --git a/tests/test_sptensor.py b/tests/test_sptensor.py index 3d7eeea7..86de5e4b 100644 --- a/tests/test_sptensor.py +++ b/tests/test_sptensor.py @@ -148,7 +148,7 @@ def test_sptensor_initialization_from_aggregator(sample_sptensor): ) assert "More subscripts than specified by shape" in str(excinfo) - badSubs = subs.copy() + badSubs = subs.copy("K") badSubs[0, 0] = 11 with pytest.raises(AssertionError) as excinfo: ttb.sptensor.from_aggregator(badSubs, vals, shape) From 325b7ff0fbfeb90b9e358ecb994d18db6777a9c2 Mon Sep 17 00:00:00 2001 From: Nick Johnson <24689722+ntjohnson1@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:42:11 -0500 Subject: [PATCH 2/2] Oops that's not an ndarray --- docs/source/tutorial/algorithm_gcp_opt.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/tutorial/algorithm_gcp_opt.ipynb b/docs/source/tutorial/algorithm_gcp_opt.ipynb index 1eebcea1..f56fac50 100644 --- a/docs/source/tutorial/algorithm_gcp_opt.ipynb +++ b/docs/source/tutorial/algorithm_gcp_opt.ipynb @@ -625,7 +625,7 @@ "outputs": [], "source": [ "# Use X and initial guess from the previous Poisson example\n", - "X_ztp = X.copy(\"K\")\n", + "X_ztp = X.copy()\n", "\n", "# Set seed for reproducibility\n", "np.random.seed(0)\n",