Skip to content

Optimize MatchingCoveredGraph’s add_edge method #39678

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

Merged
merged 5 commits into from
Mar 22, 2025

Conversation

mashraf8
Copy link
Contributor

@mashraf8 mashraf8 commented Mar 11, 2025

The add_edge method has been optimized to ensure that edges are added while preserving the matching-covered property without fully reinitializing or creating a new graph.

First, the edge is added using the base method, then the graph is checked to confirm that it remains matching-covered. If it is no longer valid, the edge is immediately removed to restore the previous state. The matching is updated only if the graph remains valid.

Doctests were successfully executed to verify the correctness of the modifications.

📝 Checklist

  • The title is concise and informative.
  • 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 and checked the documentation preview.

@mashraf8
Copy link
Contributor Author

Hi @dcoudert,

I'm new to contributing to SageMath and actively exploring its environment as I prepare for GSoC. My goal is to become a long-term contributor, and I'm gaining familiarity with the codebase through various contributions. Looking forward to your feedback on this PR!

@dcoudert
Copy link
Contributor

Thank you for this PR. I think @janmenjayap should check as he is the main contributor of this module.

I realize that we have no control on the algorithm used to check whether the graph is matching covered. It was already the case. May be we could store the parameters used for creating the instance and use them when calling is_matching_covered ?

@mashraf8
Copy link
Contributor Author

Hi @dcoudert,

Thank you for your suggestion! I've taken it into account and implemented it. Now, the instance parameters are preserved and used when calling is_matching_covered. This truly ensures better control over the validation process while preventing any contradictions with the instance's settings.

Additionally, storing these parameters provides flexibility for potential future use in other validations or optimizations within the module.

@janmenjayap
Copy link
Contributor

Thank you @mashraf8 for this PR.

The way currently the add_edge() has been implemented is with $$\mathcal{O}(|E| \cdot |V|)$$ time and the same space complexity. The current one suggested does not create a new graph, thus does not consume any extra space, but it also (as it was implemented earlier) does a check whether the whole graph is matching covered or not through is_matching_covered(), which is of $$\mathcal{O}(|E| \cdot |V|)$$ time complexity.

I have mistakenly missed/ overlooked a better implementation of add_edge() earlier, which can be achieved at $$\mathcal{O}(|E|)$$ using the existing matching (self.get_matching()). Essentially, since the base graph is matching covered, observe that each edge participates in some perfect matching. We just need to ensure that the new edge added participates in some perfect matching.

Case 1:
If the new edge added is a parallel/ multiple edge, then clearly it is admissible (aka there exists some perfect matching containing this edge). Hence we can directly add that edge using _backend.

Case 2:
If the new edge uv added is a nonparallel edge joining two existing vertices u and v, we can check whether this edge is admissible using M_alternating_even_mark() method by checking whether there exists an M alternating odd uv path starting and ending with matched edges (this will take $$\mathcal{O}(|E|)$$), using self.get_matching() as the reference perfect matching.

@janmenjayap
Copy link
Contributor

I realize that we have no control on the algorithm used to check whether the graph is matching covered. It was already the case. May be we could store the parameters used for creating the instance and use them when calling is_matching_covered ?

Hi,
As of now, the perfect matching stored self._matching is sufficient enough for the matching covered check using the algorithm implemented with M_alternating_even_mark() method. This is crucial even for some further methods that are to be implemented subsequently, for instance concerning removable edges etc. The other useful parameters and attributes (some of which might be invariant wrt the specific matching covered graph, for instance number of bricks, number of Petersen bricks, separating cut characteristic, etc.), can be stored subsequently as they appear in the further implementations.

Comment on lines 648 to 659
# Store all parameters used in this instance for reference if needed
self._params = {
"data": data,
"matching": self._matching,
"algorithm": algorithm,
"solver": solver,
"verbose": verbose,
"integrality_tolerance": integrality_tolerance,
"args": args,
"kwds": kwds
}

Copy link
Contributor

Choose a reason for hiding this comment

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

The parameters data, args and kwds are essentially consumed by Graph.__init__() method, while creating an instance of the matching covered graph. Thus, all the information and attributes of those parameters can be directly accessed from self (for example immutability, or edges being weighted).

Other parameters, that are $-$ algorithm, solver, verbose, integrality_tolerance are used for obtaining self._matching, and are not required afterwards.

This commit may be reverted.

cc: @dcoudert.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with you if you use the algorithm you proposed.

Comment on lines 1156 to 1162

# Add the new edge to the graph using the original method from the base class
super().add_edge(u, v, label=label)
# Check if the graph is still matching covered after adding the edge
if not self.is_matching_covered(**{k: self._params[k] for k in ["algorithm", "solver", "verbose","integrality_tolerance"]}):
# If it is no longer matching covered, immediately remove the edge to restore the previous state and raise an exception
super().delete_edge(u, v, label=label)
Copy link
Contributor

@janmenjayap janmenjayap Mar 13, 2025

Choose a reason for hiding this comment

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

Suggested change
# Add the new edge to the graph using the original method from the base class
super().add_edge(u, v, label=label)
# Check if the graph is still matching covered after adding the edge
if not self.is_matching_covered(**{k: self._params[k] for k in ["algorithm", "solver", "verbose","integrality_tolerance"]}):
# If it is no longer matching covered, immediately remove the edge to restore the previous state and raise an exception
super().delete_edge(u, v, label=label)
# If (u, v, label) is a multiple edge/ an existing edge
if self.has_edge(u, v):
self._backend.add_edge(u, v, label, self._directed)
return
# Check if there exists an M-alternating odd uv path starting and
# ending with edges in self._matching
from sage.graphs.matching import M_alternating_even_mark
w = next((b if a == u else a) for a, b, *_ in self.get_matching() if u in (a, b))
if v in M_alternating_even_mark(self, w, self.get_matching()):
# There exists a perfect matching containing the edge (u, v, label)
self._backend.add_edge(u, v, label, self._directed)
return

Comment on lines 1166 to 1168
# Update the matching only if the graph remains matching covered after adding the edge
self._matching = self.get_matching()

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Update the matching only if the graph remains matching covered after adding the edge
self._matching = self.get_matching()

Comment on lines 1163 to 1165
raise ValueError('the graph obtained after the addition of '
'edge (%s) is not matching covered'
% str((u, v, label)))
Copy link
Contributor

@janmenjayap janmenjayap Mar 13, 2025

Choose a reason for hiding this comment

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

Suggested change
raise ValueError('the graph obtained after the addition of '
'edge (%s) is not matching covered'
% str((u, v, label)))
raise ValueError('the graph obtained after the addition of edge (%s)'
'is not matching covered' % str((u, v, label)))

@janmenjayap
Copy link
Contributor

janmenjayap commented Mar 13, 2025

1159 -       else:
1160 -            # At least one of u or v is a nonexistent vertex.
1161 -            # Thus, the resulting graph is either disconnected
1162 -            # or has an odd order, hence not matching covered
1163 -            raise ValueError('the graph obtained after the addition of edge '
1164 -                             '(%s) is not matching covered'
1165 -                             % str((u, v, label)))

Please remove these lines, as this will be taken care by the last commit of raise ValueError(...).

Let me know if any other query is there in this regard.
Thank you.

@dcoudert
Copy link
Contributor

It's better to use if self.has_edge(u, v) and not if u in self.neighbors(v):

@mashraf8
Copy link
Contributor Author

Hi @janmenjayap,

Thank you so much for all your suggestions! I’ve actually been looking into changing is_matching_covered, and your discovery of M_alternating_even_mark was incredibly effective. As for self_params, we don’t need to use it currently, so it has been removed due to the removal of is_matching_covered.

At this last commit, is there anything else you would suggest?

Thanks again!

@dcoudert
Copy link
Contributor

I tried this PR and all tests pass.
@janmenjayap, do we have all needed tests to check that the behavior is ok ? I think so, but you should know better.

@janmenjayap
Copy link
Contributor

Yes.
We have all the necessary doc tests.

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.

some minor details.


except Exception:
raise ValueError('the graph obtained after the addition of '

Copy link
Contributor

Choose a reason for hiding this comment

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

remove trailing white spaces (this line should be empty)

# There exists a perfect matching containing the edge (u, v, label)
self._backend.add_edge(u, v, label, self._directed)
return

Copy link
Contributor

Choose a reason for hiding this comment

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

remove trailing white spaces (this line should be empty)

self._backend.add_edge(u, v, label, self._directed)
return

raise ValueError('the graph obtained after the addition of '
Copy link
Contributor

Choose a reason for hiding this comment

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

improve alignment like

        raise ValueError('the graph obtained after the addition of edge '
                         '(%s) is not matching covered' % str((u, v, label)))

Comment on lines 1148 to 1149
return
# Check if there exists an M-alternating odd uv path starting and
Copy link
Contributor

@janmenjayap janmenjayap Mar 15, 2025

Choose a reason for hiding this comment

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

Suggested change
return
# Check if there exists an M-alternating odd uv path starting and
return
# Check if there exists an M-alternating odd uv path starting and

Add an empty line in between.

Comment on lines 1152 to 1153
w = next((b if a == u else a) for a, b, *_ in self.get_matching() if u in (a, b))
if v in M_alternating_even_mark(self, w, self.get_matching()):
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
w = next((b if a == u else a) for a, b, *_ in self.get_matching() if u in (a, b))
if v in M_alternating_even_mark(self, w, self.get_matching()):
w = next((b if a == u else a) for a, b, *_ in self.get_matching() if u in (a, b))
if v in M_alternating_even_mark(self, w, self.get_matching()):

Add an empty line in between.

@mashraf8
Copy link
Contributor Author

Thank you, @dcoudert and @janmenjayap
This is my first contribution to Sage, and I will make sure to avoid these minor details in the future.
Let me know if there's anything else to improve.

@janmenjayap
Copy link
Contributor

No worries, @mashraf8.

Welcome to SageMath.
Hope you have a great journey ahead!

@janmenjayap
Copy link
Contributor

Looks good to me.
cc: @dcoudert.

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.

LGTM. Passes all tests on my laptop.

@dcoudert
Copy link
Contributor

@vbraun, I cannot launch the CIs myself. I let you launch them if necessary.
I have tested this PR on my laptop and it works well. For me it's good to go.
Thanks.

Copy link

Documentation preview for this PR (built with commit 820d609; changes) is ready! 🎉
This preview will update shortly after each push to this PR.

vbraun pushed a commit to vbraun/sage that referenced this pull request Mar 19, 2025
sagemathgh-39678: Optimize MatchingCoveredGraph’s add_edge method
    
The `add_edge` method has been optimized to ensure that edges are added
while preserving the **matching-covered** property without fully
reinitializing or creating a new graph.

First, the edge is added using the base method, then the graph is
checked to confirm that it remains matching-covered. If it is no longer
valid, the edge is immediately removed to restore the previous state.
The matching is updated only if the graph remains valid.

**Doctests** were successfully executed to verify the correctness of the
modifications.



### 📝 Checklist
- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [ ] I have updated the documentation and checked the documentation
preview.
    
URL: sagemath#39678
Reported by: Mohammed Ashraf
Reviewer(s): David Coudert, Janmenjaya Panda
@vbraun vbraun merged commit dab7799 into sagemath:develop Mar 22, 2025
24 checks passed
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.

4 participants