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

Functions for nice tree decomposition and its labelling #36504

Merged
merged 26 commits into from
Dec 6, 2023

Conversation

guojing0
Copy link
Contributor

@guojing0 guojing0 commented Oct 22, 2023

This PR intends to add two new functions:

  1. make_nice_tree_decomposition takes a graph and its tree decomposition. It returns a nice tree decomposition.
  2. label_nice_tree_decomposition takes a tree decomposition, assumed to be nice. It returns the same result, with vertices labelled accordingly.

📝 Checklist

  • The title is concise, informative, and self-explanatory.
  • The description explains in detail what this PR is about.
  • I have linked a relevant issue or discussion.
  • I have created tests covering the changes.
  • I have updated the documentation accordingly.

⌛ Dependencies

@dcoudert
Copy link
Contributor

If you need, I have a code that takes a tree decomposition and returns a nice tree decomposition. It is not optimized but it can be useful. I had in my todo list to add it to Sagemath, as well as other methods...

@guojing0
Copy link
Contributor Author

guojing0 commented Oct 23, 2023

If you need, I have a code that takes a tree decomposition and returns a nice tree decomposition. It is not optimized but it can be useful. I had in my todo list to add it to Sagemath, as well as other methods...

Merci pour la suggestion. But at least for the nice tree decomposition one, I would like to implement it myself first.

What other methods do you have in mind? The ones mentioned in TODO in the code files?

Also, by "optimization", do you mean at the level of math/algo or code (like making the tree more balanced?)

@dcoudert
Copy link
Contributor

What other methods do you have in mind? The ones mentioned in TODO in the code files?

approximation of tree length based on BFS layering. I have a code for computing the "leveled tree" or "layered tree" as proposed by Chepoi et al. I need to add it and extend it to get a tree decomposition. The same can be done using lex-M and MCS-M.

Also, by "optimization", do you mean at the level of math/algo or code (like making the tree more balanced?)

Make it faster at least.

@guojing0
Copy link
Contributor Author

approximation of tree length based on BFS layering. I have a code for computing the "leveled tree" or "layered tree" as proposed by Chepoi et al. I need to add it and extend it to get a tree decomposition. The same can be done using lex-M and MCS-M.

Sounds interesting.

Currently I don't know too much about these, but can check the literatures.

@mkoeppe mkoeppe removed this from the sage-10.2 milestone Oct 23, 2023
@guojing0
Copy link
Contributor Author

If you need, I have a code that takes a tree decomposition and returns a nice tree decomposition. It is not optimized but it can be useful. I had in my todo list to add it to Sagemath, as well as other methods...

May I see the code for reference, or improve upon it...?

@dcoudert
Copy link
Contributor

My code is below. I hope it is correct

def nice_tree_decomposition(T):
    r"""
    Return a *nice* tree-decomposition of the tree-decomposition `T`.

    https://kam.mff.cuni.cz/~ashutosh/lectures/lec06.pdf

    A *nice* tree-decomposition is a rooted binary tree with four types of
    nodes:
    - Leaf nodes have no children and bag size 1;
    - Introduce nodes have one child. The child has the same vertices as the
      parent with one deleted;
    - Forget nodes have one child. The child has the same vertices as the parent
      with one added.
    - Join nodes have two children, both identical to the parent.

    .. WARNING::

        This method assumes that the vertices of the input tree `T` are hashable
        and have attribute ``issuperset``, e.g., ``frozenset`` or
        :class:`~sage.sets.set.Set_object_enumerated_with_category`.

    INPUT:

    - ``T`` -- a tree-decomposition
    """
    from sage.graphs.graph import Graph
    if not isinstance(T, Graph):
        raise ValueError("T must be a tree-decomposition")

    name = "Nice tree-decomposition of {}".format(T.name())
    if not T:
        return Graph(name=name)

    # P1: The tree is rooted.
    # We choose a root and orient the edges in root to leaves direction.
    leaves = [u for u in T if T.degree(u) == 1]
    root = leaves.pop()
    from sage.graphs.digraph import DiGraph
    DT = DiGraph(T.breadth_first_search(start=root, edges=True),
                 format='list_of_edges')

    # We relabel the graph in range 0..|T|-1
    bag_to_int = DT.relabel(inplace=True, return_map=True)
    # get the new name of the root
    root = bag_to_int[root]
    # and force bags to be of type Set to simplify the code
    bag = {ui: Set(u) for u, ui in bag_to_int.items()}

    # P2: The root and the leaves have empty bags.
    # To do so, we add to each leaf node l of DT a children with empty bag.
    # We also add a new root with empty bag.
    root, old_root = DT.add_vertex(), root
    DT.add_edge(root, old_root)
    bag[root] = Set()
    for vi, u in enumerate(leaves, start=root + 1):
        DT.add_edge(bag_to_int[u], vi)
        bag[vi] = Set()

    # P3: Ensure that each vertex of DT has at most 2 children.
    # If b has k > 2 children (c1, c2, ..., c_k). We disconnect (c1, ... ck-1)
    # from b, introduce k - 2 new vertices (b1, b2, .., bk-2), make ci the
    # children of bi for 1 <= i <= k-2, make ck-1 the second children of bk-2,
    # make bi the second children of b_i-1, and finally make b1 the second
    # children of b. Each vertex bi has the same bag has b.
    for ui in list(DT):  # the list(..) is needed since we modify DT
        if DT.out_degree(ui) > 2:
            children = DT.neighbors_out(ui)
            children.pop()  # one vertex remains a child of ui
            DT.delete_edges((ui, vi) for v in children)
            new_vertices = [DT.add_vertex() for _ in range(len(children) - 1)]
            DT.add_edge(ui, new_vertices[0])
            DT.add_path(new_vertices)
            DT.add_edges(zip(new_vertices, children))
            DT.add_edge(new_vertices[-1], children[-1])
            bag.update((vi, bag[ui]) for vi in new_vertices)

    # P4: If b has 2 children c1 and c2, then bag[b] == bag[c1] == bag[c2]
    for ui in list(DT):
        if DT.out_degree(ui) < 2:
            continue
        for vi in DT.neighbor_out_iterator(ui):
            if bag[ui] != bag[vi]:
                DT.delete_edge(ui, vi)
                wi = DT.add_vertex()
                DT.add_path([ui, wi, vi])
                bag[wi] = bag[ui]

    # P5: if b has a single child c, then one of the following conditions holds:
    #       (i)  bag[c] is a subset of bag[b] and |bag[b]| == |bag[c]| + 1
    #       (ii) bag[b] is a subset of bag[c] and |bag[c]| == |bag[b]| + 1

    def add_path_of_introduce(ui, vi):
        """
        Replace arc (ui, vi) by a path of introduce vertices.
        """
        if len(bag[ui]) + 1 == len(bag[vi]):
            return
        diff = list(bag[vi] - bag[ui])
        diff.pop()  # when all vertices are added, we are on vi
        xi = ui
        for w in diff:
            wi = DT.add_vertex()
            bag[wi] = bag[xi].union(Set((w,)))
            DT.add_edge(xi, wi)
            xi = wi
        DT.add_edge(xi, vi)
        DT.delete_edge(ui, vi)

    def add_path_of_forget(ui, vi):
        """
        Replace arc (ui, vi) by a path of forget vertices.
        """
        if len(bag[vi]) + 1 == len(bag[ui]):
            return
        diff = list(bag[ui] - bag[vi])
        diff.pop()  # when all vertices are removed, we are on vi
        xi = ui
        for w in diff:
            wi = DT.add_vertex()
            bag[wi] = bag[xi] - Set((w,))
            DT.add_edge(xi, wi)
            xi = wi
        DT.add_edge(xi, vi)
        DT.delete_edge(ui, vi)

    for ui in list(DT):
        if DT.out_degree(ui) != 1:
            continue
        
        vi = next(DT.neighbor_out_iterator(ui))
        bag_ui, bag_vi = bag[ui], bag[vi]

        if bag_ui == bag_vi:
            # We can merge the vertices
            if DT.in_degree(ui) == 1:
                parent = next(DT.neighbor_in_iterator(ui))
                DT.add_edge(parent, vi)
            else:
                root = vi
            DT.delete_vertex(ui)

        elif bag_ui.issubset(bag_vi):
            add_path_of_introduce(ui, vi)

        elif bag_vi.issubset(bag_ui):
            add_path_of_forget(ui, vi)

        else:
            # We must first forget some nodes and then introduce new nodes
            wi = DT.add_vertex()
            bag[wi] = bag[ui] & bag[vi]
            DT.add_path([ui, wi, vi])
            DT.delete_edge(ui, vi)
            add_path_of_forget(ui, wi)
            add_path_of_introduce(wi, vi)

    # We now return the result
    nice = Graph(DT, name=name)
    nice.relabel(inplace=True,
                 perm={u: (i, bag[u]) for i, u in enumerate(nice.breadth_first_search(start=root))})
    return nice

@guojing0
Copy link
Contributor Author

@dcoudert Thank you for the reviews and suggestions. I didn't think you would review and comment until I add the label needs_review. I was working on CoCalc web-based VS code, so I pushed the codes for back-up.

@dcoudert
Copy link
Contributor

You should add examples for which we can check that the solution is what we expect.

@guojing0
Copy link
Contributor Author

You should add examples for which we can check that the solution is what we expect.

Do intend to.

Copy link
Contributor

@dcoudert dcoudert left a comment

Choose a reason for hiding this comment

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

May be you can add parameter nice to method treewidth in order to return a nice tree decomposition ?

@guojing0
Copy link
Contributor Author

May be you can add parameter nice to method treewidth in order to return a nice tree decomposition ?

I agree with your suggestion. Just added. For some reason treewidth doesn't capture nice even if it's set to True. Will dig into the issue later.

@dcoudert
Copy link
Contributor

For some reason treewidth doesn't capture nice even if it's set to True. Will dig into the issue later.

This is certainly due to the use of clique separators. Make sure to transform the result before returning the result.

@guojing0
Copy link
Contributor Author

guojing0 commented Oct 29, 2023

For some reason treewidth doesn't capture nice even if it's set to True. Will dig into the issue later.

This is certainly due to the use of clique separators. Make sure to transform the result before returning the result.

I have uploaded the latest code. I don't think this is the case. The following was run with the latest code. As you can see on line 786, I have print(nice), which was set to True, but it printed False.

The nice tree decomp of the Petersen graph should have 28 nodes.

sage: graphs.PetersenGraph().treewidth(certificate=True, nice=True)
False
Tree decomposition: Graph on 6 vertices

@dcoudert
Copy link
Contributor

You forgot to pass the parameter to a call

diff --git a/src/sage/graphs/graph_decompositions/tree_decomposition.pyx b/src/sage/graphs/graph_decompositions/tree_decomposition.pyx
index 8ce5c433c9..f18f419c0c 100644
--- a/src/sage/graphs/graph_decompositions/tree_decomposition.pyx
+++ b/src/sage/graphs/graph_decompositions/tree_decomposition.pyx
@@ -702,7 +702,7 @@ def treewidth(g, k=None, kmin=None, certificate=False, algorithm=None, nice=Fals
     # Forcing k to be defined
     if k is None:
         for i in range(max(kmin, g.clique_number() - 1, min(g.degree())), g.order()):
-            ans = g.treewidth(algorithm=algorithm, k=i, certificate=certificate)
+            ans = g.treewidth(algorithm=algorithm, k=i, certificate=certificate, nice=nice)
             if ans:
                 return ans if certificate else i

@guojing0
Copy link
Contributor Author

You are right; I thought it was caused by line 693.

@guojing0
Copy link
Contributor Author

sage: import sage.graphs.graph_decompositions.tdlib as tdlib
....: 
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In [2], line 1
----> 1 import sage.graphs.graph_decompositions.tdlib as tdlib

ModuleNotFoundError: No module named 'sage.graphs.graph_decompositions.tdlib'

I am thinking about testing the changes with tdlib. But after I read relevant documentations, it seems that tdlib is only bundled with a distribution called sagemath-tdlib.

So I was wondering if you have any suggestions? Also, one of the TO-DO items is upgrade tdlib to 0.9.0 :trac: 30813 -- are there any significant changes, such as performance?

@guojing0
Copy link
Contributor Author

@dcoudert I am not sure if this is the right place to ask, if not, I will either email or resort to other channel(s): As continuation of this PR, I am working on some functionality related to homomorphism counting. I am aware that Sage already has has_homomorphism_to function, but it only returns one homomorphism even if multiple exist.

It seems that this function was/is implemented with some LP solver. Would it be possible to extend it to support returning all homomorphisms?

@dcoudert
Copy link
Contributor

@dcoudert I am not sure if this is the right place to ask, if not, I will either email or resort to other channel(s): As continuation of this PR, I am working on some functionality related to homomorphism counting. I am aware that Sage already has has_homomorphism_to function, but it only returns one homomorphism even if multiple exist.

It seems that this function was/is implemented with some LP solver. Would it be possible to extend it to support returning all homomorphisms?

It's better to open a specific issue to discuss that. It is certainly possible.

@dcoudert
Copy link
Contributor

Can you try to clean this PR. it has conflicts.

@guojing0
Copy link
Contributor Author

@dcoudert I am not sure if this is the right place to ask, if not, I will either email or resort to other channel(s): As continuation of this PR, I am working on some functionality related to homomorphism counting. I am aware that Sage already has has_homomorphism_to function, but it only returns one homomorphism even if multiple exist.
It seems that this function was/is implemented with some LP solver. Would it be possible to extend it to support returning all homomorphisms?

It's better to open a specific issue to discuss that. It is certainly possible.

Done: #36717

Copy link

github-actions bot commented Dec 3, 2023

Documentation preview for this PR (built with commit 2df952d; changes) is ready! 🎉

vbraun pushed a commit to vbraun/sage that referenced this pull request Dec 4, 2023
…ling

    
<!-- ^^^^^
Please provide a concise, informative and self-explanatory title.
Don't put issue numbers in there, do this in the PR body below.
For example, instead of "Fixes sagemath#1234" use "Introduce new method to
calculate 1+1"
-->
<!-- Describe your changes here in detail -->

<!-- Why is this change required? What problem does it solve? -->
<!-- If this PR resolves an open issue, please link to it here. For
example "Fixes sagemath#12345". -->
<!-- If your change requires a documentation PR, please link it
appropriately. -->


This PR intends to add two new functions:

1. `make_nice_tree_decomposition` takes a graph and its tree
decomposition. It returns a *nice* tree decomposition.
2. `label_nice_tree_decomposition` takes a tree decomposition, assumed
to be nice. It returns the same result, with vertices labelled
accordingly.


### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it
appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
<!-- Feel free to remove irrelevant items. -->

- [x] The title is concise, informative, and self-explanatory.
- [x] The description explains in detail what this PR is about.
- [x] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [x] I have updated the documentation accordingly.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on
- sagemath#12345: short description why this is a dependency
- sagemath#34567: ...
-->

<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
    
URL: sagemath#36504
Reported by: Jing Guo
Reviewer(s): David Coudert, Jing Guo
@vbraun vbraun merged commit a5107c6 into sagemath:develop Dec 6, 2023
14 of 17 checks passed
@mkoeppe mkoeppe added this to the sage-10.3 milestone Dec 6, 2023
@antonio-rojas
Copy link
Contributor

This is causing a test failure on Arch:

File "/usr/lib/python3.11/site-packages/sage/graphs/graph_decompositions/tree_decomposition.pyx", line 1037, in sage.graphs.graph_decompositions.tree_decomposition.label_nice_tree_decomposition
Failed example:
    for node in sorted(label_TD):
        print(node, label_TD.get_vertex(node))
Expected:
    (0, {}) root
    (1, {0}) forget
    (2, {0, 1}) intro
    (3, {0}) forget
    (4, {0, 4}) join
    (5, {0, 4}) intro
    (6, {0, 4}) intro
    (7, {0}) forget
    (8, {0}) forget
    (9, {0, 3}) intro
    (10, {0, 2}) intro
    (11, {3}) intro
    (12, {2}) intro
    (13, {}) leaf
    (14, {}) leaf
Got:
    (0, {}) root
    (1, {0}) forget
    (2, {0, 4}) intro
    (3, {0}) forget
    (4, {0, 1}) join
    (5, {0, 1}) intro
    (6, {0, 1}) intro
    (7, {0}) forget
    (8, {0}) forget
    (9, {0, 2}) intro
    (10, {0, 3}) intro
    (11, {2}) intro
    (12, {3}) intro
    (13, {}) leaf
    (14, {}) leaf

@guojing0
Copy link
Contributor Author

@antonio-rojas There's indeed a bug in this code, I have fixed it in #36846. Does the new PR fix your issue?

@guojing0 guojing0 deleted the nice-tree-decomp branch December 12, 2023 21:17
@antonio-rojas
Copy link
Contributor

@antonio-rojas There's indeed a bug in this code, I have fixed it in #36846. Does the new PR fix your issue?

No, I'm getting the exact same failure with #36846

@dcoudert
Copy link
Contributor

@guojing0, can you modify the test in #36846 to make it more robust (may be using a smaller graph). We can always decide to add it tag # random or # not tested..
Take also the opportunity to double check that labels are correctly set.

@guojing0
Copy link
Contributor Author

guojing0 commented Dec 13, 2023

@dcoudert Done in #36846, also use the claw instead of $K_{1, 4}$.

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.

5 participants