Repository: scipy/scipy
File: scipy/sparse/_compressed.py (Python)

Code before:
            csr_sample_offsets(M, N, self.indptr, self.indices, n_samples,
                               i, j, offsets)
        if -1 not in offsets:
            # only affects existing non-zero cells
            self.data[offsets] = x
            return

        is_existing = (offsets >= 0)
        is_new = ~is_existing
        N_new = is_new.sum()

        # Boundary between csc and convert to coo
        # The value 0.001 is justified in gh-19962#issuecomment-1920499678
        if N_new < self.nnz * 0.001:
            # replace existing entries
            self.data[offsets[is_existing]] = x[is_existing]
            # create new entries
            self._insert_many(i[is_new], j[is_new], x[is_new])
        else:
            # convert to coo for _set_diag
            coo = self.tocoo()
            coo._setdiag(values, k)
            arrays = coo._coo_to_compressed(self._swap)
            self.indptr, self.indices, self.data, _ = arrays
            # Sort the indices (like in _insert_many)
            if self.has_sorted_indices:
                self.has_sorted_indices = False  # force a sort
                self.sort_indices()

    def _prepare_indices(self, i, j):
        M, N = self._swap(self._shape_as_2d)

        def check_bounds(indices, bound):
            idx = indices.max()
            if idx >= bound:
                raise IndexError(f'index ({idx}) out of range (>= {bound})')
            idx = indices.min()
            if idx < -bound:
                raise IndexError(f'index ({idx}) out of range (< -{bound})')

        i = np.atleast_1d(np.asarray(i, dtype=self.indices.dtype)).ravel()
        j = np.atleast_1d(np.asarray(j, dtype=self.indices.dtype)).ravel()
        check_bounds(i, M)
        check_bounds(j, N)
        return i, j, M, N

    def _set_many(self, i, j, x):
        """Sets value at each (i, j) to x

        Here (i,j) index major and minor respectively, and must not contain
        duplicate entries.

Review comment (performance):
Be careful here -- the attribute `self.has_sorted_indices` is a property. If it hasn't been accessed before, this will compute whether the indices are sorted (right now!) rather than indicating that the indices were already sorted before the changes. So, an unsorted input that hasn't got the cache set yet will lead to being sorted here. And a sorted input that hasn't got the cache set yet will check for sorted indices AFTER the setdiag has been done and then sort the indices. I think we want to know the sorted_indices state BEFORE the setdiag has been done to determine whether it needs it after the set is done.

The approach used in `insert_many` is to store the flag value before setting anything. That bool value determines whether a sort is done after setting the values.
```python
    do_sort = self.has_sorted_indices

    ... set stuff ...

    if do_sort:
        self.has_sorted_indices = False  # force a sort
        self.sort_indices()
```

Code after:
            csr_sample_offsets(M, N, self.indptr, self.indices, n_samples,
                               i, j, offsets)
        if -1 not in offsets:
            # only affects existing non-zero cells
            self.data[offsets] = x
            return

        is_existing = (offsets >= 0)
        N_new = len(is_existing) - np.count_nonzero(is_existing)

        # Boundary between csc and convert to coo
        # The value 0.001 is justified in gh-19962#issuecomment-1920499678
        if N_new < self.nnz * 0.001:
            # replace existing entries
            self.data[offsets[is_existing]] = x[is_existing]
            # create new entries
            is_new = np.logical_not(is_existing, out=is_existing)
            del is_existing
            self._insert_many(i[is_new], j[is_new], x[is_new])
        else:
            # convert to coo for _set_diag
            do_sort = self.has_sorted_indices
            coo = self.tocoo()
            coo._setdiag(values, k)
            arrays = coo._coo_to_compressed(self._swap)
            self.indptr, self.indices, self.data, _ = arrays
            # Sort the indices (like in _insert_many)
            if do_sort:
                self.has_sorted_indices = False  # force a sort
                self.sort_indices()

    def _prepare_indices(self, i, j):
        M, N = self._swap(self._shape_as_2d)

        def check_bounds(indices, bound):
            idx = indices.max()
            if idx >= bound:
                raise IndexError(f'index ({idx}) out of range (>= {bound})')
            idx = indices.min()
            if idx < -bound:
                raise IndexError(f'index ({idx}) out of range (< -{bound})')

        i = np.atleast_1d(np.asarray(i, dtype=self.indices.dtype)).ravel()
        j = np.atleast_1d(np.asarray(j, dtype=self.indices.dtype)).ravel()
        check_bounds(i, M)
        check_bounds(j, N)
        return i, j, M, N

    def _set_many(self, i, j, x):
        """Sets value at each (i, j) to x