Repository: scipy/scipy

Issue:
DOC: lobpcg examples improvement
### Issue with current documentation:

https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.lobpcg.html Examples includes
from scipy.sparse.linalg import LinearOperator, aslinearoperator
which is no longer use in the current development version.

### Idea or request for content:

Either remove the unused import or better yet include examples that use it

### Additional context (e.g. screenshots, GIFs)

_No response_


Discussion:
Hey @lobpcg, I noticed issue #18881 regarding the documentation for scipy.sparse.linalg.lobpcg and I'd like to contribute by providing some example that use LinearOperator, aslinearoperator. Let me know if you're open to it and if I can proceed with submitting a pull request to address this issue.
@anudeepadi - of course please feel free evidently. Ping me after you finish if you would like me to review your PR. 
Hey @lobpcg,

I worked on the lobpcg examples improvement and came up with these examples:

**Using `aslinearoperator`:**
```python
from scipy.sparse.linalg import lobpcg, aslinearoperator
import numpy as np

n = 100
vals = np.diag(np.arange(1, n+1, dtype=np.float64))
A = aslinearoperator(vals)
X = np.random.default_rng().normal(size=(n, 3))
eigenvalues, _ = lobpcg(A, X, largest=False, maxiter=80)
print(eigenvalues)  # Output: [4., 5., 6.]
```

**Using `LinearOperator` with a callable:**
```python
from scipy.sparse.linalg import lobpcg, LinearOperator
import numpy as np

n = 100
vals = np.arange(1, n+1, dtype=np.float64)
def matvec(X):
    return vals[:, np.newaxis] * X
A = LinearOperator((n, n), matvec=matvec, dtype='float64')
X = np.random.default_rng().normal(size=(n, 3))
eigenvalues, _ = lobpcg(A, X, largest=False, maxiter=80)
print(eigenvalues)  # Output: [4., 5., 6.]
```

Let me know if these are okay for the issue. If so, I'll raise a PR. Sorry for the late submission, and thanks for checking this out!
@anudeepadi - in the first example, use sparse diags. In the second one, add matmat with a comment that LOBPCG requires matmat for better performance.

But in both examples the expected output should be 1, 2, 3?
Hey @lobpcg,

I've updated the examples as per your feedback:

### Example using `aslinearoperator`:

```python
from scipy.sparse.linalg import lobpcg, aslinearoperator
import numpy as np
from scipy.sparse import diags

n = 100
vals = np.arange(1, n+1, dtype=np.float64)
A = diags(vals)  # Using sparse diags
A_aslo = aslinearoperator(A) # Converting to LinearOperator
X = np.random.default_rng().normal(size=(n, 3))
eigenvalues, _ = lobpcg(A_aslo, X, largest=False, maxiter=80)
print(eigenvalues)  # Output: [1., 2., 3.]
```

### Example using `LinearOperator` with a callable:


```python
from scipy.sparse.linalg import lobpcg, LinearOperator
import numpy as np

n = 100
vals = np.arange(1, n+1, dtype=np.float64)

# Define matvec for matrix-vector multiplication
def matvec(X):
    return vals[:, np.newaxis] * X

# Define matmat for matrix-matrix multiplication (important for LOBPCG performance)
def matmat(X):
    return vals[:, np.newaxis] * X

# Create a LinearOperator with matvec and matmat
A = LinearOperator((n, n), matvec=matvec, matmat=matmat, dtype='float64')

X = np.random.default_rng().normal(size=(n, 3))
eigenvalues, _ = lobpcg(A, X, largest=False, maxiter=80)

print(eigenvalues)  # Expected output: [1., 2., 3.]
```

Could you please review these changes? If everything is fine, I'll proceed with the PR. Thanks!
No need to define an identical matvec, just use matvec=matmat in the call.
Ok sure. Can you please point out where do I make the changes?
`A = LinearOperator((n, n), matvec=matmat, matmat=matmat, dtype='float64')`
Ok I will make the change and do you want me to include these examples in the lobpcg function?

Resolving patch:
diff --git a/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py b/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
--- a/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
+++ b/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py
@@ -356,26 +356,57 @@ def lobpcg(
     >>> eigenvalues.dtype
     dtype('float32')
 
-    LOBPCG needs only access the matrix product with `A` rather
+    `lobpcg` needs only access the matrix product with `A` rather
     then the matrix itself. Since the matrix `A` is diagonal in
-    this example, one can write a function of the product
+    this example, one can write a function of the matrix product
     ``A @ X`` using the diagonal values ``vals`` only, e.g., by
-    element-wise multiplication with broadcasting
+    element-wise multiplication with broadcasting in the lambda-function
 
-    >>> A_f = lambda X: vals[:, np.newaxis] * X
+    >>> A_lambda = lambda X: vals[:, np.newaxis] * X
 
-    and use the handle ``A_f`` to this callable function as an input
+    or the regular function
 
-    >>> eigenvalues, _ = lobpcg(A_f, X, maxiter=60)
+    >>> def A_matmat(X):
+    ...     return vals[:, np.newaxis] * X
+
+    and use the handle to one of these callables as an input
+
+    >>> eigenvalues, _ = lobpcg(A_lambda, X, maxiter=60)
+    >>> eigenvalues
+    array([100.])
+    >>> eigenvalues, _ = lobpcg(A_matmat, X, maxiter=60)
     >>> eigenvalues
     array([100.])
 
-    The next example illustrates computing 3 smallest eigenvalues of
-    the same matrix given by the function handle ``A_f`` with
-    constraints and preconditioning.
+    The traditional callable `LinearOperator` is no longer
+    necessary but still supported as the input to `lobpcg`.
+    Specifying ``matmat=A_matmat`` explicitely improves performance. 
+
+    >>> A_lo = LinearOperator((n, n), matvec=A_matmat, matmat=A_matmat, dtype=np.int16)
+    >>> eigenvalues, _ = lobpcg(A_lo, X, maxiter=80)
+    >>> eigenvalues
+    array([100.])
+
+    The least efficient callable option is `aslinearoperator`:
+
+    >>> eigenvalues, _ = lobpcg(aslinearoperator(A), X, maxiter=80)
+    >>> eigenvalues
+    array([100.])
+
+    We now switch to computing the three smallest eigenvalues specifying
 
     >>> k = 3
-    >>> X = rng.normal(size=(n, k))
+    >>> X = np.random.default_rng().normal(size=(n, k))
+
+    and ``largest=False`` parameter
+
+    >>> eigenvalues, _ = lobpcg(A, X, largest=False, maxiter=80)
+    >>> print(eigenvalues)  
+    [1. 2. 3.]
+
+    The next example illustrates computing 3 smallest eigenvalues of
+    the same matrix `A` given by the function handle ``A_matmat`` but
+    with constraints and preconditioning.
 
     Constraints - an optional input parameter is a 2D array comprising
     of column vectors that the eigenvectors must be orthogonal to
@@ -393,7 +424,7 @@ def lobpcg(
     Let us now solve the eigenvalue problem for the matrix `A` first
     without preconditioning requesting 80 iterations
 
-    >>> eigenvalues, _ = lobpcg(A_f, X, Y=Y, largest=False, maxiter=80)
+    >>> eigenvalues, _ = lobpcg(A_matmat, X, Y=Y, largest=False, maxiter=80)
     >>> eigenvalues
     array([4., 5., 6.])
     >>> eigenvalues.dtype
@@ -401,23 +432,23 @@ def lobpcg(
 
     With preconditioning we need only 20 iterations from the same `X`
 
-    >>> eigenvalues, _ = lobpcg(A_f, X, Y=Y, M=M, largest=False, maxiter=20)
+    >>> eigenvalues, _ = lobpcg(A_matmat, X, Y=Y, M=M, largest=False, maxiter=20)
     >>> eigenvalues
     array([4., 5., 6.])
 
     Note that the vectors passed in `Y` are the eigenvectors of the 3
     smallest eigenvalues. The results returned above are orthogonal to those.
 
-    Finally, the primary matrix `A` may be indefinite, e.g., after shifting
+    The primary matrix `A` may be indefinite, e.g., after shifting
     ``vals`` by 50 from 1, ..., 100 to -49, ..., 50, we still can compute
     the 3 smallest or largest eigenvalues.
 
     >>> vals = vals - 50
     >>> X = rng.normal(size=(n, k))
-    >>> eigenvalues, _ = lobpcg(A_f, X, largest=False, maxiter=99)
+    >>> eigenvalues, _ = lobpcg(A_matmat, X, largest=False, maxiter=99)
     >>> eigenvalues
     array([-49., -48., -47.])
-    >>> eigenvalues, _ = lobpcg(A_f, X, largest=True, maxiter=99)
+    >>> eigenvalues, _ = lobpcg(A_matmat, X, largest=True, maxiter=99)
     >>> eigenvalues
     array([50., 49., 48.])
 


Test patch: