Skip to content

Commit 832d776

Browse files
authored
[FEA] Add high level Graph class (#367)
* add Series.scale * symlink node_modules so we can mount notebooks in for local dev * add TS Graph class, GraphCOO.degree implementation * add basic Graph tests
1 parent db7fd66 commit 832d776

16 files changed

Lines changed: 405 additions & 137 deletions

File tree

dev/dockerfiles/devel/notebook.Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ if __name__ == \"__main__\":\n\
3535
\"--hide-undefined\",\n\
3636
\"{connection_file}\",\n\
3737
\"--protocol=5.0\",\n\
38-
\"--session-working-dir=/opt/rapids/node\"\n\
38+
\"--session-working-dir=/opt/rapids\"\n\
3939
],\n\
4040
\"name\": \"javascript\",\n\
4141
\"language\": \"javascript\",\n\
4242
\"display_name\": \"Javascript (Node.js)\"\n\
4343
}' > /opt/rapids/.local/share/jupyter/kernels/javascript/kernel.json" \
4444
&& chmod 0644 /opt/rapids/.local/share/jupyter/kernels/javascript/logo-{32x32,64x64}.png \
45+
&& ln -s /opt/rapids/node/node_modules /opt/rapids/node_modules \
4546
&& mkdir -p /opt/rapids/.jupyter \
4647
&& mkdir -p /opt/rapids/.config/jupyterlab-desktop/lab/user-settings/@jupyterlab/apputils-extension \
4748
&& bash -c "echo -e '{\n\

modules/cudf/src/column/filling.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <cudf/column/column.hpp>
2121
#include <cudf/filling.hpp>
2222
#include <cudf/types.hpp>
23+
2324
#include <rmm/device_buffer.hpp>
2425

2526
#include <napi.h>
@@ -89,7 +90,8 @@ Napi::Value Column::sequence(Napi::CallbackInfo const& info) {
8990
if (!Scalar::IsInstance(args[1])) {
9091
throw Napi::Error::New(info.Env(), "sequence init argument expects a scalar");
9192
}
92-
auto& init = *Scalar::Unwrap(args[1]);
93+
94+
Scalar::wrapper_t init = args[1].As<Napi::Object>();
9395

9496
if (args.Length() == 3) {
9597
rmm::mr::device_memory_resource* mr = args[2];
@@ -98,7 +100,7 @@ Napi::Value Column::sequence(Napi::CallbackInfo const& info) {
98100
if (!Scalar::IsInstance(args[2])) {
99101
throw Napi::Error::New(info.Env(), "sequence step argument expects a scalar");
100102
}
101-
auto& step = *Scalar::Unwrap(args[2]);
103+
Scalar::wrapper_t step = args[2].As<Napi::Object>();
102104
rmm::mr::device_memory_resource* mr = args[3];
103105
return Column::sequence(info.Env(), size, init, step, mr);
104106
}

modules/cudf/src/node_cudf/column.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,23 @@ struct Column : public EnvLocalObjectWrap<Column> {
669669
cudf::scalar const& step,
670670
rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource());
671671

672+
inline static Column::wrapper_t sequence(
673+
Napi::Env const& env,
674+
cudf::size_type size,
675+
Scalar::wrapper_t const& init,
676+
rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()) {
677+
return sequence(env, size, init->operator cudf::scalar&(), mr);
678+
}
679+
680+
inline static Column::wrapper_t sequence(
681+
Napi::Env const& env,
682+
cudf::size_type size,
683+
Scalar::wrapper_t const& init,
684+
Scalar::wrapper_t const& step,
685+
rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()) {
686+
return sequence(env, size, init->operator cudf::scalar&(), step->operator cudf::scalar&(), mr);
687+
}
688+
672689
// column/transform.cpp
673690
std::pair<std::unique_ptr<rmm::device_buffer>, cudf::size_type> nans_to_nulls(
674691
rmm::mr::device_memory_resource* mr = rmm::mr::get_current_device_resource()) const;

modules/cudf/src/series/numeric.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,4 +1590,17 @@ export abstract class NumericSeries<T extends Numeric> extends Series<T> {
15901590
}
15911591
return this._col.any(memoryResource);
15921592
}
1593+
1594+
/**
1595+
* @summary Scale values to [0, 1] in float64
1596+
*
1597+
* @param memoryResource The optional MemoryResource used to allocate the result Column's device
1598+
* memory.
1599+
*
1600+
* @returns Series with values scaled between [0, 1].
1601+
*/
1602+
scale(memoryResource?: MemoryResource) {
1603+
const [min, max] = this.minmax() as [any, any];
1604+
return this.sub(min).div(max - min, memoryResource);
1605+
}
15931606
}

modules/cugraph/src/graph.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) 2022, NVIDIA CORPORATION.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Float32Buffer, MemoryView} from '@rapidsai/cuda';
16+
import {DataFrame, DataType, Int32, scope, Series} from '@rapidsai/cudf';
17+
import {DeviceBuffer} from '@rapidsai/rmm';
18+
19+
import {GraphCOO} from './addon';
20+
import {CUGraph, ForceAtlas2Options} from './node_cugraph';
21+
import {renumberEdges, renumberNodes} from './renumber';
22+
23+
export interface GraphOptions {
24+
directedEdges?: boolean;
25+
}
26+
27+
export class Graph<T extends DataType = any> {
28+
public static fromEdgeList<T extends DataType>(src: Series<T>,
29+
dst: Series<T>,
30+
options: GraphOptions = {directedEdges: true}) {
31+
const nodes = renumberNodes(src, dst);
32+
const edges = renumberEdges(src, dst, nodes);
33+
return new Graph(nodes, edges, options);
34+
}
35+
36+
protected constructor(nodes: DataFrame<{id: Int32, node: T}>,
37+
edges: DataFrame<{id: Int32, src: Int32, dst: Int32}>,
38+
options: GraphOptions = {directedEdges: true}) {
39+
this._edges = edges;
40+
this._nodes = nodes;
41+
this._directed = options?.directedEdges ?? true;
42+
}
43+
44+
declare protected _nodes: DataFrame<{id: Int32, node: T}>;
45+
declare protected _edges: DataFrame<{id: Int32, src: Int32, dst: Int32}>;
46+
declare protected _directed: boolean;
47+
48+
declare protected _graph: CUGraph;
49+
50+
protected get graph(): CUGraph {
51+
return this._graph || (this._graph = new GraphCOO(this._edges.get('src')._col,
52+
this._edges.get('dst')._col,
53+
{directedEdges: this._directed}));
54+
}
55+
56+
/**
57+
* @summary The number of edges in this Graph
58+
*/
59+
public get numEdges() { return this.graph.numEdges(); }
60+
61+
/**
62+
* @summary The number of nodes in this Graph
63+
*/
64+
public get numNodes() { return this.graph.numNodes(); }
65+
66+
public get nodes() { return this._nodes.drop(['id']); }
67+
68+
public get edges() {
69+
const unnumber = (typ: 'src'|'dst') => {
70+
const id = this._edges.get(typ);
71+
const eid = this._edges.get('id');
72+
const lhs = new DataFrame({id, eid});
73+
const rhs = this._nodes.rename({node: typ});
74+
return lhs.join({on: ['id'], other: rhs});
75+
};
76+
77+
return scope(() => unnumber('src') //
78+
.join({on: ['eid'], other: unnumber('dst')})
79+
.sortValues({eid: {ascending: true}}),
80+
[this])
81+
.rename({eid: 'id'})
82+
.select(['id', 'src', 'dst']);
83+
}
84+
85+
public get nodeIds() { return this._nodes.select(['id']); }
86+
87+
public get edgeIds() { return this._edges.select(['id', 'src', 'dst']); }
88+
89+
/**
90+
* @summary Compute the total number of edges incident to a vertex (both in and out edges).
91+
*/
92+
public degree() {
93+
return new DataFrame({id: this._nodes.get('id')._col, degree: this.graph.degree()});
94+
}
95+
96+
/**
97+
* @summary ForceAtlas2 is a continuous graph layout algorithm for handy network visualization.
98+
*
99+
* @note Peak memory allocation occurs at 30*V.
100+
*
101+
* @param {ForceAtlas2Options} options
102+
*
103+
* @returns {Float32Buffer} The new positions.
104+
*/
105+
public forceAtlas2(options: ForceAtlas2Options = {}) {
106+
const {numNodes} = this;
107+
let positions: Float32Buffer|undefined;
108+
if (options.positions) {
109+
positions = options.positions ? new Float32Buffer(options.positions instanceof MemoryView
110+
? options.positions?.buffer
111+
: options.positions)
112+
: undefined;
113+
if (positions && positions.length !== numNodes * 2) {
114+
// reallocate new positions and copy over old X/Y positions
115+
const p =
116+
new Float32Buffer(new DeviceBuffer(numNodes * 2 * Float32Buffer.BYTES_PER_ELEMENT));
117+
if (positions.length > 0) {
118+
const pn = positions.length / 2;
119+
const sx = positions.subarray(0, Math.min(numNodes, pn));
120+
const sy = positions.subarray(pn, pn + Math.min(numNodes, pn));
121+
p.copyFrom(sx, 0, 0).copyFrom(sy, 0, numNodes);
122+
}
123+
positions = p;
124+
}
125+
}
126+
return new Float32Buffer(this.graph.forceAtlas2({...options, positions}));
127+
}
128+
}

modules/cugraph/src/graph_coo.cpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2020-2021, NVIDIA CORPORATION.
1+
// Copyright (c) 2020-2022, NVIDIA CORPORATION.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -14,14 +14,20 @@
1414

1515
#include <node_cugraph/graph_coo.hpp>
1616

17+
#include <node_cudf/table.hpp>
1718
#include <node_cudf/utilities/dtypes.hpp>
1819

1920
#include <node_cuda/utilities/error.hpp>
2021
#include <node_cuda/utilities/napi_to_cpp.hpp>
2122

23+
#include <cudf/column/column_factories.hpp>
24+
#include <cudf/filling.hpp>
2225
#include <cudf/reduction.hpp>
26+
#include <cudf/scalar/scalar_factories.hpp>
2327
#include <cudf/types.hpp>
2428

29+
#include <rmm/device_buffer.hpp>
30+
2531
#include <napi.h>
2632

2733
namespace nv {
@@ -33,6 +39,7 @@ Napi::Function GraphCOO::Init(Napi::Env const& env, Napi::Object exports) {
3339
InstanceMethod<&GraphCOO::num_edges>("numEdges"),
3440
InstanceMethod<&GraphCOO::num_nodes>("numNodes"),
3541
InstanceMethod<&GraphCOO::force_atlas2>("forceAtlas2"),
42+
InstanceMethod<&GraphCOO::degree>("degree"),
3643
});
3744
}
3845

@@ -65,7 +72,7 @@ GraphCOO::GraphCOO(CallbackArgs const& args) : EnvLocalObjectWrap<GraphCOO>(args
6572
directed_edges_ = options.Get("directedEdges");
6673
}
6774

68-
size_t GraphCOO::num_nodes() {
75+
int32_t GraphCOO::num_nodes() {
6976
if (!node_count_computed_) {
7077
auto max_id = [&](Napi::Reference<Column::wrapper_t> const& col) -> int32_t {
7178
return col.Value()->minmax().second->get_value().ToNumber();
@@ -76,7 +83,7 @@ size_t GraphCOO::num_nodes() {
7683
return node_count_;
7784
}
7885

79-
size_t GraphCOO::num_edges() {
86+
int32_t GraphCOO::num_edges() {
8087
if (!edge_count_computed_) {
8188
auto const& src = *src_.Value();
8289
auto const& dst = *dst_.Value();
@@ -104,4 +111,16 @@ Napi::Value GraphCOO::num_edges(Napi::CallbackInfo const& info) {
104111
return Napi::Value::From(info.Env(), num_edges());
105112
}
106113

114+
Napi::Value GraphCOO::degree(Napi::CallbackInfo const& info) {
115+
auto env = info.Env();
116+
auto zero = Scalar::New(env, Napi::Number::New(env, 0), cudf::data_type{cudf::type_id::INT32});
117+
auto degree = Column::sequence(
118+
env, num_nodes(), zero->operator cudf::scalar&(), zero->operator cudf::scalar&());
119+
120+
view().degree(degree->mutable_view().begin<int32_t>(),
121+
cugraph::legacy::DegreeDirection::IN_PLUS_OUT);
122+
123+
return degree;
124+
}
125+
107126
} // namespace nv

modules/cugraph/src/hypergraph.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414

1515
import {Categorical, DataFrame, Int32, Series, StringSeries, Utf8String} from '@rapidsai/cudf';
1616
import {TypeMap} from '@rapidsai/cudf';
17-
18-
import {GraphCOO} from './addon';
19-
import {renumberEdges, renumberNodes} from './renumber';
17+
import {Graph} from './graph';
2018

2119
export type HypergraphBaseProps<T extends TypeMap = any> = {
2220
/** An optional sequence of column names to process. */
@@ -67,7 +65,7 @@ export type HypergraphReturn = {
6765
/** A DataFrame of edge attributes. */
6866
edges: DataFrame,
6967
/** Graph of the found entity nodes, hyper nodes, and edges. */
70-
graph: GraphCOO,
68+
graph: Graph,
7169
/** a DataFrame of hyper node attributes for direct graphs, else empty. */
7270
events: DataFrame,
7371
/** A DataFrame of the found entity node attributes. */
@@ -130,7 +128,7 @@ hypergraph<T extends TypeMap = any>(values: DataFrame<T>, {
130128
const events = _create_hyper_nodes(initial_events, nodeId, eventId, category, nodeType);
131129
const nodes = entities.concat(events);
132130

133-
const graph = create_graph(edges, attribId, eventId);
131+
const graph = Graph.fromEdgeList(edges.get(attribId), edges.get(eventId));
134132

135133
return {nodes, edges, events, entities, graph};
136134
}
@@ -193,7 +191,7 @@ export function hypergraphDirect<T extends TypeMap = any>(values: DataFrame<T>,
193191
const events = new DataFrame({});
194192
const nodes = entities;
195193

196-
const graph = create_graph(edges, source, target);
194+
const graph = Graph.fromEdgeList(edges.get(source), edges.get(target));
197195

198196
return {nodes, edges, events, entities, graph};
199197
}
@@ -378,16 +376,6 @@ function _create_direct_edges(events: DataFrame,
378376
return new DataFrame().concat(...edge_dfs).select(cols);
379377
}
380378

381-
function create_graph(edges: DataFrame, source: string, target: string): GraphCOO {
382-
const src = edges.get(source);
383-
const dst = edges.get(target);
384-
385-
const rnodes = renumberNodes(src, dst);
386-
const redges = renumberEdges(src, dst, rnodes);
387-
388-
return new GraphCOO(redges.get('src')._col, redges.get('dst')._col, {directedEdges: true});
389-
}
390-
391379
function _prepend_str(series: Series, val: string, delim: string) {
392380
const prefix = val + delim;
393381
const suffix = series.cast(new Categorical(new Utf8String));

modules/cugraph/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414

1515
export * as addon from './addon';
1616
export {GraphCOO} from './addon';
17+
export {Graph} from './graph';
1718
export {hypergraph, hypergraphDirect} from './hypergraph';
1819
export {renumberEdges, renumberNodes} from './renumber';

0 commit comments

Comments
 (0)