Repository: sagemath/sage

Issue:
Speed up poset characteristic polynomial computation by caching Möbius function
It appears that the built-in methods to compute the characteristic polynomial of a poset recompute Moebius function values redundantly, leading to slowdown. For example:

    sage: P = characteristic_polynomial(posets.SetPartitions(7))
	sage: %time S.characteristic_polynomial()
	CPU times: user 27.2 ms, sys: 14.4 ms, total: 41.6 ms
	Wall time: 69.4 ms
	q^4 - 10*q^3 + 35*q^2 - 50*q + 24

but by memoizing:

	sage: %time characteristic_polynomial(S)
	CPU times: user 4.8 ms, sys: 212 µs, total: 5.01 ms
	Wall time: 5.09 ms
	t^4 - 10*t^3 + 35*t^2 - 50*t + 24



Discussion:
Hi @trevorkarn,

Is it cool if I take up this issue?

Any pointers for starting?
Thanks for your interest. I actually already have all of the code written I
just need to figure out the new workflow to get it up. However I’d love
help in reviewing my code once I push it.

On Tue, Feb 7, 2023 at 4:04 AM Agamdeep Singh ***@***.***>
wrote:

> Hi @trevorkarn <https://github.com/trevorkarn>,
>
> Is it cool if I take up this issue?
>
> Any pointers for starting?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/sagemath/sage/issues/34971#issuecomment-1420506627>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AMUZQKMYGSYDTBBXEOLTJYDWWIM3DANCNFSM6AAAAAAUSS3DDY>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>
-- 
Best Regards,

Trevor Karn
Graduate Student, Mathematics

@trevorkarn So the general steps:

1 - Create a fork of the repo.
2 - Create a branch and push the changes to your fork.
3 - Use that to create a pull request (PR).
4 - Make sure to mention this issue in the PR statement so they get linked.
5 - Put me (@tscrim) as a (suggested) reviewer. (I want to test that this is working properly.)
> Thanks for your interest. I actually already have all of the code written I just need to figure out the new workflow to get it up. However I’d love help in reviewing my code once I push it.
> On Tue, Feb 7, 2023 at 4:04 AM Agamdeep Singh ***@***.***> wrote: Hi @trevorkarn <https://github.com/trevorkarn>, Is it cool if I take up this issue? Any pointers for starting? — Reply to this email directly, view it on GitHub <[#34971 (comment)](https://github.com/sagemath/sage/issues/34971#issuecomment-1420506627)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AMUZQKMYGSYDTBBXEOLTJYDWWIM3DANCNFSM6AAAAAAUSS3DDY> . You are receiving this because you were mentioned.Message ID: ***@***.***>
> -- Best Regards, Trevor Karn Graduate Student, Mathematics

@trevorkarn 

I'm a newbie in contributing to SageMath. So I'll try my best to keep up with you.
Just had clear basics in Data Structures and Algorithms so was looking to optimize the algorithm as you suggested.


@tscrim  Thanks!

@jnash10 I don't have much formal training in algorithms, but I took a dynamic programming approach here. If you do take a look at it and see anything that I could do better feel free to make comment.

Resolving patch:
diff --git a/src/sage/combinat/posets/hasse_diagram.py b/src/sage/combinat/posets/hasse_diagram.py
--- a/src/sage/combinat/posets/hasse_diagram.py
+++ b/src/sage/combinat/posets/hasse_diagram.py
@@ -419,6 +419,7 @@ def maximal_elements(self):
         """
         return self.sinks()
 
+    @cached_method
     def bottom(self):
         """
         Return the bottom element of the poset, if it exists.
@@ -968,6 +969,64 @@ def moebius_function(self, i, j):  # dumb algorithm
                     self._moebius_function_values[(i, j)] = -sum(self.moebius_function(i, k) for k in ci[:-1])
         return self._moebius_function_values[(i, j)]
 
+    def bottom_moebius_function(self, j):
+        r"""
+        Return the value of the Möbius function of the poset
+        on the elements ``zero`` and ``j``, where ``zero`` is
+        ``self.bottom()``, the unique minimal element of the poset.
+
+        EXAMPLES::
+
+            sage: P = Poset({0: [1,2]})
+            sage: hasse = P._hasse_diagram
+            sage: hasse.bottom_moebius_function(1)
+            -1
+            sage: hasse.bottom_moebius_function(2)
+            -1
+            sage: P = Poset({0: [1,3], 1:[2], 2:[4], 3:[4]})
+            sage: hasse = P._hasse_diagram
+            sage: for i in range(5):
+            ....:   print(hasse.bottom_moebius_function(i))
+            1
+            -1
+            0
+            -1
+            1
+
+        TESTS::
+
+            sage: P = Poset({0:[2], 1:[2]})
+            sage: hasse = P._hasse_diagram
+            sage: hasse.bottom_moebius_function(1)
+            Traceback (most recent call last):
+            ...
+            ValueError: the poset does not have a bottom element
+        """
+        zero = self.bottom()
+        if zero is None:
+            raise ValueError("the poset does not have a bottom element")
+        # if the value has already been computed, either by self.moebius_function
+        # or by self.bottom_moebius_function, then just use the cached value.
+        try:
+            return self._moebius_function_values[(zero, j)]
+        # if the dict has not been initialized, do that and try again
+        except AttributeError:
+            self._moebius_function_values = {}
+            return self.bottom_moebius_function(j)
+        # if mu(zero, j) has not already been computed, we'll get a key error.
+        except KeyError:
+            if zero == j:
+                self._moebius_function_values[(zero, j)] = 1
+            # since zero is the minimal element, we can ignore the case that zero > j,
+            # and move on to computing the interval, which is exactly the order ideal.
+            else:
+                # do the depth_first_search over order_ideal, because we don't care
+                # about sorting the elements of the order ideal.
+                ci = self._backend.depth_first_search(j, reverse=True)
+                next(ci)  # throw out the first element, which is j
+                self._moebius_function_values[(zero, j)] = -sum(self.bottom_moebius_function(k) for k in ci)
+        return self._moebius_function_values[(zero, j)]
+
     def moebius_function_matrix(self, algorithm='cython'):
         r"""
         Return the matrix of the Möbius function of this poset.
diff --git a/src/sage/combinat/posets/posets.py b/src/sage/combinat/posets/posets.py
--- a/src/sage/combinat/posets/posets.py
+++ b/src/sage/combinat/posets/posets.py
@@ -7612,11 +7612,10 @@ def characteristic_polynomial(self):
         if not self.is_graded():
             raise ValueError("the poset is not graded")
         if not self.has_bottom():
-            raise ValueError("the poset has not a bottom element")
+            raise ValueError("the poset does not have a bottom element")
         n = rk(hasse.maximal_elements()[0])
-        x0 = hasse.minimal_elements()[0]
         q = polygen(ZZ, 'q')
-        return sum(hasse.moebius_function(x0, x) * q**(n - rk(x)) for x in hasse)
+        return sum(hasse.bottom_moebius_function(x) * q**(n - rk(x)) for x in hasse)
 
     def chain_polynomial(self):
         """


Test patch: