Skip to content

feat: directed vertices record incoming edges; degree is in plus out; validate(); BaseGraph - #40

Merged
petercorke merged 7 commits into
mainfrom
feat/directed-in-edges
Oct 11, 2026
Merged

petercorke merged 7 commits into
mainfrom
feat/directed-in-edges

Conversation

@petercorke

Copy link
Copy Markdown
Owner

Summary

Directed vertices now record the edges that end at them as well as the ones that leave them, and the rest of the in-edge / degree / hygiene work that follows from it. Six commits, each verified green on exactly its own contents:

  1. BaseGraph is the public name of the common base class (_BaseGraph was never released, so no alias); BaseGraph and BaseVertex are exported with an __all__.
  2. In-edges and degree. predecessors(), successors() (= adjacent()), in_edges(), out_edges(), in_degree, out_degree. remove_vertex finds a vertex's edges from its own lists (O(degree), was O(E)); iscyclic uses in-degrees (O(V), was O(E)).
  3. graph.validate() returns a list of the ways the graph's records disagree (empty when consistent). It does not change the graph, costs O(V+E) and never raises on corruption.
  4. Hygiene. Vertex.label is read-only; assigning Vertex.name updates the graph's name table, refuses a duplicate or None; exporters use only the public API.
  5. add_vertex names. A duplicate name raises ValueError and changes nothing; the default #N name is the first free one (it could collide after a removal); a vertex that already belongs to a graph is refused.

Breaking change for DGraph (1.0)

degree is now the graph-theory degree, in plus out, so a self-loop counts 2 and the degrees of a graph sum to twice its edges. It counted only the edges leaving the vertex.

Changed Old value
vertex.degree vertex.out_degree
graph.degree() np.diag([v.out_degree for v in g])
graph.connectivity() [v.out_degree for v in g]
graph.average_degree() is 2E/N g.ne / g.n

Laplacian() is unchanged (it is D_out - A, and now takes the out-degree explicitly). For an undirected graph every quantity is as before. Each is documented with a versionchanged note. A survey of RTB, MVTB and RVC3 found no caller of any of them on a DGraph.

Tests

New: test_in_edges, test_validate (27 deliberate corruptions each detected and named; random edit sequences stay valid), test_attribute_hygiene, test_add_vertex_names.

🤖 Generated with Claude Code

petercorke and others added 6 commits October 12, 2026 09:21
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The class was named _BaseGraph when PGraph was split into a base class and
UGraph/DGraph, but it is in the signature of every graph method, is shown in
the documentation, and is what a user writes in a type hint, while its
counterpart BaseVertex has no underscore. _BaseGraph was never released, so
there is no alias.

BaseGraph and BaseVertex are exported from pgraph, with an __all__.

Mechanical: with the rename reversed, PGraph.py and exporters.py are
byte-identical to the previous commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s out

A directed vertex now holds the edges that end at it as well as the ones that
leave it, kept in step by _attach/_detach. That gives:

- predecessors(), successors() (which is adjacent()), in_edges(), out_edges()
- in_degree and out_degree
- remove_vertex finding a vertex's edges from its own lists rather than
  scanning every edge in the graph (O(degree), was O(E))
- iscyclic taking in-degrees from the vertices (O(V), was O(E))

BREAKING, for a DGraph: degree is now the graph-theory degree, in plus out, so
a self-loop counts 2 and the degrees of a graph sum to twice its edges. It
counted only the edges leaving the vertex. This changes:

  vertex.degree              (use out_degree for the old value)
  graph.degree()             (np.diag([v.out_degree for v in g]))
  graph.connectivity()       ([v.out_degree for v in g])
  graph.average_degree()     2E/N, was E/N (g.ne / g.n)

Laplacian() is unchanged: it is D_out - A, and now takes the out-degree
explicitly instead of relying on degree(). For an undirected graph every
quantity is as before. Each is documented with a versionchanged note, and a
survey of RTB, MVTB and RVC3 found no caller of any of them on a DGraph.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A graph keeps the same facts in several places: its set of edges, each
vertex's outgoing (and, if directed, incoming) edge lists, each edge's two
vertices, each vertex's pointer back to its graph, and the name table.
validate() returns a list of the ways they disagree, empty when consistent.
It does not change the graph, costs O(V+E), reports every problem rather than
the first, and does not raise on corruption, including stray objects in the
containers.

For tests, debugging, and code that reaches into the internals; a graph built
and edited through the public methods is always valid.

27 deliberate corruptions, one per kind of record, are each detected and
named; random edit sequences stay valid.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a graph

label     read-only. It is the component number, computed by the graph.
name      assigning to the name of a vertex that is in a graph updates the
          graph's name table, refuses a name already in use (ValueError) and
          refuses to clear it. It left the table stale before, so g[new]
          failed.
exporters use the public API: out_edges() instead of a vertex's private
          _edgelist, and get_exporter(name) instead of the private table.
docs      BaseVertex no longer lists _edgelist as an attribute.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…name

Names are how g[name] finds a vertex, so they must be unique in a graph.

- adding a vertex whose name is already used raised nothing and replaced the
  earlier vertex in the name table, leaving it in the graph but unreachable
  by name; it now raises ValueError and changes nothing
- the default name #N used the number of vertices, so after a removal it
  could equal an existing name (remove #0 from #0 #1 #2, add one: #2 twice);
  it now takes the first #N that is free
- a vertex that already belongs to a graph, this one or another, could be
  added: it was then listed by both. It now raises ValueError; a removed
  vertex can be added again, or elsewhere
- every check precedes any change, so a refused add leaves the graph and the
  vertex exactly as they were

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 166 complexity · 2 duplication

Metric Results
Complexity 166
Duplication 2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.41520% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.60%. Comparing base (9ac0001) to head (8cab999).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
src/pgraph/PGraph.py 99.37% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #40      +/-   ##
==========================================
+ Coverage   93.86%   94.60%   +0.73%     
==========================================
  Files           3        3              
  Lines        1076     1205     +129     
==========================================
+ Hits         1010     1140     +130     
+ Misses         66       65       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@petercorke
petercorke merged commit e68d477 into main Oct 11, 2026
12 checks passed
@petercorke
petercorke deleted the feat/directed-in-edges branch October 11, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant