Giter Site home page Giter Site logo

namedgraphs.jl's Introduction

NamedGraphs

Stable Dev Build Status Coverage Code Style: Blue

Installation

You can install the package using Julia's package manager:

julia> ] add NamedGraphs

Introduction

This packages introduces graph types with named vertices, which are built on top of the Graph/SimpleGraph type in the Graphs.jl package that only have contiguous integer vertices (i.e. linear indexing). The vertex names can be strings, tuples of integers, or other unique identifiers (anything that is hashable).

There is a supertype AbstractNamedGraph that defines an interface and fallback implementations of standard Graphs.jl operations, and two implementations: NamedGraph and NamedDiGraph.

NamedGraph

NamedGraph simply takes a set of names for the vertices of the graph. For example:

julia> using Graphs: grid, has_edge, has_vertex, neighbors

julia> using NamedGraphs: NamedGraph

julia> using NamedGraphs.GraphsExtensions: , disjoint_union, subgraph, rename_vertices

julia> g = NamedGraph(grid((4,)), ["A", "B", "C", "D"])
NamedGraphs.NamedGraph{String} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{String}
 "A"
 "B"
 "C"
 "D"

and 3 edge(s):
"A" => "B"
"B" => "C"
"C" => "D"

Common operations are defined as you would expect:

julia> has_vertex(g, "A")
true

julia> has_edge(g, "A" => "B")
true

julia> has_edge(g, "A" => "C")
false

julia> neighbors(g, "B")
2-element Vector{String}:
 "A"
 "C"

julia> subgraph(g, ["A", "B"])
NamedGraphs.NamedGraph{String} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{String}
 "A"
 "B"

and 1 edge(s):
"A" => "B"

Internally, this type wraps a SimpleGraph, and stores a Dictionary from the Dictionaries.jl package that maps the vertex names to the linear indices of the underlying SimpleGraph.

Graph operations are implemented by mapping back and forth between the generalized named vertices and the linear index vertices of the SimpleGraph.

It is natural to use tuples of integers as the names for the vertices of graphs with grid connectivities. For example:

julia> dims = (2, 2)
(2, 2)

julia> g = NamedGraph(grid(dims), Tuple.(CartesianIndices(dims)))
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 1)
 (1, 2)
 (2, 2)

and 4 edge(s):
(1, 1) => (2, 1)
(1, 1) => (1, 2)
(2, 1) => (2, 2)
(1, 2) => (2, 2)

In the future we will provide a shorthand notation for this, such as cartesian_graph(grid((2, 2)), (2, 2)). Internally the vertices are all stored as tuples with a label in each dimension.

Vertices can be referred to by their tuples:

julia> has_vertex(g, (1, 1))
true

julia> has_edge(g, (1, 1) => (2, 1))
true

julia> has_edge(g, (1, 1) => (2, 2))
false

julia> neighbors(g, (2, 2))
2-element Vector{Tuple{Int64, Int64}}:
 (2, 1)
 (1, 2)

You can use vertex names to get induced subgraphs:

julia> subgraph(v -> v[1] == 1, g)
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (1, 2)

and 1 edge(s):
(1, 1) => (1, 2)


julia> subgraph(v -> v[2] == 2, g)
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 2)
 (2, 2)

and 1 edge(s):
(1, 2) => (2, 2)


julia> subgraph(g, [(1, 1), (2, 2)])
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 2 vertices:
2-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 2)

and 0 edge(s):

You can also take disjoint unions or concatenations of graphs:

julia> g₁ = g
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 1)
 (1, 2)
 (2, 2)

and 4 edge(s):
(1, 1) => (2, 1)
(1, 1) => (1, 2)
(2, 1) => (2, 2)
(1, 2) => (2, 2)


julia> g₂ = g
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 1)
 (1, 2)
 (2, 2)

and 4 edge(s):
(1, 1) => (2, 1)
(1, 1) => (1, 2)
(2, 1) => (2, 2)
(1, 2) => (2, 2)


julia> disjoint_union(g₁, g₂)
NamedGraphs.NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices:
8-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Tuple{Int64, Int64}, Int64}}
 ((1, 1), 1)
 ((2, 1), 1)
 ((1, 2), 1)
 ((2, 2), 1)
 ((1, 1), 2)
 ((2, 1), 2)
 ((1, 2), 2)
 ((2, 2), 2)

and 8 edge(s):
((1, 1), 1) => ((2, 1), 1)
((1, 1), 1) => ((1, 2), 1)
((2, 1), 1) => ((2, 2), 1)
((1, 2), 1) => ((2, 2), 1)
((1, 1), 2) => ((2, 1), 2)
((1, 1), 2) => ((1, 2), 2)
((2, 1), 2) => ((2, 2), 2)
((1, 2), 2) => ((2, 2), 2)


julia> g₁  g₂ # Same as above
NamedGraphs.NamedGraph{Tuple{Tuple{Int64, Int64}, Int64}} with 8 vertices:
8-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Tuple{Int64, Int64}, Int64}}
 ((1, 1), 1)
 ((2, 1), 1)
 ((1, 2), 1)
 ((2, 2), 1)
 ((1, 1), 2)
 ((2, 1), 2)
 ((1, 2), 2)
 ((2, 2), 2)

and 8 edge(s):
((1, 1), 1) => ((2, 1), 1)
((1, 1), 1) => ((1, 2), 1)
((2, 1), 1) => ((2, 2), 1)
((1, 2), 1) => ((2, 2), 1)
((1, 1), 2) => ((2, 1), 2)
((1, 1), 2) => ((1, 2), 2)
((2, 1), 2) => ((2, 2), 2)
((1, 2), 2) => ((2, 2), 2)

The symbol is just an alias for disjoint_union and can be written in the terminal or in your favorite IDE with the appropriate Julia extension with \sqcup<tab>

By default, this maps the vertices v₁ ∈ vertices(g₁) to (v₁, 1) and the vertices v₂ ∈ vertices(g₂) to (v₂, 2), so the resulting vertices of the unioned graph will always be unique. The resulting graph will have no edges between vertices (v₁, 1) and (v₂, 2), these would have to be added manually.

The original graphs can be obtained from subgraphs:

julia> rename_vertices(first, subgraph(v -> v[2] == 1, g₁  g₂))
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 1)
 (1, 2)
 (2, 2)

and 4 edge(s):
(1, 1) => (2, 1)
(1, 1) => (1, 2)
(2, 1) => (2, 2)
(1, 2) => (2, 2)


julia> rename_vertices(first, subgraph(v -> v[2] == 2, g₁  g₂))
NamedGraphs.NamedGraph{Tuple{Int64, Int64}} with 4 vertices:
4-element NamedGraphs.OrderedDictionaries.OrderedIndices{Tuple{Int64, Int64}}
 (1, 1)
 (2, 1)
 (1, 2)
 (2, 2)

and 4 edge(s):
(1, 1) => (2, 1)
(1, 1) => (1, 2)
(2, 1) => (2, 2)
(1, 2) => (2, 2)

Generating this README

This file was generated with Weave.jl with the following commands:

using NamedGraphs: NamedGraphs
using Weave: Weave
Weave.weave(
  joinpath(pkgdir(NamedGraphs), "examples", "README.jl");
  doctype="github",
  out_path=pkgdir(NamedGraphs),
)

namedgraphs.jl's People

Contributors

emstoudenmire avatar joeyt1994 avatar leburgel avatar linjianma avatar mtfishman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

namedgraphs.jl's Issues

`similar` for generically constructing graphs of the same/similar type without edges

Graphs.jl doesn't seem to provide a generic interface for creating graphs of the same type without any edges, and possibly with new vertices.

The closest concept in Julia seems to be Base.similar, for example for sparse arrays:

julia> using SparseArrays

julia> A = sprandn(4, 4, 0.5)
4×4 SparseMatrixCSC{Float64, Int64} with 7 stored entries:
                            
 -0.21712       -0.741368   0.199437
                          1.78635
  0.486078       0.593271  -0.199654

julia> similar(A, 4, 4)
4×4 SparseMatrixCSC{Float64, Int64} with 0 stored entries:
               
               
               
               

though for some reason it doesn't work for types:

julia> similar(typeof(A), 4, 4)
ERROR: MethodError: no method matching SparseMatrixCSC{Float64, Int64}(::UndefInitializer, ::Tuple{Int64, Int64})
Closest candidates are:
  SparseMatrixCSC{Tv, Ti}(::LinearAlgebra.UniformScaling, ::Tuple{Int64, Int64}) where {Tv, Ti} at /Applications/Julia-1.8.5.app/Contents/Resources/julia/share/julia/stdlib/v1.8/SparseArrays/src/sparsematrix.jl:1788
Stacktrace:
 [1] similar(#unused#::Type{SparseMatrixCSC{Float64, Int64}}, dims::Tuple{Int64, Int64})
   @ Base ./abstractarray.jl:841
 [2] similar(::Type{SparseMatrixCSC{Float64, Int64}}, ::Int64, ::Int64)
   @ Base ./abstractarray.jl:839
 [3] top-level scope
   @ REPL[20]:1

We could adopt the interface:

Base.similar(g::AbstractGraph, vertices)
Base.similar(g::AbstractGraph, nvertices::Integer)
Base.similar(G::Type{<:AbstractGraph}, vertices)
Base.similar(G::Type{<:AbstractGraph}, nvertices::Integer)

as a way to generically create a graph of the same type G without any edges with either the specified vertex collection or number of vertices. Additionally this could be used as an interface for creating a similar graph with a new vertex type, and new vertex data and edge data types in the case of data graphs.

Originally posted by @mtfishman in #32 (comment)

Make `hexagonal_lattice_graph` more consistent between periodic and non-periodic

Changing the setting of periodic in hexagonal_lattice_graph changes the lattice size:

julia> using NamedGraphs

julia> nv(hexagonal_lattice_graph(2, 2; periodic=false))
16

julia> nv(hexagonal_lattice_graph(2, 2; periodic=true))
8

I think instead it should output graphs with the same number of vertices and structure, just with extra periodic edges if periodic=true. It is implemented that way right now because the periodic graphs are constructed by first constructing the corresponding non-periodic version of the lattice and then merging vertices, it could still be implemented that way with some code restructuring, for example having an inner non-periodic version which the periodic version calls. The current design makes it difficult to reason about what size lattice will get output. (@JoeyT1994)

`Graphs.jl` functions that need to be wrapped

Disable `g[[1, 2, 3]]` for getting an induced subgraph

Disable g[[1, 2, 3]] for getting an induced subgraph. The syntax is ambiguous since it isn't clear of [1, 2, 3] is a list of vertices or a single vertex.

As an alternative, we could define an iterator Vertices such that g[Vertices([1, 2, 3])] unambiguously returns and induced subgraph, with the implicit assumption that Vertices can't be used as vertex names of named graphs.

`internal_edges` function

This is a good idea for a generic graph function. It is the complement of the edge boundary of a subgraph (https://en.wikipedia.org/wiki/Boundary_(graph_theory) and boundary_edges added in #20).

As we discussed, a simple implementation would be internal_edges(g::AbstractGraph, vertices) = edges(subgraph(g, vertices)). Then this function could be defined as internal_edges(P::AbstractProjTTNO) = internal_edges(underlying_graph(P), sites(P)).

Originally posted by @mtfishman in ITensor/ITensorNetworks.jl#18 (comment)

Constructors for decorated graphs

We should think about having construction methods for decorated graphs.

E.g. given a named graph g we have some function like

decorate_graph(g::NamedGraph, edge_map::Function)

where edge_map::Function is a map which takes an edge of g e and outputs the namedgraph which will then be inserted along e in the original graph g,

For example something like

g = named_grid((L,L))
g_dec = decorate_graph(g::NamedGraph, e -> named_grid((1)))

would build the L x L Lieb lattice by inserting a single vertex along the edges of the regular square lattice.

Obviously we would have to think about how to name the new vertices that have been added to g and also how to specify which vertices of the inserted graph connect to src(e) and dst(e) (perhaps by defaulting to the first vertex of the decorated graph with additional specification if desired).

Should Graphs.jl algorithms automatically work on a NamedDimGraph?

I was trying to use some shortest path algorithms from Graphs.jl on NamedDimGraphs, e.g:

image

Using some of the traversal algorithms defined here: https://juliagraphs.org/Graphs.jl/dev/algorithms/shortestpaths/#Graphs.johnson_shortest_paths-Union{Tuple{AbstractGraph{U}},%20Tuple{T},%20Tuple{U},%20Tuple{AbstractGraph{U},%20AbstractMatrix{T}}}%20where%20{U%3C:Integer,%20T%3C:Real}

Is it possible to export these into NamedGraphs.jl? I can then fork and add a function which creates a subgraph of a NamedDimGraph containing only the vertices within a certain pathlength of a source_vertex. Such a function could then be utilised in ITensorNetworks.jl to orthogonalise a network up to some distance r from a source_vertex.

Introduce `cartesian_graph(...)`

Introduce cartesian_graph(dims...) for making a NamedGraph with cartesian vertex names, i.e.:

cartesian_graph(2, 2)

would have vertices [(1, 1), (1, 2), (2, 1), (2, 2)].

Additionally,

cartesian_graph(grid((2, 2)), (2, 2))

to wrap a SimpleGraph in a NamedGraph with cartesian vertices.

Don't reexport anything from `Graphs.jl`

Instead, we will expect users to load Graphs.jl explicitly to access those functions, or they can load them individually with using NamedGraphs: vertices, though that will only work if they have been brought into the namespace of NamedGraphs so may be fragile.

Introduce a PartitionedGraph Type

We should introduce a PartitionedGraph structure. Ideally this should be separate from NamedGraphs and just be an abstract graph type with the relevant functionality. For now though we will make a PartitionedGraphs.jl submodule within NamedGraphs.jl.

The general structure could look like

abstract type AbstractPartitionedGraph{V,PV} <: AbstractGraph{V} end

struct PartitionedGraph{V,PV,G<:AbstractGraph{V},PG<:AbstractGraph{PV}} <: AbstractPartitionedGraph{PV}
  graph::G
  partioned_graph::PG
  partition_vertices::Dictionary{PV,Vector{V}}
  partition::Dictionary{V,PV}
end

such that a partitioned graph essentially consists of two graphs: the original graph G and the partitioned graph PG. The structure then also contains a map (and its reverse) between the subsets of vertices of G which consistute the vertices of PG.

We then need to define functionality for this structure. It would make sense to define standard Graphs.jl calls on a PartitionedGraph to pass to the original graph G. For instance vertices(pg::PartitionedGraph) = vertices(pg.G) Then we define partitioned versions of those calls which operate on PG instead. E.g. partitioned_vertices(pg::PartitionedGraph) = vertices(pg.PG).
Other examples of such functions might be has_vertex versus partitioned_has_vertex, is_tree versus partitioned_is_tree etc.

Helpful functions might then be determining whether an edge of G is within a partition or between partitions. And which partitions.
E.g

function edge_partitions(e::AbstractEdge, PG::PartitionedGraph)
   return find_key(PG.partition, src(e)), find_key(PG.partition, dst(e))
end

function edge_in_partition(e::AbstractEdge, PG::PartitionedGraph)
  return all_equal(edge_partitions(e, PG))
end

Another question is whether we define types such as PartitionedVertex and PartitionedEdge so that calls such as has_vertex(PG, PartitionedVertex(1)) pass directly to partitioned_has_vertex. This is mostly for convenience.

@mtfishman

Lattices to add

Lattices to add to NamedGraphs.NamedGraphGenerators:

  • Kagome

See also #84.

`named_*(vertices)`

Add support in functions like named_path_graph, named_grid, etc. for specifying custom vertex names with a function, dictionary, array, etc.

For example:

named_path_graph(["a", "b", "c"])
named_path_graph(v -> "v$v", 3)
named_grid(["a" "b"; "c" "d"])
named_grid(v -> "v$(v[1]),$(v[2])", (2, 2))

Throw an error if `named_hexagonal_lattice_graph(n, m; periodic = true)` is called with `n` or `m` = 2

We should throw an error if named_hexagonal_lattice_graph(n, m; periodic = true) is called with n =2 or m = 2 because this will output a graph that essentially is a ladder (with shortest loop size 4) and resembles nothing like a hexagonal graph so may be misleading to the user.

This is likely an issue implicitly tied to the hexagonal lattice itself (hexagonal_lattice_graph for Networkx in Python also has the same output).

More sophisticated lattice boundary conditions

Something that is missing from the current lattice definitions like square/grid, triangular, and hexagonal is the concept of orienting those lattices in different ways, for example rotating them by a certain angle, which can be useful for 1D tensor network algorithms like DMRG where it is common practice to perform the same calculation on cylinders built from wrapping the same lattice in different ways to see how the ground state changes or doesn't change.

This may require adding on some concept of translation vectors. A simple approach could be to add positions as metadata on the vertices of the graph representations of the lattices (say using DataGraphs.jl, where the vertex data storing the positions could be the Point type from GeometryBasics.jl). Then, lattice rotations could be geometric operations on those positions, and then lattice wrapping could be done through lattice rotations and then cutting certain edges of the lattice (say by only keeping vertices that have positions that fall within a certain rectangular region of space), and then adding back edges to implement periodic boundary conditions.

@JoeyT1994 @emstoudenmire

Orientation of edges in `incident_edges`

Currently incident_edges(graph, vertex) returns a list of edges which are directed away from vertex towards its neighbors:

https://github.com/mtfishman/NamedGraphs.jl/blob/869194950011956ca5c5b056a84550e7f898fa7c/src/Graphs/abstractgraph.jl#L41

I thought it might make more sense to reverse the direction to have the incident_edges point towards the vertex instead. Not that this is very relevant by itself, but I bumped into it when overloading this for effective Hamiltonians on trees in which case I definitely wanted to have the incident_edges pointing inward (https://github.com/leburgel/ITensorNetworks.jl/blob/0eb9ef98ab7d758117c3918452c9fc8831b4e914/src/treetensornetwork/abstractprojttno.jl#L27).

If there was a particular reason to have them point outward in the first place then I could just as well change it in the ITensorNetworks PR, since it would be best to stick to the same convention everywhere.

Naming convention for `NamedGraph` graph generators

Going forward we should always adopt the naming convention that functions f which generate NamedGraph types are prefixed with named_f whilst the SimpleGraph version is simply f.

So for instance having named_hexagonal_lattice_graph versus hexagonal_lattice_graph and named_triangular_lattice_graph versus triangular_lattice_graph (two instances where we currently don't follow that convention).

We should also introduce:

named_random_regular, named_uniform_tree and others from [https://juliagraphs.org/Graphs.jl/dev/core_functions/simplegraphs_generators/] we wish to have easy constructors for.

More `GraphsExtensions` tests

As a follow-up to #69, the functions in GraphsExtensions still need to be tested:

  • post_order_dfs_vertices
  • pre_order_dfs_vertices
  • post_order_dfs_edges
  • vertex_path
  • edge_path
  • parent_vertices
  • parent_vertex
  • parent_edges
  • parent_edge
  • mincut_partitions
  • eccentricities
  • decorate_graph_edges
  • decorate_graph_vertices
  • random_bfs_tree

Implement `next_nearest_neighbors`

Implement next_nearest_neighbors(g, v) which returns the next-to-nearest neighbors of a vertex v in g.

It looks like we could make use of Graphs.neighborhood_dists to create a more general function for outputting the vertices that are the k-nearest neighbors of a specified vertex, for example:

function k_nearest_neighbors(g::AbstractGraph, v, d)
  nds = neighborhood_dists(g, v, d)
  # Filter `nds` for vertices with distances equal to `d`
end

Then next_nearest_neighbors(g, v) = k_nearest_neighbors(g, v, 2).

Originally posted by @mtfishman in ITensor/ITensorNetworks.jl#18 (comment)

[BUG] Possible bug in `steiner_tree`

Currently, the steiner_tree function reproduces the same behavior that it has for SimpleGraph where in that case, all vertices in the range 1:nv(steiner_tree(g,...)) remain. However, it may be that NamedGraphs should not have this behavior, and that only vertices which are reachable by traversing the Steiner tree should be included in the returned graph.

Here is a small code demonstrating the current behavior and output:

using Graphs: add_edge!, edges, nv, steiner_tree, vertices
using NamedGraphs

g = NamedGraph(["a","b","c"])
add_edge!(g,"a","c")
add_edge!(g,"a","b")

st = steiner_tree(g,["a","c"])
@show st

Output:

st = NamedGraph{String} with 3 vertices:
3-element NamedGraphs.OrderedDictionaries.OrderedIndices{String}
 "a"
 "b"
 "c"

and 1 edge(s):
"a" => "c"

and one can confirm that in this case nv(st) == 3.

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

Change the design of `GenericNamedGraph` type

Change the design of parent_graph, vertex_to_parent_vertex, etc. using a type like OrderedCollections.OrderedSet, AcceleratedArrays.jl, or OrdinalIndexing.jl. A better name for parent_graph would be ordinal_graph (indicating the vertices are ordered integers), and ordinal_vertices/ordinal_vertex could refer to the integer vertices of the ordinal_graph. vertices could be an ordered AbstractVector of the named vertices with fast lookup of the vertex positions in the ordinal_graph.

Name change summary:

  • parent_graph -> ordinal_graph
  • vertex_to_parent_vertex -> ordinal_vertex, ordinal_vertices
  • parent_vertex_to_vertex -> to_vertex, ordered_vertices

Add `pre_order_dfs_edges`

Currently there are the following functions:

  • post_order_dfs_vertices
  • post_order_dfs_edges
  • pre_order_dfs_vertices

So it could be good to add pre_order_dfs_edges.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.