Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(matrices): Add fraction free solvers for DomainMatrix #25346

Merged
merged 11 commits into from
Jul 14, 2023

Conversation

oscarbenjamin
Copy link
Collaborator

@oscarbenjamin oscarbenjamin commented Jul 9, 2023

References to other Issues or PRs

Partial implementation of the fraction-free methods in gh-21834.

Brief description of what is fixed or changed

Adds various methods like solve_den, rref_den, inv_den, rref_den to DomainMatrix. These methods provide fraction-free or division-free algorithms for matrix inverse and linear solves over rings rather than fields.

This will speed up various operations including linsolve, Matrix.solve and Matrix.inv if used by those methods in future. Example:

In [27]: M = Matrix(symbols('x:4')).reshape(2, 2)

In [28]: dM = M.to_DM() # convert to DomainMatrix

In [29]: %time Minv, detM = dM.inv_den()
CPU times: user 1.2 ms, sys: 44 µs, total: 1.24 ms
Wall time: 1.26 ms

In [30]: Minv.to_Matrix()
Out[30]: 
⎡x-x₁⎤
⎢        ⎥
⎣-xx₀ ⎦

In [31]: detM
Out[31]: x0*x3 - x1*x2

In [32]: Minv * dM == detM * dM.I()
Out[32]: True

In [33]: Minv.to_Matrix() / Minv.domain.to_sympy(detM)
Out[33]: 
⎡      x-x₁     ⎤
⎢─────────────  ─────────────⎥
⎢x₀⋅x- x₁⋅xx₀⋅x- x₁⋅x₂⎥
⎢                            ⎥
⎢     -xx₀     ⎥
⎢─────────────  ─────────────⎥
⎣x₀⋅x- x₁⋅xx₀⋅x- x₁⋅x₂⎦

In [34]: Minv.to_field() / detM
Out[34]: DomainMatrix({0: {0: x3/(x0*x3 - x1*x2), 1: -x1/(x0*x3 - x1*x2)}, 1: {0: -x2/(x0*x3 - x1*x2), 1: x0/(x0*x3 - x1*x2)}}, (2, 2), ZZ(x0,x1,x2,x3))

In [35]: (Minv.to_field() / detM).to_Matrix()
Out[35]: 
⎡      x-x₁     ⎤
⎢─────────────  ─────────────⎥
⎢x₀⋅x- x₁⋅xx₀⋅x- x₁⋅x₂⎥
⎢                            ⎥
⎢     -xx₀     ⎥
⎢─────────────  ─────────────⎥
⎣x₀⋅x- x₁⋅xx₀⋅x- x₁⋅x₂⎦

The speed different is significant for large matrices or more symbols. Here is the current Matrix method timing:

In [40]: %time ok = Matrix(symbols('x:4')).reshape(2, 2).inv()
CPU times: user 224 ms, sys: 3.98 ms, total: 228 ms
Wall time: 226 ms

In [41]: %time ok = Matrix(symbols('x:9')).reshape(3, 3).inv()
CPU times: user 1.53 s, sys: 0 ns, total: 1.53 s
Wall time: 1.53 s

In [42]: %time ok = Matrix(symbols('x:16')).reshape(4, 4).inv()
^C^C
KeyboardInterrupt

Here is the same with the PR using DomainMatrix:

In [44]: %time ok = Matrix(symbols('x:4')).reshape(2, 2).to_DM().inv_den()
CPU times: user 4.14 ms, sys: 2 µs, total: 4.14 ms
Wall time: 3.83 ms

In [45]: %time ok = Matrix(symbols('x:9')).reshape(3, 3).to_DM().inv_den()
CPU times: user 11 ms, sys: 0 ns, total: 11 ms
Wall time: 10.3 ms

In [46]: %time ok = Matrix(symbols('x:16')).reshape(4, 4).to_DM().inv_den()
CPU times: user 40.6 ms, sys: 7 µs, total: 40.6 ms
Wall time: 39.1 ms

In [47]: %time ok = Matrix(symbols('x:25')).reshape(5, 5).to_DM().inv_den()
CPU times: user 250 ms, sys: 0 ns, total: 250 ms
Wall time: 248 ms

In [48]: %time ok = Matrix(symbols('x:36')).reshape(6, 6).to_DM().inv_den()
CPU times: user 18.1 s, sys: 7.47 ms, total: 18.1 s
Wall time: 18.1 s

Obviously this example scales very badly by any technique but DomainMatrix is orders of magnitude faster.

Other comments

There are a few things to finish and a few tests to add and release notes before this is ready for merge.

Release Notes

  • matrices
    • A number of fraction-free and division free methods for matrix inverse, RREF and solving matrix equations are added to DomainMatrix. These methods are much faster than approaches based on field division for e.g. solving a system of linear equations where the coefficients are polynomial expressions. Also documentation was added for many DomainMatrix methods and for related classes and functions.
      • DomainMatrix.rref_den: A fraction-free method for computing the reduced row echelon form (RREF) with denominator was added.
      • DomainMatrix.adj_det: A division-free method for computing the adjugate and determinant of a square matrix was added.
      • DomainMatrix.adjugate: A division-free method for computing the adjugate of a square matrix was added.
      • DomainMatrix.inv_den: Fraction-free or division-free method for computing the inverse of a matrix with denominator was added.
      • DomainMatrix.solve_den: Fraction-free or division-free method for solving a matrix equation A*x = b was added
      • DomainMatrix.adj_poly_det: A method for computing the polynomial p such that p(A) = adjugate(A) was added.
      • DomainMatrix.eval_poly: A method for evaluating a polynomial function of a matrix p(A) was added.
      • DomainMatrix.eval_poly_mul: A method for effciently evaluating a polynomial matrix product p(A)*b was added.

@sympy-bot
Copy link

sympy-bot commented Jul 9, 2023

Hi, I am the SymPy bot (v170). I'm here to help you write a release notes entry. Please read the guide on how to write release notes.

Your release notes are in good order.

Here is what the release notes will look like:

  • matrices

    • A number of fraction-free and division free methods for matrix inverse, RREF and solving matrix equations are added to DomainMatrix. These methods are much faster than approaches based on field division for e.g. solving a system of linear equations where the coefficients are polynomial expressions. Also documentation was added for many DomainMatrix methods and for related classes and functions. (#25346 by @oscarbenjamin)
    • DomainMatrix.rref_den: A fraction-free method for computing the reduced row echelon form (RREF) with denominator was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.adj_det: A division-free method for computing the adjugate and determinant of a square matrix was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.adjugate: A division-free method for computing the adjugate of a square matrix was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.inv_den: Fraction-free or division-free method for computing the inverse of a matrix with denominator was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.solve_den: Fraction-free or division-free method for solving a matrix equation A*x = b was added (#25346 by @oscarbenjamin)

    • DomainMatrix.adj_poly_det: A method for computing the polynomial p such that p(A) = adjugate(A) was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.eval_poly: A method for evaluating a polynomial function of a matrix p(A) was added. (#25346 by @oscarbenjamin)

    • DomainMatrix.eval_poly_mul: A method for effciently evaluating a polynomial matrix product p(A)*b was added. (#25346 by @oscarbenjamin)

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.13.

Click here to see the pull request description that was parsed.
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234" (see
https://tinyurl.com/auto-closing for more information). Also, please
write a comment on that issue linking back to this pull request once it is
open. -->

Partial implementation of the fraction-free methods in gh-21834.

#### Brief description of what is fixed or changed

Adds various methods like `solve_den`, `rref_den`, `inv_den`, `rref_den` to `DomainMatrix`. These methods provide fraction-free or division-free algorithms for matrix inverse and linear solves over rings rather than fields.

This will speed up various operations including `linsolve`, `Matrix.solve` and `Matrix.inv` if used by those methods in future. Example:
```python
In [27]: M = Matrix(symbols('x:4')).reshape(2, 2)

In [28]: dM = M.to_DM() # convert to DomainMatrix

In [29]: %time Minv, detM = dM.inv_den()
CPU times: user 1.2 ms, sys: 44 µs, total: 1.24 ms
Wall time: 1.26 ms

In [30]: Minv.to_Matrix()
Out[30]: 
⎡x₃   -x₁⎤
⎢        ⎥
⎣-x₂  x₀ ⎦

In [31]: detM
Out[31]: x0*x3 - x1*x2

In [32]: Minv * dM == detM * dM.I()
Out[32]: True

In [33]: Minv.to_Matrix() / Minv.domain.to_sympy(detM)
Out[33]: 
⎡      x₃            -x₁     ⎤
⎢─────────────  ─────────────⎥
⎢x₀⋅x₃ - x₁⋅x₂  x₀⋅x₃ - x₁⋅x₂⎥
⎢                            ⎥
⎢     -x₂             x₀     ⎥
⎢─────────────  ─────────────⎥
⎣x₀⋅x₃ - x₁⋅x₂  x₀⋅x₃ - x₁⋅x₂⎦

In [34]: Minv.to_field() / detM
Out[34]: DomainMatrix({0: {0: x3/(x0*x3 - x1*x2), 1: -x1/(x0*x3 - x1*x2)}, 1: {0: -x2/(x0*x3 - x1*x2), 1: x0/(x0*x3 - x1*x2)}}, (2, 2), ZZ(x0,x1,x2,x3))

In [35]: (Minv.to_field() / detM).to_Matrix()
Out[35]: 
⎡      x₃            -x₁     ⎤
⎢─────────────  ─────────────⎥
⎢x₀⋅x₃ - x₁⋅x₂  x₀⋅x₃ - x₁⋅x₂⎥
⎢                            ⎥
⎢     -x₂             x₀     ⎥
⎢─────────────  ─────────────⎥
⎣x₀⋅x₃ - x₁⋅x₂  x₀⋅x₃ - x₁⋅x₂⎦
```
The speed different is significant for large matrices or more symbols. Here is the current Matrix method timing:
```python
In [40]: %time ok = Matrix(symbols('x:4')).reshape(2, 2).inv()
CPU times: user 224 ms, sys: 3.98 ms, total: 228 ms
Wall time: 226 ms

In [41]: %time ok = Matrix(symbols('x:9')).reshape(3, 3).inv()
CPU times: user 1.53 s, sys: 0 ns, total: 1.53 s
Wall time: 1.53 s

In [42]: %time ok = Matrix(symbols('x:16')).reshape(4, 4).inv()
^C^C
KeyboardInterrupt
```
Here is the same with the PR using DomainMatrix:
```python
In [44]: %time ok = Matrix(symbols('x:4')).reshape(2, 2).to_DM().inv_den()
CPU times: user 4.14 ms, sys: 2 µs, total: 4.14 ms
Wall time: 3.83 ms

In [45]: %time ok = Matrix(symbols('x:9')).reshape(3, 3).to_DM().inv_den()
CPU times: user 11 ms, sys: 0 ns, total: 11 ms
Wall time: 10.3 ms

In [46]: %time ok = Matrix(symbols('x:16')).reshape(4, 4).to_DM().inv_den()
CPU times: user 40.6 ms, sys: 7 µs, total: 40.6 ms
Wall time: 39.1 ms

In [47]: %time ok = Matrix(symbols('x:25')).reshape(5, 5).to_DM().inv_den()
CPU times: user 250 ms, sys: 0 ns, total: 250 ms
Wall time: 248 ms

In [48]: %time ok = Matrix(symbols('x:36')).reshape(6, 6).to_DM().inv_den()
CPU times: user 18.1 s, sys: 7.47 ms, total: 18.1 s
Wall time: 18.1 s
```
Obviously this example scales very badly by any technique but DomainMatrix is orders of magnitude faster.

#### Other comments

There are a few things to finish and a few tests to add and release notes before this is ready for merge.

#### Release Notes

<!-- Write the release notes for this release below between the BEGIN and END
statements. The basic format is a bulleted list with the name of the subpackage
and the release note for this PR. For example:

* solvers
  * Added a new solver for logarithmic equations.

* functions
  * Fixed a bug with log of integers. Formerly, `log(-x)` incorrectly gave `-log(x)`.

* physics.units
  * Corrected a semantical error in the conversion between volt and statvolt which
    reported the volt as being larger than the statvolt.

or if no release note(s) should be included use:

NO ENTRY

See https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more
information on how to write release notes. The bot will check your release
notes automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
* matrices
   * A number of fraction-free and division free methods for matrix inverse, RREF and solving matrix equations are added to DomainMatrix. These methods are much faster than approaches based on field division for e.g. solving a system of linear equations where the coefficients are polynomial expressions. Also documentation was added for many DomainMatrix methods and for related classes and functions.
       - `DomainMatrix.rref_den`: A fraction-free method for computing the reduced row echelon form (RREF) with denominator was added.
       - `DomainMatrix.adj_det`: A division-free method for computing the adjugate and determinant of a square matrix was added.
       - `DomainMatrix.adjugate`: A division-free method for computing the adjugate of a square matrix was added.
       - `DomainMatrix.inv_den`: Fraction-free or division-free method for computing the inverse of a matrix with denominator was added.
       - `DomainMatrix.solve_den`: Fraction-free or division-free method for solving a matrix equation `A*x = b` was added
       - `DomainMatrix.adj_poly_det`: A method for computing the polynomial `p` such that `p(A) = adjugate(A)` was added.
       - `DomainMatrix.eval_poly`: A method for evaluating a polynomial function of a matrix `p(A)` was added.
       - `DomainMatrix.eval_poly_mul`: A method for effciently evaluating a polynomial matrix product `p(A)*b` was added.
<!-- END RELEASE NOTES -->

Update

The release notes on the wiki have been updated.

@github-actions
Copy link

github-actions bot commented Jul 9, 2023

Benchmark results from GitHub Actions

Lower numbers are good, higher numbers are bad. A ratio less than 1
means a speed up and greater than 1 means a slowdown. Green lines
beginning with + are slowdowns (the PR is slower then master or
master is slower than the previous release). Red lines beginning
with - are speedups.

Significantly changed benchmark results (PR vs master)

Significantly changed benchmark results (master vs previous release)

       before           after         ratio
     [8059df73]       [46558492]
     <sympy-1.12^0>                 
-      84.6±0.9ms       55.3±0.3ms     0.65  integrate.TimeIntegrationRisch02.time_doit(10)
-        84.9±1ms       53.8±0.2ms     0.63  integrate.TimeIntegrationRisch02.time_doit_risch(10)

Full benchmark results can be found as artifacts in GitHub Actions
(click on checks at the top of the PR).

@sylee957
Copy link
Member

I think that it's mostly ready, but the only problems are that it is polyfilled for sparse matrix,
and the existing Matrix may not benefit from this before it is used by that.

Comment on lines 554 to 556
def rref_den(a):
"""Reduced-row echelon form of a and list of pivots"""
b = a.copy()
Copy link
Member

@smichr smichr Jul 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def rref_den(a):
"""Reduced-row echelon form of a and list of pivots"""
b = a.copy()
def rref_den(a):
"""Return ``(ra*d, d, p)`` where ``ra`` is the reduced-row echelon form of $a$ and ``p`` is a list of pivots"""
b = a.copy()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or something like that. The denominator being returned should be mentioned.

@oscarbenjamin
Copy link
Collaborator Author

I think that it's mostly ready, but the only problems are that it is polyfilled for sparse matrix,
and the existing Matrix may not benefit from this before it is used by that.

I would expect to expose this for Matrix by having some logic around exactly what domain or format should be used.

Sparse matrices are mostly only useful in the case of simple domains where the elementary ring arithmetic is relatively cheap. Here that means ZZ, QQ, QQ<sqrt(2)> etc. Those domains can all use the existing RREF with division methods reasonably well anyway e.g.:

In [455]: %time ok = M.to_DM().to_field().rref()
CPU times: user 1.65 s, sys: 0 ns, total: 1.65 s
Wall time: 1.65 s

In [456]: M = randMatrix(100)

In [457]: %time ok = M.to_DM().to_field().rref()
CPU times: user 1.71 s, sys: 0 ns, total: 1.71 s
Wall time: 1.7 s

When the matrix is sparse there can be benefits to using the sparse representation but the benefits are usually not huge:

In [458]: M = randMatrix(100, percent=1)

In [459]: %time ok = M.to_DM().to_field().rref()
CPU times: user 2.83 ms, sys: 0 ns, total: 2.83 ms
Wall time: 2.85 ms

In [460]: %time ok = M.to_DM().to_field().to_dense().rref()
CPU times: user 9.11 ms, sys: 49 µs, total: 9.16 ms
Wall time: 9.15 ms

When you have a more complicated domain like QQ[x, y, z, ...] then the cost of a bit of redundant arithmetic with zeros is trivial compared to the cost of multiplying the nonzero elements:

In [468]: M = eye(10)*(x+1)**10

In [469]: %time ok = M.to_DM().det()
CPU times: user 209 ms, sys: 0 ns, total: 209 ms
Wall time: 207 ms

In [470]: %time ok = M.to_DM().to_dense().det()
CPU times: user 203 ms, sys: 0 ns, total: 203 ms
Wall time: 201 ms

In [471]: %time ok = M.to_DM().to_field().rref()
CPU times: user 24.8 ms, sys: 0 ns, total: 24.8 ms
Wall time: 23.2 ms

In [472]: %time ok = M.to_DM().to_field().to_dense().rref()
CPU times: user 22.7 ms, sys: 0 ns, total: 22.7 ms
Wall time: 22.1 ms

In this example the nonzero elements are so expensive and the zero elements are so cheap that the benefit of using sparse routines is gone. Within SymPy there are a mixture of cases for these matrix operations but usually when things are really slow it is because of expression blowup rather than anything that sparse matrix techniques would help with.

The example in gh-25009 is a good one.:

In [486]: p, q, r, s, t, u, v, w, x, y = symbols('p, q, r, s, t, u, v, w, x, y', positive=True)
     ...: 
     ...: X = Matrix([
     ...:     [1/y + 1/w + 1/t + 1/s + 1/p, 0, -1/w, 0, 0, -1/t, 1],
     ...:     [0, 1/u + 1/q, -1/q, 0, 0, 0, 0],
     ...:     [-1/w, -1/q, 1/w + 1/q + 1/x, -1/x, 0, 0, -1],
     ...:     [0, 0, -1/x, 1/r + 1/x, -1/r, 0, 0],
     ...:     [0, 0, 0, -1/r, 1/v + 1/r, -1/v, 0],
     ...:     [-1/t, 0, 0, 0, -1/v, 1/v + 1/t, 0],
     ...:     [1, 0, -1, 0, 0, 0, 0]
     ...: ])

In [487]: print(repr(X))
Matrix([
[1/y + 1/w + 1/t + 1/s + 1/p,         0,            -1/w,         0,         0,      -1/t,  1],
[                          0, 1/u + 1/q,            -1/q,         0,         0,         0,  0],
[                       -1/w,      -1/q, 1/x + 1/w + 1/q,      -1/x,         0,         0, -1],
[                          0,         0,            -1/x, 1/x + 1/r,      -1/r,         0,  0],
[                          0,         0,               0,      -1/r, 1/v + 1/r,      -1/v,  0],
[                       -1/t,         0,               0,         0,      -1/v, 1/v + 1/t,  0],
[                          1,         0,              -1,         0,         0,         0,  0]])

This matrix is 7x7 but computing the inverse is extremely slow because it requires very large expressions. We are almost at the point of being to make this happen easily for users:

In [497]: %time denom, Xnum = X.to_DM().clear_denoms(convert=True)
CPU times: user 34.8 ms, sys: 0 ns, total: 34.8 ms
Wall time: 33.8 ms

In [498]: %time Xinv, denom2 = Xnum.inv_den()
CPU times: user 601 ms, sys: 0 ns, total: 601 ms
Wall time: 600 ms

In [499]: %time Xinvf = Xinv.to_field()
CPU times: user 2.25 ms, sys: 0 ns, total: 2.25 ms
Wall time: 2.27 ms

In [500]: %time Xinv = Xinvf * (Xinvf.domain.convert(denom.element) / Xinvf.domain.convert(denom2))
CPU times: user 216 ms, sys: 0 ns, total: 216 ms
Wall time: 215 ms

In [502]: %time (Xinv * X.to_DM()).to_Matrix()
CPU times: user 817 ms, sys: 0 ns, total: 817 ms
Wall time: 815 ms
Out[502]: 
⎡1  0  0  0  0  0  0⎤
⎢                   ⎥
⎢0  1  0  0  0  0  0⎥
⎢                   ⎥
⎢0  0  1  0  0  0  0⎥
⎢                   ⎥
⎢0  0  0  1  0  0  0⎥
⎢                   ⎥
⎢0  0  0  0  1  0  0⎥
⎢                   ⎥
⎢0  0  0  0  0  1  0⎥
⎢                   ⎥
⎣0  0  0  0  0  0  1

I think that it is this kind of case: small matrices but complicated expressions that is mostly significant for SymPy users. There are certainly applications for sparse matrices but it is mostly limited to either extremely sparse matrices or to simple domains. Division free approaches are mostly significant where they allow preventing growth of the denominator expressions.

@sympy-bot
Copy link

sympy-bot commented Jul 10, 2023

🟠

Hi, I am the SymPy bot (v170). I've noticed that some of your commits add or delete files. Since this is sometimes done unintentionally, I wanted to alert you about it.

This is an experimental feature of SymPy Bot. If you have any feedback on it, please comment at sympy/sympy-bot#75.

The following commits add new files:

  • d44a34b:
    • sympy/polys/matrices/tests/test_rref.py
  • df8257b:
    • sympy/polys/matrices/tests/test_inverse.py

If these files were added/deleted on purpose, you can ignore this message.

@oscarbenjamin
Copy link
Collaborator Author

The pyodide tests are failing, specifically job 3:

== 2710 passed, 161 skipped, 8849 deselected, 15 xfailed in 583.45s (0:09:43) ==
pytest finished with exit code 0
`pyodide.runPython` completed successfully
Error: The operation was canceled.

That has happened repeatedly CC @brocksam @eagleoflqj

@oscarbenjamin
Copy link
Collaborator Author

This is ready for review now.

@oscarbenjamin
Copy link
Collaborator Author

I've added release notes.

@oscarbenjamin
Copy link
Collaborator Author

The pyodide tests are failing, specifically job 3:

This seems to have passed in the latest run so maybe it is an intermittent problem. The same job failed more than once before.

>>> from sympy.polys.matrices.ddm import ddm_iinv, ddm_imatmul
>>> from sympy import QQ
>>> a = [[QQ(1), QQ(2)], [QQ(3), QQ(4)]]
>>> ainv = [[None, None], [None, None]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
>>> ainv = [[None, None], [None, None]]
>>> ainv = []

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not really intended that that would be able to work...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In retrospect it is probably a mistake that these operations work in place. You can see in this example that it does not give any performance benefit but it does complicate the interface.

I would rather not change that right now though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how it complicates the interface. You need somewhere to put the result if you aren't going to return it. I am only suggesting that you change the docstring to show that you don't have to worry about what shape or what values must be in the result (mutable list) that you send: just send an empty list and it will be filled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just send an empty list and it will be filled.

I don’t think that there can be a guarantee that that may work in the future though

Copy link
Member

@sylee957 sylee957 Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we do want to work with list of lists then still it would be better just to have ainv = ddm_inv(a, K) that returns a new list.

I generally advice to do type hinting for these sort if possible, than having to be worrying about if you wrote something that consistently returns or not.

I don’t prefer that kind of pattern, because it doesn’t make it clear the if the input list references the output list or the output is new.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not going to change this right now but a future PR could add out of place versions for any current in place functions. Possibly the in place versions could be removed or otherwise the docstring could just be changed to call the out of place version of the function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if anyone will read this now that the PR has merged (and I might not even see the answer) but if you pass a list of lists as is done in the docstring, then it is pointless to fill it with anything. Neither the content nor the shape of the contents have any impact on what is returned -- and what is returned is a list of lists. All I was suggesting that be changed was the docstring to show a list instead of a list filled with lists filled with values (none of which matter and should not even be thought of when calling the routine that only returns a list of lists that replaces the contents of what was passed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, the things to put on the docstrings should be the best practices, or the things that are invariant to the code

Copy link
Member

@smichr smichr Jul 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what is the best practice when the routine does not look at the contents or shape of what you are passing to it? In one docstring it is filled like this ainv = [[None]*3 for i in range(3)] and in another like ainv = [[Q(1)]*3 for i in range(3)]. None of this matters if the routine modifies like this ainv[:] = [row[:n] for row in smthng]. We are making the user reading the docstring think about the contents of ainv when it doesn't matter.

@oscarbenjamin
Copy link
Collaborator Author

I think I have responded to all comments. I agree with all except #25346 (comment)

Thanks for the reviews!

@sylee957 sylee957 merged commit a9320ec into sympy:master Jul 14, 2023
56 checks passed
@sylee957
Copy link
Member

Thanks, I merge this

@oscarbenjamin oscarbenjamin deleted the pr_inv_den branch July 14, 2023 08:04
@oscarbenjamin
Copy link
Collaborator Author

Thanks all for the reviews!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants