Spaces:
Running
Running
Change formatters from 80 columns to 100 columns.
Browse files- biome.json +1 -0
- lynxkite-app/src/lynxkite_app/crdt.py +2 -6
- lynxkite-app/src/lynxkite_app/main.py +1 -4
- lynxkite-app/web/eslint.config.js +1 -4
- lynxkite-app/web/src/Directory.tsx +6 -21
- lynxkite-app/web/src/index.css +1 -2
- lynxkite-app/web/src/workspace/NodeSearch.tsx +2 -7
- lynxkite-app/web/src/workspace/Workspace.tsx +8 -38
- lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx +4 -16
- lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +6 -19
- lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx +2 -6
- lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +8 -37
- lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx +1 -3
- lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx +3 -10
- lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +2 -7
- lynxkite-app/web/tests/errors.spec.ts +1 -3
- lynxkite-app/web/tests/graph_creation.spec.ts +6 -15
- lynxkite-app/web/tests/import.spec.ts +2 -9
- lynxkite-app/web/tests/lynxkite.ts +6 -15
- lynxkite-app/web/tsconfig.json +1 -4
- lynxkite-bio/src/lynxkite_bio/rdkit.py +1 -3
- lynxkite-core/src/lynxkite/core/executors/one_by_one.py +3 -3
- lynxkite-core/src/lynxkite/core/ops.py +3 -7
- lynxkite-core/src/lynxkite/core/workspace.py +1 -3
- lynxkite-core/tests/test_ops.py +3 -9
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py +9 -28
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py +5 -17
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py +12 -39
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py +1 -3
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py +5 -15
- lynxkite-lynxscribe/src/lynxkite_lynxscribe/llm_ops.py +2 -6
biome.json
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
{
|
2 |
"formatter": {
|
3 |
"ignore": ["**/node_modules/**", "**/dist/**"],
|
|
|
4 |
"indentStyle": "space"
|
5 |
},
|
6 |
"linter": {
|
|
|
1 |
{
|
2 |
"formatter": {
|
3 |
"ignore": ["**/node_modules/**", "**/dist/**"],
|
4 |
+
"lineWidth": 100,
|
5 |
"indentStyle": "space"
|
6 |
},
|
7 |
"linter": {
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
@@ -40,9 +40,7 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
40 |
ydoc["workspace"] = ws = pycrdt.Map()
|
41 |
# Replay updates from the store.
|
42 |
try:
|
43 |
-
for update, timestamp in [
|
44 |
-
(item[0], item[-1]) async for item in ystore.read()
|
45 |
-
]:
|
46 |
ydoc.apply_update(update)
|
47 |
except pycrdt_websocket.ystore.YDocNotFound:
|
48 |
pass
|
@@ -212,9 +210,7 @@ async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt
|
|
212 |
await execute(name, ws_crdt, ws_pyd)
|
213 |
|
214 |
|
215 |
-
async def execute(
|
216 |
-
name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0
|
217 |
-
):
|
218 |
"""Execute the workspace and update the CRDT object with the results.
|
219 |
|
220 |
Args:
|
|
|
40 |
ydoc["workspace"] = ws = pycrdt.Map()
|
41 |
# Replay updates from the store.
|
42 |
try:
|
43 |
+
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
|
|
|
|
44 |
ydoc.apply_update(update)
|
45 |
except pycrdt_websocket.ystore.YDocNotFound:
|
46 |
pass
|
|
|
210 |
await execute(name, ws_crdt, ws_pyd)
|
211 |
|
212 |
|
213 |
+
async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, delay: int = 0):
|
|
|
|
|
214 |
"""Execute the workspace and update the CRDT object with the results.
|
215 |
|
216 |
Args:
|
lynxkite-app/src/lynxkite_app/main.py
CHANGED
@@ -34,10 +34,7 @@ app.add_middleware(GZipMiddleware)
|
|
34 |
|
35 |
@app.get("/api/catalog")
|
36 |
def get_catalog():
|
37 |
-
return {
|
38 |
-
k: {op.name: op.model_dump() for op in v.values()}
|
39 |
-
for k, v in ops.CATALOGS.items()
|
40 |
-
}
|
41 |
|
42 |
|
43 |
class SaveRequest(workspace.BaseConfig):
|
|
|
34 |
|
35 |
@app.get("/api/catalog")
|
36 |
def get_catalog():
|
37 |
+
return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
|
|
|
|
|
|
|
38 |
|
39 |
|
40 |
class SaveRequest(workspace.BaseConfig):
|
lynxkite-app/web/eslint.config.js
CHANGED
@@ -19,10 +19,7 @@ export default tseslint.config(
|
|
19 |
},
|
20 |
rules: {
|
21 |
...reactHooks.configs.recommended.rules,
|
22 |
-
"react-refresh/only-export-components": [
|
23 |
-
"warn",
|
24 |
-
{ allowConstantExport: true },
|
25 |
-
],
|
26 |
},
|
27 |
},
|
28 |
);
|
|
|
19 |
},
|
20 |
rules: {
|
21 |
...reactHooks.configs.recommended.rules,
|
22 |
+
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
|
|
|
|
|
|
23 |
},
|
24 |
},
|
25 |
);
|
lynxkite-app/web/src/Directory.tsx
CHANGED
@@ -52,21 +52,13 @@ export default function () {
|
|
52 |
}
|
53 |
}
|
54 |
|
55 |
-
function newWorkspaceIn(
|
56 |
-
path: string,
|
57 |
-
list: DirectoryEntry[],
|
58 |
-
workspaceName?: string,
|
59 |
-
) {
|
60 |
const pathSlash = path ? `${path}/` : "";
|
61 |
const name = workspaceName || newName(list);
|
62 |
navigate(`/edit/${pathSlash}${name}`, { replace: true });
|
63 |
}
|
64 |
|
65 |
-
async function newFolderIn(
|
66 |
-
path: string,
|
67 |
-
list: DirectoryEntry[],
|
68 |
-
folderName?: string,
|
69 |
-
) {
|
70 |
const name = folderName || newName(list, "New Folder");
|
71 |
const pathSlash = path ? `${path}/` : "";
|
72 |
|
@@ -83,12 +75,10 @@ export default function () {
|
|
83 |
}
|
84 |
|
85 |
async function deleteItem(item: DirectoryEntry) {
|
86 |
-
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`))
|
87 |
-
return;
|
88 |
const pathSlash = path ? `${path}/` : "";
|
89 |
|
90 |
-
const apiPath =
|
91 |
-
item.type === "directory" ? "/api/dir/delete" : "/api/delete";
|
92 |
await fetch(apiPath, {
|
93 |
method: "POST",
|
94 |
headers: { "Content-Type": "application/json" },
|
@@ -124,9 +114,7 @@ export default function () {
|
|
124 |
newWorkspaceIn(
|
125 |
path || "",
|
126 |
list.data,
|
127 |
-
(
|
128 |
-
e.target as HTMLFormElement
|
129 |
-
).workspaceName.value.trim(),
|
130 |
);
|
131 |
}}
|
132 |
>
|
@@ -138,10 +126,7 @@ export default function () {
|
|
138 |
/>
|
139 |
</form>
|
140 |
)}
|
141 |
-
<button
|
142 |
-
type="button"
|
143 |
-
onClick={() => setIsCreatingWorkspace(true)}
|
144 |
-
>
|
145 |
<FolderPlus /> New workspace
|
146 |
</button>
|
147 |
</div>
|
|
|
52 |
}
|
53 |
}
|
54 |
|
55 |
+
function newWorkspaceIn(path: string, list: DirectoryEntry[], workspaceName?: string) {
|
|
|
|
|
|
|
|
|
56 |
const pathSlash = path ? `${path}/` : "";
|
57 |
const name = workspaceName || newName(list);
|
58 |
navigate(`/edit/${pathSlash}${name}`, { replace: true });
|
59 |
}
|
60 |
|
61 |
+
async function newFolderIn(path: string, list: DirectoryEntry[], folderName?: string) {
|
|
|
|
|
|
|
|
|
62 |
const name = folderName || newName(list, "New Folder");
|
63 |
const pathSlash = path ? `${path}/` : "";
|
64 |
|
|
|
75 |
}
|
76 |
|
77 |
async function deleteItem(item: DirectoryEntry) {
|
78 |
+
if (!window.confirm(`Are you sure you want to delete "${item.name}"?`)) return;
|
|
|
79 |
const pathSlash = path ? `${path}/` : "";
|
80 |
|
81 |
+
const apiPath = item.type === "directory" ? "/api/dir/delete" : "/api/delete";
|
|
|
82 |
await fetch(apiPath, {
|
83 |
method: "POST",
|
84 |
headers: { "Content-Type": "application/json" },
|
|
|
114 |
newWorkspaceIn(
|
115 |
path || "",
|
116 |
list.data,
|
117 |
+
(e.target as HTMLFormElement).workspaceName.value.trim(),
|
|
|
|
|
118 |
);
|
119 |
}}
|
120 |
>
|
|
|
126 |
/>
|
127 |
</form>
|
128 |
)}
|
129 |
+
<button type="button" onClick={() => setIsCreatingWorkspace(true)}>
|
|
|
|
|
|
|
130 |
<FolderPlus /> New workspace
|
131 |
</button>
|
132 |
</div>
|
lynxkite-app/web/src/index.css
CHANGED
@@ -102,8 +102,7 @@ body {
|
|
102 |
--status-color-1: oklch(75% 0.2 55);
|
103 |
--status-color-2: oklch(75% 0.2 55);
|
104 |
--status-color-3: oklch(75% 0.2 55);
|
105 |
-
transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3
|
106 |
-
0.3s;
|
107 |
}
|
108 |
|
109 |
.lynxkite-node .title.active {
|
|
|
102 |
--status-color-1: oklch(75% 0.2 55);
|
103 |
--status-color-2: oklch(75% 0.2 55);
|
104 |
--status-color-3: oklch(75% 0.2 55);
|
105 |
+
transition: --status-color-1 0.3s, --status-color-2 0.3s, --status-color-3 0.3s;
|
|
|
106 |
}
|
107 |
|
108 |
.lynxkite-node .title.active {
|
lynxkite-app/web/src/workspace/NodeSearch.tsx
CHANGED
@@ -30,9 +30,7 @@ export default function (props: {
|
|
30 |
boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
|
31 |
return boxes;
|
32 |
}, [props.boxes]);
|
33 |
-
const hits: { item: OpsOp }[] = searchText
|
34 |
-
? fuse.search<OpsOp>(searchText)
|
35 |
-
: allOps;
|
36 |
const [selectedIndex, setSelectedIndex] = useState(0);
|
37 |
useEffect(() => searchBox.current.focus());
|
38 |
function typed(text: string) {
|
@@ -64,10 +62,7 @@ export default function (props: {
|
|
64 |
}
|
65 |
|
66 |
return (
|
67 |
-
<div
|
68 |
-
className="node-search"
|
69 |
-
style={{ top: props.pos.y, left: props.pos.x }}
|
70 |
-
>
|
71 |
<input
|
72 |
ref={searchBox}
|
73 |
value={searchText}
|
|
|
30 |
boxes.sort((a, b) => a.item.name.localeCompare(b.item.name));
|
31 |
return boxes;
|
32 |
}, [props.boxes]);
|
33 |
+
const hits: { item: OpsOp }[] = searchText ? fuse.search<OpsOp>(searchText) : allOps;
|
|
|
|
|
34 |
const [selectedIndex, setSelectedIndex] = useState(0);
|
35 |
useEffect(() => searchBox.current.focus());
|
36 |
function typed(text: string) {
|
|
|
62 |
}
|
63 |
|
64 |
return (
|
65 |
+
<div className="node-search" style={{ top: props.pos.y, left: props.pos.x }}>
|
|
|
|
|
|
|
66 |
<input
|
67 |
ref={searchBox}
|
68 |
value={searchText}
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
@@ -15,13 +15,7 @@ import {
|
|
15 |
useUpdateNodeInternals,
|
16 |
} from "@xyflow/react";
|
17 |
import axios from "axios";
|
18 |
-
import {
|
19 |
-
type MouseEvent,
|
20 |
-
useCallback,
|
21 |
-
useEffect,
|
22 |
-
useMemo,
|
23 |
-
useState,
|
24 |
-
} from "react";
|
25 |
// The LynxKite workspace editor.
|
26 |
import { useParams } from "react-router";
|
27 |
import useSWR, { type Fetcher } from "swr";
|
@@ -37,11 +31,7 @@ import favicon from "../assets/favicon.ico";
|
|
37 |
// import NodeWithTableView from './NodeWithTableView';
|
38 |
import EnvironmentSelector from "./EnvironmentSelector";
|
39 |
import { LynxKiteState } from "./LynxKiteState";
|
40 |
-
import NodeSearch, {
|
41 |
-
type OpsOp,
|
42 |
-
type Catalog,
|
43 |
-
type Catalogs,
|
44 |
-
} from "./NodeSearch.tsx";
|
45 |
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
46 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
47 |
import NodeWithParams from "./nodes/NodeWithParams";
|
@@ -69,11 +59,7 @@ function LynxKiteFlow() {
|
|
69 |
setState(state);
|
70 |
const doc = getYjsDoc(state);
|
71 |
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
72 |
-
const wsProvider = new WebsocketProvider(
|
73 |
-
`${proto}//${location.host}/ws/crdt`,
|
74 |
-
path!,
|
75 |
-
doc,
|
76 |
-
);
|
77 |
const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
|
78 |
if (origin === wsProvider) {
|
79 |
// An update from the CRDT. Apply it to the local state.
|
@@ -190,11 +176,7 @@ function LynxKiteFlow() {
|
|
190 |
useEffect(() => {
|
191 |
const handleKeyDown = (event: KeyboardEvent) => {
|
192 |
// Show the node search dialog on "/".
|
193 |
-
if (
|
194 |
-
event.key === "/" &&
|
195 |
-
!nodeSearchSettings &&
|
196 |
-
!isTypingInFormElement()
|
197 |
-
) {
|
198 |
event.preventDefault();
|
199 |
setNodeSearchSettings({
|
200 |
pos: { x: 100, y: 100 },
|
@@ -238,11 +220,7 @@ function LynxKiteFlow() {
|
|
238 |
},
|
239 |
[catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
|
240 |
);
|
241 |
-
function addNode(
|
242 |
-
node: Partial<WorkspaceNode>,
|
243 |
-
state: { workspace: Workspace },
|
244 |
-
nodes: Node[],
|
245 |
-
) {
|
246 |
const title = node.data?.title;
|
247 |
let i = 1;
|
248 |
node.id = `${title} ${i}`;
|
@@ -260,9 +238,7 @@ function LynxKiteFlow() {
|
|
260 |
data: {
|
261 |
meta: meta,
|
262 |
title: meta.name,
|
263 |
-
params: Object.fromEntries(
|
264 |
-
Object.values(meta.params).map((p) => [p.name, p.default]),
|
265 |
-
),
|
266 |
},
|
267 |
};
|
268 |
return node;
|
@@ -310,9 +286,7 @@ function LynxKiteFlow() {
|
|
310 |
try {
|
311 |
await axios.post("/api/upload", formData, {
|
312 |
onUploadProgress: (progressEvent) => {
|
313 |
-
const percentCompleted = Math.round(
|
314 |
-
(100 * progressEvent.loaded) / progressEvent.total!,
|
315 |
-
);
|
316 |
if (percentCompleted === 100) setMessage("Processing file...");
|
317 |
else setMessage(`Uploading ${percentCompleted}%`);
|
318 |
},
|
@@ -365,11 +339,7 @@ function LynxKiteFlow() {
|
|
365 |
</a>
|
366 |
</div>
|
367 |
</div>
|
368 |
-
<div
|
369 |
-
style={{ height: "100%", width: "100vw" }}
|
370 |
-
onDragOver={onDragOver}
|
371 |
-
onDrop={onDrop}
|
372 |
-
>
|
373 |
<LynxKiteState.Provider value={state}>
|
374 |
<ReactFlow
|
375 |
nodes={nodes}
|
|
|
15 |
useUpdateNodeInternals,
|
16 |
} from "@xyflow/react";
|
17 |
import axios from "axios";
|
18 |
+
import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
// The LynxKite workspace editor.
|
20 |
import { useParams } from "react-router";
|
21 |
import useSWR, { type Fetcher } from "swr";
|
|
|
31 |
// import NodeWithTableView from './NodeWithTableView';
|
32 |
import EnvironmentSelector from "./EnvironmentSelector";
|
33 |
import { LynxKiteState } from "./LynxKiteState";
|
34 |
+
import NodeSearch, { type OpsOp, type Catalog, type Catalogs } from "./NodeSearch.tsx";
|
|
|
|
|
|
|
|
|
35 |
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
36 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
37 |
import NodeWithParams from "./nodes/NodeWithParams";
|
|
|
59 |
setState(state);
|
60 |
const doc = getYjsDoc(state);
|
61 |
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
62 |
+
const wsProvider = new WebsocketProvider(`${proto}//${location.host}/ws/crdt`, path!, doc);
|
|
|
|
|
|
|
|
|
63 |
const onChange = (_update: any, origin: any, _doc: any, _tr: any) => {
|
64 |
if (origin === wsProvider) {
|
65 |
// An update from the CRDT. Apply it to the local state.
|
|
|
176 |
useEffect(() => {
|
177 |
const handleKeyDown = (event: KeyboardEvent) => {
|
178 |
// Show the node search dialog on "/".
|
179 |
+
if (event.key === "/" && !nodeSearchSettings && !isTypingInFormElement()) {
|
|
|
|
|
|
|
|
|
180 |
event.preventDefault();
|
181 |
setNodeSearchSettings({
|
182 |
pos: { x: 100, y: 100 },
|
|
|
220 |
},
|
221 |
[catalog, state, nodeSearchSettings, suppressSearchUntil, closeNodeSearch],
|
222 |
);
|
223 |
+
function addNode(node: Partial<WorkspaceNode>, state: { workspace: Workspace }, nodes: Node[]) {
|
|
|
|
|
|
|
|
|
224 |
const title = node.data?.title;
|
225 |
let i = 1;
|
226 |
node.id = `${title} ${i}`;
|
|
|
238 |
data: {
|
239 |
meta: meta,
|
240 |
title: meta.name,
|
241 |
+
params: Object.fromEntries(Object.values(meta.params).map((p) => [p.name, p.default])),
|
|
|
|
|
242 |
},
|
243 |
};
|
244 |
return node;
|
|
|
286 |
try {
|
287 |
await axios.post("/api/upload", formData, {
|
288 |
onUploadProgress: (progressEvent) => {
|
289 |
+
const percentCompleted = Math.round((100 * progressEvent.loaded) / progressEvent.total!);
|
|
|
|
|
290 |
if (percentCompleted === 100) setMessage("Processing file...");
|
291 |
else setMessage(`Uploading ${percentCompleted}%`);
|
292 |
},
|
|
|
339 |
</a>
|
340 |
</div>
|
341 |
</div>
|
342 |
+
<div style={{ height: "100%", width: "100vw" }} onDragOver={onDragOver} onDrop={onDrop}>
|
|
|
|
|
|
|
|
|
343 |
<LynxKiteState.Provider value={state}>
|
344 |
<ReactFlow
|
345 |
nodes={nodes}
|
lynxkite-app/web/src/workspace/nodes/GraphCreationNode.tsx
CHANGED
@@ -20,12 +20,7 @@ function toMD(v: any): string {
|
|
20 |
function displayTable(name: string, df: any) {
|
21 |
if (df.data.length > 1) {
|
22 |
return (
|
23 |
-
<Table
|
24 |
-
key={`${name}-table`}
|
25 |
-
name={`${name}-table`}
|
26 |
-
columns={df.columns}
|
27 |
-
data={df.data}
|
28 |
-
/>
|
29 |
);
|
30 |
}
|
31 |
if (df.data.length) {
|
@@ -60,9 +55,7 @@ export default function NodeWithGraphCreationView(props: any) {
|
|
60 |
const display = props.data.display?.value;
|
61 |
const tables = display?.dataframes || {};
|
62 |
const singleTable = tables && Object.keys(tables).length === 1;
|
63 |
-
const [relations, setRelations] = useState(
|
64 |
-
relationsToDict(display?.relations) || {},
|
65 |
-
);
|
66 |
const singleRelation = relations && Object.keys(relations).length === 1;
|
67 |
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
68 |
reactFlow.updateNodeData(props.id, {
|
@@ -211,9 +204,7 @@ export default function NodeWithGraphCreationView(props: any) {
|
|
211 |
|
212 |
<datalist id="edges-column-options">
|
213 |
{tables[relation.source_table] &&
|
214 |
-
tables[relation.df].columns.map((name: string) =>
|
215 |
-
<option key={name} value={name} />
|
216 |
-
))}
|
217 |
</datalist>
|
218 |
|
219 |
<datalist id="source-node-column-options">
|
@@ -274,10 +265,7 @@ export default function NodeWithGraphCreationView(props: any) {
|
|
274 |
<div className="graph-relations">
|
275 |
<div className="graph-table-header">
|
276 |
Relationships
|
277 |
-
<button
|
278 |
-
className="add-relationship-button"
|
279 |
-
onClick={(_) => addRelation()}
|
280 |
-
>
|
281 |
+
|
282 |
</button>
|
283 |
</div>
|
|
|
20 |
function displayTable(name: string, df: any) {
|
21 |
if (df.data.length > 1) {
|
22 |
return (
|
23 |
+
<Table key={`${name}-table`} name={`${name}-table`} columns={df.columns} data={df.data} />
|
|
|
|
|
|
|
|
|
|
|
24 |
);
|
25 |
}
|
26 |
if (df.data.length) {
|
|
|
55 |
const display = props.data.display?.value;
|
56 |
const tables = display?.dataframes || {};
|
57 |
const singleTable = tables && Object.keys(tables).length === 1;
|
58 |
+
const [relations, setRelations] = useState(relationsToDict(display?.relations) || {});
|
|
|
|
|
59 |
const singleRelation = relations && Object.keys(relations).length === 1;
|
60 |
function setParam(name: string, newValue: any, opts: UpdateOptions) {
|
61 |
reactFlow.updateNodeData(props.id, {
|
|
|
204 |
|
205 |
<datalist id="edges-column-options">
|
206 |
{tables[relation.source_table] &&
|
207 |
+
tables[relation.df].columns.map((name: string) => <option key={name} value={name} />)}
|
|
|
|
|
208 |
</datalist>
|
209 |
|
210 |
<datalist id="source-node-column-options">
|
|
|
265 |
<div className="graph-relations">
|
266 |
<div className="graph-table-header">
|
267 |
Relationships
|
268 |
+
<button className="add-relationship-button" onClick={(_) => addRelation()}>
|
|
|
|
|
|
|
269 |
+
|
270 |
</button>
|
271 |
</div>
|
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx
CHANGED
@@ -1,9 +1,4 @@
|
|
1 |
-
import {
|
2 |
-
Handle,
|
3 |
-
NodeResizeControl,
|
4 |
-
type Position,
|
5 |
-
useReactFlow,
|
6 |
-
} from "@xyflow/react";
|
7 |
// @ts-ignore
|
8 |
import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
|
9 |
|
@@ -38,10 +33,8 @@ function getHandles(inputs: object, outputs: object) {
|
|
38 |
}
|
39 |
for (const e of handles) {
|
40 |
e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
|
41 |
-
const simpleHorizontal =
|
42 |
-
|
43 |
-
const simpleVertical =
|
44 |
-
counts.left === 0 && counts.right === 0 && handles.length <= 2;
|
45 |
e.showLabel = !simpleHorizontal && !simpleVertical;
|
46 |
}
|
47 |
return handles;
|
@@ -71,10 +64,7 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
|
|
71 |
}}
|
72 |
>
|
73 |
<div className="lynxkite-node" style={props.nodeStyle}>
|
74 |
-
<div
|
75 |
-
className={`title bg-primary ${data.status}`}
|
76 |
-
onClick={titleClicked}
|
77 |
-
>
|
78 |
{data.title}
|
79 |
{data.error && <span className="title-icon">⚠️</span>}
|
80 |
{expanded || <span className="title-icon">⋯</span>}
|
@@ -99,14 +89,11 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
|
|
99 |
type={handle.type}
|
100 |
position={handle.position as Position}
|
101 |
style={{
|
102 |
-
[handleOffsetDirection[handle.position]]:
|
103 |
-
`${handle.offsetPercentage}% `,
|
104 |
}}
|
105 |
>
|
106 |
{handle.showLabel && (
|
107 |
-
<span className="handle-name">
|
108 |
-
{handle.name.replace(/_/g, " ")}
|
109 |
-
</span>
|
110 |
)}
|
111 |
</Handle>
|
112 |
))}
|
|
|
1 |
+
import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
|
|
|
|
|
|
|
|
|
|
|
2 |
// @ts-ignore
|
3 |
import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
|
4 |
|
|
|
33 |
}
|
34 |
for (const e of handles) {
|
35 |
e.offsetPercentage = (100 * (e.index + 1)) / (counts[e.position] + 1);
|
36 |
+
const simpleHorizontal = counts.top === 0 && counts.bottom === 0 && handles.length <= 2;
|
37 |
+
const simpleVertical = counts.left === 0 && counts.right === 0 && handles.length <= 2;
|
|
|
|
|
38 |
e.showLabel = !simpleHorizontal && !simpleVertical;
|
39 |
}
|
40 |
return handles;
|
|
|
64 |
}}
|
65 |
>
|
66 |
<div className="lynxkite-node" style={props.nodeStyle}>
|
67 |
+
<div className={`title bg-primary ${data.status}`} onClick={titleClicked}>
|
|
|
|
|
|
|
68 |
{data.title}
|
69 |
{data.error && <span className="title-icon">⚠️</span>}
|
70 |
{expanded || <span className="title-icon">⋯</span>}
|
|
|
89 |
type={handle.type}
|
90 |
position={handle.position as Position}
|
91 |
style={{
|
92 |
+
[handleOffsetDirection[handle.position]]: `${handle.offsetPercentage}% `,
|
|
|
93 |
}}
|
94 |
>
|
95 |
{handle.showLabel && (
|
96 |
+
<span className="handle-name">{handle.name.replace(/_/g, " ")}</span>
|
|
|
|
|
97 |
)}
|
98 |
</Handle>
|
99 |
))}
|
lynxkite-app/web/src/workspace/nodes/NodeGroupParameter.tsx
CHANGED
@@ -38,9 +38,7 @@ export default function NodeGroupParameter({
|
|
38 |
}: NodeGroupParameterProps) {
|
39 |
const selector = meta.selector;
|
40 |
const groups = meta.groups;
|
41 |
-
const [selectedValue, setSelectedValue] = useState<string>(
|
42 |
-
value || selector.default,
|
43 |
-
);
|
44 |
|
45 |
const handleSelectorChange = (value: any, opts?: { delay: number }) => {
|
46 |
setSelectedValue(value);
|
@@ -49,9 +47,7 @@ export default function NodeGroupParameter({
|
|
49 |
|
50 |
useEffect(() => {
|
51 |
// Clean possible previous parameters first
|
52 |
-
Object.values(groups).flatMap((group) =>
|
53 |
-
group.map((entry) => deleteParam(entry.name)),
|
54 |
-
);
|
55 |
for (const param of groups[selectedValue]) {
|
56 |
setParam(param.name, param.default);
|
57 |
}
|
|
|
38 |
}: NodeGroupParameterProps) {
|
39 |
const selector = meta.selector;
|
40 |
const groups = meta.groups;
|
41 |
+
const [selectedValue, setSelectedValue] = useState<string>(value || selector.default);
|
|
|
|
|
42 |
|
43 |
const handleSelectorChange = (value: any, opts?: { delay: number }) => {
|
44 |
setSelectedValue(value);
|
|
|
47 |
|
48 |
useEffect(() => {
|
49 |
// Clean possible previous parameters first
|
50 |
+
Object.values(groups).flatMap((group) => group.map((entry) => deleteParam(entry.name)));
|
|
|
|
|
51 |
for (const param of groups[selectedValue]) {
|
52 |
setParam(param.name, param.default);
|
53 |
}
|
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx
CHANGED
@@ -6,12 +6,9 @@ const MODEL_TRAINING_INPUT_MAPPING =
|
|
6 |
"<class 'lynxkite_graph_analytics.lynxkite_ops.ModelTrainingInputMapping'>";
|
7 |
const MODEL_INFERENCE_INPUT_MAPPING =
|
8 |
"<class 'lynxkite_graph_analytics.lynxkite_ops.ModelInferenceInputMapping'>";
|
9 |
-
const MODEL_OUTPUT_MAPPING =
|
10 |
-
"<class 'lynxkite_graph_analytics.lynxkite_ops.ModelOutputMapping'>";
|
11 |
function ParamName({ name }: { name: string }) {
|
12 |
-
return (
|
13 |
-
<span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>
|
14 |
-
);
|
15 |
}
|
16 |
|
17 |
function Input({
|
@@ -27,9 +24,7 @@ function Input({
|
|
27 |
value={value || ""}
|
28 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
29 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
30 |
-
onKeyDown={(evt) =>
|
31 |
-
evt.code === "Enter" && onChange(evt.currentTarget.value, { delay: 0 })
|
32 |
-
}
|
33 |
/>
|
34 |
);
|
35 |
}
|
@@ -41,10 +36,7 @@ function getModelBindings(
|
|
41 |
function bindingsOfModel(m: any): string[] {
|
42 |
switch (variant) {
|
43 |
case "training input":
|
44 |
-
return [
|
45 |
-
...m.inputs,
|
46 |
-
...m.loss_inputs.filter((i: string) => !m.outputs.includes(i)),
|
47 |
-
];
|
48 |
case "inference input":
|
49 |
return m.inputs;
|
50 |
case "output":
|
@@ -185,13 +177,7 @@ interface NodeParameterProps {
|
|
185 |
onChange: (value: any, options?: { delay: number }) => void;
|
186 |
}
|
187 |
|
188 |
-
export default function NodeParameter({
|
189 |
-
name,
|
190 |
-
value,
|
191 |
-
meta,
|
192 |
-
data,
|
193 |
-
onChange,
|
194 |
-
}: NodeParameterProps) {
|
195 |
return (
|
196 |
// biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
|
197 |
<label className="param">
|
@@ -241,32 +227,17 @@ export default function NodeParameter({
|
|
241 |
) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
|
242 |
<>
|
243 |
<ParamName name={name} />
|
244 |
-
<ModelMapping
|
245 |
-
value={value}
|
246 |
-
data={data}
|
247 |
-
variant="training input"
|
248 |
-
onChange={onChange}
|
249 |
-
/>
|
250 |
</>
|
251 |
) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
|
252 |
<>
|
253 |
<ParamName name={name} />
|
254 |
-
<ModelMapping
|
255 |
-
value={value}
|
256 |
-
data={data}
|
257 |
-
variant="inference input"
|
258 |
-
onChange={onChange}
|
259 |
-
/>
|
260 |
</>
|
261 |
) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
|
262 |
<>
|
263 |
<ParamName name={name} />
|
264 |
-
<ModelMapping
|
265 |
-
value={value}
|
266 |
-
data={data}
|
267 |
-
variant="output"
|
268 |
-
onChange={onChange}
|
269 |
-
/>
|
270 |
</>
|
271 |
) : (
|
272 |
<>
|
|
|
6 |
"<class 'lynxkite_graph_analytics.lynxkite_ops.ModelTrainingInputMapping'>";
|
7 |
const MODEL_INFERENCE_INPUT_MAPPING =
|
8 |
"<class 'lynxkite_graph_analytics.lynxkite_ops.ModelInferenceInputMapping'>";
|
9 |
+
const MODEL_OUTPUT_MAPPING = "<class 'lynxkite_graph_analytics.lynxkite_ops.ModelOutputMapping'>";
|
|
|
10 |
function ParamName({ name }: { name: string }) {
|
11 |
+
return <span className="param-name bg-base-200">{name.replace(/_/g, " ")}</span>;
|
|
|
|
|
12 |
}
|
13 |
|
14 |
function Input({
|
|
|
24 |
value={value || ""}
|
25 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
26 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
27 |
+
onKeyDown={(evt) => evt.code === "Enter" && onChange(evt.currentTarget.value, { delay: 0 })}
|
|
|
|
|
28 |
/>
|
29 |
);
|
30 |
}
|
|
|
36 |
function bindingsOfModel(m: any): string[] {
|
37 |
switch (variant) {
|
38 |
case "training input":
|
39 |
+
return [...m.inputs, ...m.loss_inputs.filter((i: string) => !m.outputs.includes(i))];
|
|
|
|
|
|
|
40 |
case "inference input":
|
41 |
return m.inputs;
|
42 |
case "output":
|
|
|
177 |
onChange: (value: any, options?: { delay: number }) => void;
|
178 |
}
|
179 |
|
180 |
+
export default function NodeParameter({ name, value, meta, data, onChange }: NodeParameterProps) {
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
return (
|
182 |
// biome-ignore lint/a11y/noLabelWithoutControl: Most of the time there is a control.
|
183 |
<label className="param">
|
|
|
227 |
) : meta?.type?.type === MODEL_TRAINING_INPUT_MAPPING ? (
|
228 |
<>
|
229 |
<ParamName name={name} />
|
230 |
+
<ModelMapping value={value} data={data} variant="training input" onChange={onChange} />
|
|
|
|
|
|
|
|
|
|
|
231 |
</>
|
232 |
) : meta?.type?.type === MODEL_INFERENCE_INPUT_MAPPING ? (
|
233 |
<>
|
234 |
<ParamName name={name} />
|
235 |
+
<ModelMapping value={value} data={data} variant="inference input" onChange={onChange} />
|
|
|
|
|
|
|
|
|
|
|
236 |
</>
|
237 |
) : meta?.type?.type === MODEL_OUTPUT_MAPPING ? (
|
238 |
<>
|
239 |
<ParamName name={name} />
|
240 |
+
<ModelMapping value={value} data={data} variant="output" onChange={onChange} />
|
|
|
|
|
|
|
|
|
|
|
241 |
</>
|
242 |
) : (
|
243 |
<>
|
lynxkite-app/web/src/workspace/nodes/NodeWithImage.tsx
CHANGED
@@ -3,9 +3,7 @@ import NodeWithParams from "./NodeWithParams";
|
|
3 |
const NodeWithImage = (props: any) => {
|
4 |
return (
|
5 |
<NodeWithParams {...props}>
|
6 |
-
{props.data.display &&
|
7 |
-
<img src={props.data.display} alt="Node Display" />
|
8 |
-
)}
|
9 |
</NodeWithParams>
|
10 |
);
|
11 |
};
|
|
|
3 |
const NodeWithImage = (props: any) => {
|
4 |
return (
|
5 |
<NodeWithParams {...props}>
|
6 |
+
{props.data.display && <img src={props.data.display} alt="Node Display" />}
|
|
|
|
|
7 |
</NodeWithParams>
|
8 |
);
|
9 |
};
|
lynxkite-app/web/src/workspace/nodes/NodeWithParams.tsx
CHANGED
@@ -36,10 +36,7 @@ function NodeWithParams(props: any) {
|
|
36 |
return (
|
37 |
<LynxKiteNode {...props}>
|
38 |
{props.collapsed && (
|
39 |
-
<div
|
40 |
-
className="params-expander"
|
41 |
-
onClick={() => setCollapsed(!collapsed)}
|
42 |
-
>
|
43 |
<Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
|
44 |
</div>
|
45 |
)}
|
@@ -54,9 +51,7 @@ function NodeWithParams(props: any) {
|
|
54 |
setParam={(name: string, value: any, opts?: UpdateOptions) =>
|
55 |
setParam(name, value, opts || {})
|
56 |
}
|
57 |
-
deleteParam={(name: string, opts?: UpdateOptions) =>
|
58 |
-
deleteParam(name, opts || {})
|
59 |
-
}
|
60 |
/>
|
61 |
) : (
|
62 |
<NodeParameter
|
@@ -65,9 +60,7 @@ function NodeWithParams(props: any) {
|
|
65 |
value={value}
|
66 |
data={props.data}
|
67 |
meta={metaParams?.[name]}
|
68 |
-
onChange={(value: any, opts?: UpdateOptions) =>
|
69 |
-
setParam(name, value, opts || {})
|
70 |
-
}
|
71 |
/>
|
72 |
),
|
73 |
)}
|
|
|
36 |
return (
|
37 |
<LynxKiteNode {...props}>
|
38 |
{props.collapsed && (
|
39 |
+
<div className="params-expander" onClick={() => setCollapsed(!collapsed)}>
|
|
|
|
|
|
|
40 |
<Triangle className={`flippy ${collapsed ? "flippy-90" : ""}`} />
|
41 |
</div>
|
42 |
)}
|
|
|
51 |
setParam={(name: string, value: any, opts?: UpdateOptions) =>
|
52 |
setParam(name, value, opts || {})
|
53 |
}
|
54 |
+
deleteParam={(name: string, opts?: UpdateOptions) => deleteParam(name, opts || {})}
|
|
|
|
|
55 |
/>
|
56 |
) : (
|
57 |
<NodeParameter
|
|
|
60 |
value={value}
|
61 |
data={props.data}
|
62 |
meta={metaParams?.[name]}
|
63 |
+
onChange={(value: any, opts?: UpdateOptions) => setParam(name, value, opts || {})}
|
|
|
|
|
64 |
/>
|
65 |
),
|
66 |
)}
|
lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx
CHANGED
@@ -17,8 +17,7 @@ function toMD(v: any): string {
|
|
17 |
export default function NodeWithTableView(props: any) {
|
18 |
const [open, setOpen] = useState({} as { [name: string]: boolean });
|
19 |
const display = props.data.display?.value;
|
20 |
-
const single =
|
21 |
-
display?.dataframes && Object.keys(display?.dataframes).length === 1;
|
22 |
const dfs = Object.entries(display?.dataframes || {});
|
23 |
dfs.sort();
|
24 |
return (
|
@@ -37,11 +36,7 @@ export default function NodeWithTableView(props: any) {
|
|
37 |
)}
|
38 |
{(single || open[name]) &&
|
39 |
(df.data.length > 1 ? (
|
40 |
-
<Table
|
41 |
-
key={`${name}-table`}
|
42 |
-
columns={df.columns}
|
43 |
-
data={df.data}
|
44 |
-
/>
|
45 |
) : df.data.length ? (
|
46 |
<dl key={`${name}-dl`}>
|
47 |
{df.columns.map((c: string, i: number) => (
|
|
|
17 |
export default function NodeWithTableView(props: any) {
|
18 |
const [open, setOpen] = useState({} as { [name: string]: boolean });
|
19 |
const display = props.data.display?.value;
|
20 |
+
const single = display?.dataframes && Object.keys(display?.dataframes).length === 1;
|
|
|
21 |
const dfs = Object.entries(display?.dataframes || {});
|
22 |
dfs.sort();
|
23 |
return (
|
|
|
36 |
)}
|
37 |
{(single || open[name]) &&
|
38 |
(df.data.length > 1 ? (
|
39 |
+
<Table key={`${name}-table`} columns={df.columns} data={df.data} />
|
|
|
|
|
|
|
|
|
40 |
) : df.data.length ? (
|
41 |
<dl key={`${name}-dl`}>
|
42 |
{df.columns.map((c: string, i: number) => (
|
lynxkite-app/web/tests/errors.spec.ts
CHANGED
@@ -35,9 +35,7 @@ test("unknown operation", async () => {
|
|
35 |
await graphBox.getByLabel("n", { exact: true }).fill("10");
|
36 |
await workspace.setEnv("LynxScribe");
|
37 |
const csvBox = workspace.getBox("NX › Scale-Free Graph 1");
|
38 |
-
await expect(csvBox.locator(".error")).toHaveText(
|
39 |
-
'Operation "NX › Scale-Free Graph" not found.',
|
40 |
-
);
|
41 |
await workspace.setEnv("LynxKite Graph Analytics");
|
42 |
await expect(csvBox.locator(".error")).not.toBeVisible();
|
43 |
});
|
|
|
35 |
await graphBox.getByLabel("n", { exact: true }).fill("10");
|
36 |
await workspace.setEnv("LynxScribe");
|
37 |
const csvBox = workspace.getBox("NX › Scale-Free Graph 1");
|
38 |
+
await expect(csvBox.locator(".error")).toHaveText('Operation "NX › Scale-Free Graph" not found.');
|
|
|
|
|
39 |
await workspace.setEnv("LynxKite Graph Analytics");
|
40 |
await expect(csvBox.locator(".error")).not.toBeVisible();
|
41 |
});
|
lynxkite-app/web/tests/graph_creation.spec.ts
CHANGED
@@ -5,15 +5,9 @@ import { Splash, Workspace } from "./lynxkite";
|
|
5 |
let workspace: Workspace;
|
6 |
|
7 |
test.beforeEach(async ({ browser }) => {
|
8 |
-
workspace = await Workspace.empty(
|
9 |
-
await browser.newPage(),
|
10 |
-
"graph_creation_spec_test",
|
11 |
-
);
|
12 |
await workspace.addBox("NX › Scale-Free Graph");
|
13 |
-
await workspace
|
14 |
-
.getBox("NX › Scale-Free Graph 1")
|
15 |
-
.getByLabel("n", { exact: true })
|
16 |
-
.fill("10");
|
17 |
await workspace.addBox("Create graph");
|
18 |
await workspace.connectBoxes("NX › Scale-Free Graph 1", "Create graph 1");
|
19 |
});
|
@@ -45,9 +39,7 @@ test("Tables are displayed in the Graph creation box", async () => {
|
|
45 |
|
46 |
test("Adding and removing relationships", async () => {
|
47 |
const graphBox = await workspace.getBox("Create graph 1");
|
48 |
-
const addRelationshipButton = await graphBox.locator(
|
49 |
-
".add-relationship-button",
|
50 |
-
);
|
51 |
await addRelationshipButton.click();
|
52 |
const formData: Record<string, string> = {
|
53 |
name: "relation_1",
|
@@ -69,10 +61,9 @@ test("Adding and removing relationships", async () => {
|
|
69 |
// check that the relationship has been saved in the backend
|
70 |
await workspace.page.reload();
|
71 |
const graphBoxAfterReload = await workspace.getBox("Create graph 1");
|
72 |
-
const relationHeader = await graphBoxAfterReload.locator(
|
73 |
-
|
74 |
-
|
75 |
-
);
|
76 |
await expect(relationHeader).toBeVisible();
|
77 |
await relationHeader.locator("button").click(); // Delete the relationship
|
78 |
await expect(relationHeader).not.toBeVisible();
|
|
|
5 |
let workspace: Workspace;
|
6 |
|
7 |
test.beforeEach(async ({ browser }) => {
|
8 |
+
workspace = await Workspace.empty(await browser.newPage(), "graph_creation_spec_test");
|
|
|
|
|
|
|
9 |
await workspace.addBox("NX › Scale-Free Graph");
|
10 |
+
await workspace.getBox("NX › Scale-Free Graph 1").getByLabel("n", { exact: true }).fill("10");
|
|
|
|
|
|
|
11 |
await workspace.addBox("Create graph");
|
12 |
await workspace.connectBoxes("NX › Scale-Free Graph 1", "Create graph 1");
|
13 |
});
|
|
|
39 |
|
40 |
test("Adding and removing relationships", async () => {
|
41 |
const graphBox = await workspace.getBox("Create graph 1");
|
42 |
+
const addRelationshipButton = await graphBox.locator(".add-relationship-button");
|
|
|
|
|
43 |
await addRelationshipButton.click();
|
44 |
const formData: Record<string, string> = {
|
45 |
name: "relation_1",
|
|
|
61 |
// check that the relationship has been saved in the backend
|
62 |
await workspace.page.reload();
|
63 |
const graphBoxAfterReload = await workspace.getBox("Create graph 1");
|
64 |
+
const relationHeader = await graphBoxAfterReload.locator(".graph-relations .df-head", {
|
65 |
+
hasText: "relation_1",
|
66 |
+
});
|
|
|
67 |
await expect(relationHeader).toBeVisible();
|
68 |
await relationHeader.locator("button").click(); // Delete the relationship
|
69 |
await expect(relationHeader).not.toBeVisible();
|
lynxkite-app/web/tests/import.spec.ts
CHANGED
@@ -7,10 +7,7 @@ import { Splash, Workspace } from "./lynxkite";
|
|
7 |
let workspace: Workspace;
|
8 |
|
9 |
test.beforeEach(async ({ browser }) => {
|
10 |
-
workspace = await Workspace.empty(
|
11 |
-
await browser.newPage(),
|
12 |
-
"import_spec_test",
|
13 |
-
);
|
14 |
});
|
15 |
|
16 |
test.afterEach(async () => {
|
@@ -22,11 +19,7 @@ test.afterEach(async () => {
|
|
22 |
await splash.deleteEntry("import_spec_test");
|
23 |
});
|
24 |
|
25 |
-
async function validateImport(
|
26 |
-
workspace: Workspace,
|
27 |
-
fileName: string,
|
28 |
-
fileFormat: string,
|
29 |
-
) {
|
30 |
const __filename = fileURLToPath(import.meta.url);
|
31 |
const __dirname = dirname(__filename);
|
32 |
const filePath = join(__dirname, "data", fileName);
|
|
|
7 |
let workspace: Workspace;
|
8 |
|
9 |
test.beforeEach(async ({ browser }) => {
|
10 |
+
workspace = await Workspace.empty(await browser.newPage(), "import_spec_test");
|
|
|
|
|
|
|
11 |
});
|
12 |
|
13 |
test.afterEach(async () => {
|
|
|
19 |
await splash.deleteEntry("import_spec_test");
|
20 |
});
|
21 |
|
22 |
+
async function validateImport(workspace: Workspace, fileName: string, fileFormat: string) {
|
|
|
|
|
|
|
|
|
23 |
const __filename = fileURLToPath(import.meta.url);
|
24 |
const __dirname = dirname(__filename);
|
25 |
const filePath = join(__dirname, "data", fileName);
|
lynxkite-app/web/tests/lynxkite.ts
CHANGED
@@ -63,10 +63,7 @@ export class Workspace {
|
|
63 |
|
64 |
await this.page.locator(".ws-name").click();
|
65 |
await this.page.keyboard.press("/");
|
66 |
-
await this.page
|
67 |
-
.locator(".node-search")
|
68 |
-
.getByText(boxName, { exact: true })
|
69 |
-
.click();
|
70 |
await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
|
71 |
}
|
72 |
|
@@ -107,9 +104,7 @@ export class Workspace {
|
|
107 |
|
108 |
getBoxHandle(boxId: string, pos?: string) {
|
109 |
if (pos) {
|
110 |
-
return this.page.locator(
|
111 |
-
`[data-id="${boxId}"] [data-handlepos="${pos}"]`,
|
112 |
-
);
|
113 |
}
|
114 |
return this.page.getByTestId(boxId);
|
115 |
}
|
@@ -133,11 +128,9 @@ export class Workspace {
|
|
133 |
} else if (offset) {
|
134 |
// Without steps the movement is too fast and the box is not dragged. The more steps,
|
135 |
// the better the movement is captured
|
136 |
-
await this.page.mouse.move(
|
137 |
-
|
138 |
-
|
139 |
-
{ steps: 5 },
|
140 |
-
);
|
141 |
}
|
142 |
await this.page.mouse.up();
|
143 |
}
|
@@ -197,9 +190,7 @@ export class Splash {
|
|
197 |
workspaceName = name;
|
198 |
await this.page.locator('input[name="workspaceName"]').fill(name);
|
199 |
} else {
|
200 |
-
workspaceName = await this.page
|
201 |
-
.locator('input[name="workspaceName"]')
|
202 |
-
.inputValue();
|
203 |
}
|
204 |
await this.page.locator('input[name="workspaceName"]').press("Enter");
|
205 |
const ws = new Workspace(this.page, workspaceName);
|
|
|
63 |
|
64 |
await this.page.locator(".ws-name").click();
|
65 |
await this.page.keyboard.press("/");
|
66 |
+
await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
|
|
|
|
|
|
|
67 |
await expect(this.getBoxes()).toHaveCount(allBoxes.length + 1);
|
68 |
}
|
69 |
|
|
|
104 |
|
105 |
getBoxHandle(boxId: string, pos?: string) {
|
106 |
if (pos) {
|
107 |
+
return this.page.locator(`[data-id="${boxId}"] [data-handlepos="${pos}"]`);
|
|
|
|
|
108 |
}
|
109 |
return this.page.getByTestId(boxId);
|
110 |
}
|
|
|
128 |
} else if (offset) {
|
129 |
// Without steps the movement is too fast and the box is not dragged. The more steps,
|
130 |
// the better the movement is captured
|
131 |
+
await this.page.mouse.move(boxCenterX + offset.offsetX, boxCenterY + offset.offsetY, {
|
132 |
+
steps: 5,
|
133 |
+
});
|
|
|
|
|
134 |
}
|
135 |
await this.page.mouse.up();
|
136 |
}
|
|
|
190 |
workspaceName = name;
|
191 |
await this.page.locator('input[name="workspaceName"]').fill(name);
|
192 |
} else {
|
193 |
+
workspaceName = await this.page.locator('input[name="workspaceName"]').inputValue();
|
|
|
|
|
194 |
}
|
195 |
await this.page.locator('input[name="workspaceName"]').press("Enter");
|
196 |
const ws = new Workspace(this.page, workspaceName);
|
lynxkite-app/web/tsconfig.json
CHANGED
@@ -1,7 +1,4 @@
|
|
1 |
{
|
2 |
"files": [],
|
3 |
-
"references": [
|
4 |
-
{ "path": "./tsconfig.app.json" },
|
5 |
-
{ "path": "./tsconfig.node.json" }
|
6 |
-
]
|
7 |
}
|
|
|
1 |
{
|
2 |
"files": [],
|
3 |
+
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
|
|
|
|
|
|
4 |
}
|
lynxkite-bio/src/lynxkite_bio/rdkit.py
CHANGED
@@ -36,9 +36,7 @@ def _get_similarity_matrix(mols):
|
|
36 |
|
37 |
|
38 |
@op("Graph from molecule similarity")
|
39 |
-
def graph_from_similarity(
|
40 |
-
bundle: Bundle, *, table="df", mols_column="mols", average_degree=10
|
41 |
-
):
|
42 |
"""Creates edges for pairs of molecules that are the most similar."""
|
43 |
df = bundle.dfs[table]
|
44 |
mols = df[mols_column]
|
|
|
36 |
|
37 |
|
38 |
@op("Graph from molecule similarity")
|
39 |
+
def graph_from_similarity(bundle: Bundle, *, table="df", mols_column="mols", average_degree=10):
|
|
|
|
|
40 |
"""Creates edges for pairs of molecules that are the most similar."""
|
41 |
df = bundle.dfs[table]
|
42 |
mols = df[mols_column]
|
lynxkite-core/src/lynxkite/core/executors/one_by_one.py
CHANGED
@@ -167,9 +167,9 @@ async def execute(ws: workspace.Workspace, catalog, cache=None):
|
|
167 |
op = catalog[t.data.title]
|
168 |
i = op.inputs[edge.targetHandle]
|
169 |
if i.position in "top or bottom":
|
170 |
-
batch_inputs.setdefault(
|
171 |
-
|
172 |
-
)
|
173 |
else:
|
174 |
tasks.setdefault(edge.target, []).extend(results)
|
175 |
node.publish_result(result)
|
|
|
167 |
op = catalog[t.data.title]
|
168 |
i = op.inputs[edge.targetHandle]
|
169 |
if i.position in "top or bottom":
|
170 |
+
batch_inputs.setdefault((edge.target, edge.targetHandle), []).extend(
|
171 |
+
results
|
172 |
+
)
|
173 |
else:
|
174 |
tasks.setdefault(edge.target, []).extend(results)
|
175 |
node.publish_result(result)
|
lynxkite-core/src/lynxkite/core/ops.py
CHANGED
@@ -208,9 +208,7 @@ def op(env: str, name: str, *, view="basic", outputs=None, params=None):
|
|
208 |
if outputs:
|
209 |
_outputs = {name: Output(name=name, type=None) for name in outputs}
|
210 |
else:
|
211 |
-
_outputs = (
|
212 |
-
{"output": Output(name="output", type=None)} if view == "basic" else {}
|
213 |
-
)
|
214 |
op = Op(
|
215 |
func=func,
|
216 |
name=name,
|
@@ -264,12 +262,10 @@ def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], para
|
|
264 |
name=name,
|
265 |
params={p.name: p for p in params},
|
266 |
inputs=dict(
|
267 |
-
(i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i)
|
268 |
-
for i in inputs
|
269 |
),
|
270 |
outputs=dict(
|
271 |
-
(o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o)
|
272 |
-
for o in outputs
|
273 |
),
|
274 |
)
|
275 |
CATALOGS.setdefault(env, {})
|
|
|
208 |
if outputs:
|
209 |
_outputs = {name: Output(name=name, type=None) for name in outputs}
|
210 |
else:
|
211 |
+
_outputs = {"output": Output(name="output", type=None)} if view == "basic" else {}
|
|
|
|
|
212 |
op = Op(
|
213 |
func=func,
|
214 |
name=name,
|
|
|
262 |
name=name,
|
263 |
params={p.name: p for p in params},
|
264 |
inputs=dict(
|
265 |
+
(i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i) for i in inputs
|
|
|
266 |
),
|
267 |
outputs=dict(
|
268 |
+
(o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o) for o in outputs
|
|
|
269 |
),
|
270 |
)
|
271 |
CATALOGS.setdefault(env, {})
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -111,9 +111,7 @@ def save(ws: Workspace, path: str):
|
|
111 |
if dirname:
|
112 |
os.makedirs(dirname, exist_ok=True)
|
113 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
114 |
-
with tempfile.NamedTemporaryFile(
|
115 |
-
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
116 |
-
) as f:
|
117 |
temp_name = f.name
|
118 |
f.write(j)
|
119 |
os.replace(temp_name, path)
|
|
|
111 |
if dirname:
|
112 |
os.makedirs(dirname, exist_ok=True)
|
113 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
114 |
+
with tempfile.NamedTemporaryFile("w", prefix=f".{basename}.", dir=dirname, delete=False) as f:
|
|
|
|
|
115 |
temp_name = f.name
|
116 |
f.write(j)
|
117 |
os.replace(temp_name, path)
|
lynxkite-core/tests/test_ops.py
CHANGED
@@ -14,9 +14,7 @@ def test_op_decorator_no_params_no_types_default_positions():
|
|
14 |
"a": ops.Input(name="a", type=inspect._empty, position="left"),
|
15 |
"b": ops.Input(name="b", type=inspect._empty, position="left"),
|
16 |
}
|
17 |
-
assert add.__op__.outputs == {
|
18 |
-
"result": ops.Output(name="result", type=None, position="right")
|
19 |
-
}
|
20 |
assert add.__op__.type == "basic"
|
21 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
22 |
|
@@ -34,9 +32,7 @@ def test_op_decorator_custom_positions():
|
|
34 |
"a": ops.Input(name="a", type=inspect._empty, position="right"),
|
35 |
"b": ops.Input(name="b", type=inspect._empty, position="top"),
|
36 |
}
|
37 |
-
assert add.__op__.outputs == {
|
38 |
-
"result": ops.Output(name="result", type=None, position="bottom")
|
39 |
-
}
|
40 |
assert add.__op__.type == "basic"
|
41 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
42 |
|
@@ -76,9 +72,7 @@ def test_op_decorator_with_complex_types():
|
|
76 |
assert complex_op.__op__.inputs == {
|
77 |
"color": ops.Input(name="color", type=Color, position="left"),
|
78 |
"color_list": ops.Input(name="color_list", type=list[Color], position="left"),
|
79 |
-
"color_dict": ops.Input(
|
80 |
-
name="color_dict", type=dict[str, Color], position="left"
|
81 |
-
),
|
82 |
}
|
83 |
assert complex_op.__op__.type == "basic"
|
84 |
assert complex_op.__op__.outputs == {
|
|
|
14 |
"a": ops.Input(name="a", type=inspect._empty, position="left"),
|
15 |
"b": ops.Input(name="b", type=inspect._empty, position="left"),
|
16 |
}
|
17 |
+
assert add.__op__.outputs == {"result": ops.Output(name="result", type=None, position="right")}
|
|
|
|
|
18 |
assert add.__op__.type == "basic"
|
19 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
20 |
|
|
|
32 |
"a": ops.Input(name="a", type=inspect._empty, position="right"),
|
33 |
"b": ops.Input(name="b", type=inspect._empty, position="top"),
|
34 |
}
|
35 |
+
assert add.__op__.outputs == {"result": ops.Output(name="result", type=None, position="bottom")}
|
|
|
|
|
36 |
assert add.__op__.type == "basic"
|
37 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
38 |
|
|
|
72 |
assert complex_op.__op__.inputs == {
|
73 |
"color": ops.Input(name="color", type=Color, position="left"),
|
74 |
"color_list": ops.Input(name="color_list", type=list[Color], position="left"),
|
75 |
+
"color_dict": ops.Input(name="color_dict", type=dict[str, Color], position="left"),
|
|
|
|
|
76 |
}
|
77 |
assert complex_op.__op__.type == "basic"
|
78 |
assert complex_op.__op__.outputs == {
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/bionemo_ops.py
CHANGED
@@ -89,9 +89,7 @@ def download_cellxgene_dataset(
|
|
89 |
adata.write_h5ad(h5ad_outfile)
|
90 |
with tempfile.TemporaryDirectory() as temp_dir:
|
91 |
coll = SingleCellCollection(temp_dir)
|
92 |
-
coll.load_h5ad_multi(
|
93 |
-
h5ad_outfile.parent, max_workers=max_workers, use_processes=use_mp
|
94 |
-
)
|
95 |
coll.flatten(DATA_PATH / save_path, destroy_on_copy=True)
|
96 |
return DATA_PATH / save_path
|
97 |
|
@@ -148,9 +146,7 @@ def download_model(*, model_name: str) -> str:
|
|
148 |
|
149 |
@op("BioNeMo > Infer")
|
150 |
@mem.cache(verbose=1)
|
151 |
-
def infer(
|
152 |
-
dataset_path: str, model_path: str | None = None, *, results_path: str
|
153 |
-
) -> str:
|
154 |
"""Infer on a dataset."""
|
155 |
# This import is slow, so we only import it when we need it.
|
156 |
from bionemo.geneformer.scripts.infer_geneformer import infer_model
|
@@ -175,10 +171,7 @@ def infer(
|
|
175 |
@op("BioNeMo > Load results")
|
176 |
def load_results(results_path: str):
|
177 |
embeddings = (
|
178 |
-
torch.load(f"{results_path}/predictions__rank_0.pt")["embeddings"]
|
179 |
-
.float()
|
180 |
-
.cpu()
|
181 |
-
.numpy()
|
182 |
)
|
183 |
return embeddings
|
184 |
|
@@ -248,9 +241,7 @@ def run_benchmark(data, labels, *, use_pca: bool = False):
|
|
248 |
]
|
249 |
)
|
250 |
else:
|
251 |
-
pipeline = Pipeline(
|
252 |
-
[("classifier", RandomForestClassifier(class_weight="balanced"))]
|
253 |
-
)
|
254 |
|
255 |
# Set up StratifiedKFold to ensure each fold reflects the overall distribution of labels
|
256 |
cv = StratifiedKFold(n_splits=5)
|
@@ -258,9 +249,7 @@ def run_benchmark(data, labels, *, use_pca: bool = False):
|
|
258 |
# Define the scoring functions
|
259 |
scoring = {
|
260 |
"accuracy": make_scorer(accuracy_score),
|
261 |
-
"precision": make_scorer(
|
262 |
-
precision_score, average="macro"
|
263 |
-
), # 'macro' averages over classes
|
264 |
"recall": make_scorer(recall_score, average="macro"),
|
265 |
"f1_score": make_scorer(f1_score, average="macro"),
|
266 |
# 'roc_auc' requires probability or decision function; hence use multi_class if applicable
|
@@ -298,9 +287,7 @@ def plot_confusion_matrix(benchmark_output, labels):
|
|
298 |
# heatmap has the 0,0 at the bottom left corner
|
299 |
num_rows = len(str_labels)
|
300 |
heatmap_data = [
|
301 |
-
[j, num_rows - i - 1, norm_cm[i][j]]
|
302 |
-
for i in range(len(labels))
|
303 |
-
for j in range(len(labels))
|
304 |
]
|
305 |
|
306 |
options = {
|
@@ -332,9 +319,7 @@ def plot_confusion_matrix(benchmark_output, labels):
|
|
332 |
"orient": "vertical",
|
333 |
"right": 10,
|
334 |
"top": "center",
|
335 |
-
"inRange": {
|
336 |
-
"color": ["#E0F7FA", "#81D4FA", "#29B6F6", "#0288D1", "#01579B"]
|
337 |
-
},
|
338 |
},
|
339 |
"series": [
|
340 |
{
|
@@ -424,9 +409,7 @@ def accuracy_comparison(benchmark_output10m, benchmark_output100m):
|
|
424 |
{
|
425 |
"name": "Error Bars",
|
426 |
"type": "errorbar",
|
427 |
-
"data": [
|
428 |
-
[val - err, val + err] for val, err in zip(values, error_bars)
|
429 |
-
],
|
430 |
"itemStyle": {"color": "#1f77b4"},
|
431 |
},
|
432 |
],
|
@@ -509,9 +492,7 @@ def f1_comparison(benchmark_output10m, benchmark_output100m):
|
|
509 |
{
|
510 |
"name": "Error Bars",
|
511 |
"type": "errorbar",
|
512 |
-
"data": [
|
513 |
-
[val - err, val + err] for val, err in zip(values, error_bars)
|
514 |
-
],
|
515 |
"itemStyle": {"color": "#1f77b4"},
|
516 |
},
|
517 |
],
|
|
|
89 |
adata.write_h5ad(h5ad_outfile)
|
90 |
with tempfile.TemporaryDirectory() as temp_dir:
|
91 |
coll = SingleCellCollection(temp_dir)
|
92 |
+
coll.load_h5ad_multi(h5ad_outfile.parent, max_workers=max_workers, use_processes=use_mp)
|
|
|
|
|
93 |
coll.flatten(DATA_PATH / save_path, destroy_on_copy=True)
|
94 |
return DATA_PATH / save_path
|
95 |
|
|
|
146 |
|
147 |
@op("BioNeMo > Infer")
|
148 |
@mem.cache(verbose=1)
|
149 |
+
def infer(dataset_path: str, model_path: str | None = None, *, results_path: str) -> str:
|
|
|
|
|
150 |
"""Infer on a dataset."""
|
151 |
# This import is slow, so we only import it when we need it.
|
152 |
from bionemo.geneformer.scripts.infer_geneformer import infer_model
|
|
|
171 |
@op("BioNeMo > Load results")
|
172 |
def load_results(results_path: str):
|
173 |
embeddings = (
|
174 |
+
torch.load(f"{results_path}/predictions__rank_0.pt")["embeddings"].float().cpu().numpy()
|
|
|
|
|
|
|
175 |
)
|
176 |
return embeddings
|
177 |
|
|
|
241 |
]
|
242 |
)
|
243 |
else:
|
244 |
+
pipeline = Pipeline([("classifier", RandomForestClassifier(class_weight="balanced"))])
|
|
|
|
|
245 |
|
246 |
# Set up StratifiedKFold to ensure each fold reflects the overall distribution of labels
|
247 |
cv = StratifiedKFold(n_splits=5)
|
|
|
249 |
# Define the scoring functions
|
250 |
scoring = {
|
251 |
"accuracy": make_scorer(accuracy_score),
|
252 |
+
"precision": make_scorer(precision_score, average="macro"), # 'macro' averages over classes
|
|
|
|
|
253 |
"recall": make_scorer(recall_score, average="macro"),
|
254 |
"f1_score": make_scorer(f1_score, average="macro"),
|
255 |
# 'roc_auc' requires probability or decision function; hence use multi_class if applicable
|
|
|
287 |
# heatmap has the 0,0 at the bottom left corner
|
288 |
num_rows = len(str_labels)
|
289 |
heatmap_data = [
|
290 |
+
[j, num_rows - i - 1, norm_cm[i][j]] for i in range(len(labels)) for j in range(len(labels))
|
|
|
|
|
291 |
]
|
292 |
|
293 |
options = {
|
|
|
319 |
"orient": "vertical",
|
320 |
"right": 10,
|
321 |
"top": "center",
|
322 |
+
"inRange": {"color": ["#E0F7FA", "#81D4FA", "#29B6F6", "#0288D1", "#01579B"]},
|
|
|
|
|
323 |
},
|
324 |
"series": [
|
325 |
{
|
|
|
409 |
{
|
410 |
"name": "Error Bars",
|
411 |
"type": "errorbar",
|
412 |
+
"data": [[val - err, val + err] for val, err in zip(values, error_bars)],
|
|
|
|
|
413 |
"itemStyle": {"color": "#1f77b4"},
|
414 |
},
|
415 |
],
|
|
|
492 |
{
|
493 |
"name": "Error Bars",
|
494 |
"type": "errorbar",
|
495 |
+
"data": [[val - err, val + err] for val, err in zip(values, error_bars)],
|
|
|
|
|
496 |
"itemStyle": {"color": "#1f77b4"},
|
497 |
},
|
498 |
],
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py
CHANGED
@@ -19,12 +19,8 @@ class RelationDefinition:
|
|
19 |
"""Defines a set of edges."""
|
20 |
|
21 |
df: str # The DataFrame that contains the edges.
|
22 |
-
source_column:
|
23 |
-
|
24 |
-
)
|
25 |
-
target_column: (
|
26 |
-
str # The column in the edge DataFrame that contains the target node ID.
|
27 |
-
)
|
28 |
source_table: str # The DataFrame that contains the source nodes.
|
29 |
target_table: str # The DataFrame that contains the target nodes.
|
30 |
source_key: str # The column in the source table that contains the node ID.
|
@@ -86,11 +82,7 @@ class Bundle:
|
|
86 |
(
|
87 |
e["source"],
|
88 |
e["target"],
|
89 |
-
{
|
90 |
-
k: e[k]
|
91 |
-
for k in edges.columns
|
92 |
-
if k not in ["source", "target"]
|
93 |
-
},
|
94 |
)
|
95 |
for e in edges.to_records()
|
96 |
]
|
@@ -129,9 +121,7 @@ class Bundle:
|
|
129 |
for name, df in self.dfs.items()
|
130 |
},
|
131 |
"relations": [dataclasses.asdict(relation) for relation in self.relations],
|
132 |
-
"other": {
|
133 |
-
k: getattr(v, "metadata", lambda: {})() for k, v in self.other.items()
|
134 |
-
},
|
135 |
}
|
136 |
|
137 |
|
@@ -199,9 +189,7 @@ def _execute_node(node, ws, catalog, outputs):
|
|
199 |
node.publish_started()
|
200 |
# TODO: Handle multi-inputs.
|
201 |
input_map = {
|
202 |
-
edge.targetHandle: outputs[edge.source]
|
203 |
-
for edge in ws.edges
|
204 |
-
if edge.target == node.id
|
205 |
}
|
206 |
# Convert inputs types to match operation signature.
|
207 |
try:
|
|
|
19 |
"""Defines a set of edges."""
|
20 |
|
21 |
df: str # The DataFrame that contains the edges.
|
22 |
+
source_column: str # The column in the edge DataFrame that contains the source node ID.
|
23 |
+
target_column: str # The column in the edge DataFrame that contains the target node ID.
|
|
|
|
|
|
|
|
|
24 |
source_table: str # The DataFrame that contains the source nodes.
|
25 |
target_table: str # The DataFrame that contains the target nodes.
|
26 |
source_key: str # The column in the source table that contains the node ID.
|
|
|
82 |
(
|
83 |
e["source"],
|
84 |
e["target"],
|
85 |
+
{k: e[k] for k in edges.columns if k not in ["source", "target"]},
|
|
|
|
|
|
|
|
|
86 |
)
|
87 |
for e in edges.to_records()
|
88 |
]
|
|
|
121 |
for name, df in self.dfs.items()
|
122 |
},
|
123 |
"relations": [dataclasses.asdict(relation) for relation in self.relations],
|
124 |
+
"other": {k: getattr(v, "metadata", lambda: {})() for k, v in self.other.items()},
|
|
|
|
|
125 |
}
|
126 |
|
127 |
|
|
|
189 |
node.publish_started()
|
190 |
# TODO: Handle multi-inputs.
|
191 |
input_map = {
|
192 |
+
edge.targetHandle: outputs[edge.source] for edge in ws.edges if edge.target == node.id
|
|
|
|
|
193 |
}
|
194 |
# Convert inputs types to match operation signature.
|
195 |
try:
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py
CHANGED
@@ -35,9 +35,7 @@ class FileFormat(enum.StrEnum):
|
|
35 |
params={
|
36 |
"file_format": ops.ParameterGroup(
|
37 |
name="file_format",
|
38 |
-
selector=ops.Parameter(
|
39 |
-
name="file_format", type=FileFormat, default=FileFormat.csv
|
40 |
-
),
|
41 |
groups={
|
42 |
"csv": [
|
43 |
ops.Parameter.basic("columns", type=str, default="<from file>"),
|
@@ -45,9 +43,7 @@ class FileFormat(enum.StrEnum):
|
|
45 |
],
|
46 |
"parquet": [],
|
47 |
"json": [],
|
48 |
-
"excel": [
|
49 |
-
ops.Parameter.basic("sheet_name", type=str, default="Sheet1")
|
50 |
-
],
|
51 |
},
|
52 |
default=FileFormat.csv,
|
53 |
),
|
@@ -68,9 +64,7 @@ def import_file(
|
|
68 |
"""
|
69 |
if file_format == "csv":
|
70 |
names = kwargs.pop("columns", "<from file>")
|
71 |
-
names = (
|
72 |
-
pd.api.extensions.no_default if names == "<from file>" else names.split(",")
|
73 |
-
)
|
74 |
sep = kwargs.pop("separator", "<auto>")
|
75 |
sep = pd.api.extensions.no_default if sep == "<auto>" else sep
|
76 |
df = pd.read_csv(file_path, names=names, sep=sep, **kwargs)
|
@@ -93,15 +87,11 @@ def import_parquet(*, filename: str):
|
|
93 |
|
94 |
@op("Import CSV")
|
95 |
@mem.cache
|
96 |
-
def import_csv(
|
97 |
-
*, filename: str, columns: str = "<from file>", separator: str = "<auto>"
|
98 |
-
):
|
99 |
"""Imports a CSV file."""
|
100 |
return pd.read_csv(
|
101 |
filename,
|
102 |
-
names=pd.api.extensions.no_default
|
103 |
-
if columns == "<from file>"
|
104 |
-
else columns.split(","),
|
105 |
sep=pd.api.extensions.no_default if separator == "<auto>" else separator,
|
106 |
)
|
107 |
|
@@ -145,12 +135,7 @@ def sql(bundle: core.Bundle, *, query: ops.LongStr, save_as: str = "result"):
|
|
145 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
146 |
with pl.Config() as cfg:
|
147 |
cfg.set_verbose(True)
|
148 |
-
res = (
|
149 |
-
pl.SQLContext(bundle.dfs)
|
150 |
-
.execute(query)
|
151 |
-
.collect(engine="gpu")
|
152 |
-
.to_pandas()
|
153 |
-
)
|
154 |
# TODO: Currently `collect()` moves the data from cuDF to Polars. Then we convert it to Pandas,
|
155 |
# which (hopefully) puts it back into cuDF. Hopefully we will be able to keep it in cuDF.
|
156 |
else:
|
@@ -211,9 +196,7 @@ def _map_color(value):
|
|
211 |
colors = cmap.colors[: len(categories)]
|
212 |
return [
|
213 |
"#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
214 |
-
for r, g, b in [
|
215 |
-
colors[min(len(colors) - 1, categories.get_loc(v))] for v in value
|
216 |
-
]
|
217 |
]
|
218 |
|
219 |
|
@@ -250,14 +233,10 @@ def visualize_graph(
|
|
250 |
curveness = 0 # Street maps are better with straight streets.
|
251 |
break
|
252 |
else:
|
253 |
-
pos = nx.spring_layout(
|
254 |
-
graph.to_nx(), iterations=max(1, int(10000 / len(nodes)))
|
255 |
-
)
|
256 |
curveness = 0.3
|
257 |
nodes = nodes.to_records()
|
258 |
-
edges = core.df_for_frontend(
|
259 |
-
graph.dfs["edges"].drop_duplicates(["source", "target"]), 10_000
|
260 |
-
)
|
261 |
if color_edges_by:
|
262 |
edges["color"] = _map_color(edges[color_edges_by])
|
263 |
edges = edges.to_records()
|
@@ -291,9 +270,7 @@ def visualize_graph(
|
|
291 |
"itemStyle": {"color": n.color} if color_nodes_by else {},
|
292 |
"label": {"show": label_by is not None},
|
293 |
"name": str(getattr(n, label_by, "")) if label_by else None,
|
294 |
-
"value": str(getattr(n, color_nodes_by, ""))
|
295 |
-
if color_nodes_by
|
296 |
-
else None,
|
297 |
}
|
298 |
for n in nodes
|
299 |
],
|
@@ -302,9 +279,7 @@ def visualize_graph(
|
|
302 |
"source": str(r.source),
|
303 |
"target": str(r.target),
|
304 |
"lineStyle": {"color": r.color} if color_edges_by else {},
|
305 |
-
"value": str(getattr(r, color_edges_by, ""))
|
306 |
-
if color_edges_by
|
307 |
-
else None,
|
308 |
}
|
309 |
for r in edges
|
310 |
],
|
@@ -342,9 +317,7 @@ def create_graph(bundle: core.Bundle, *, relations: str = None) -> core.Bundle:
|
|
342 |
"""
|
343 |
bundle = bundle.copy()
|
344 |
if not (relations is None or relations.strip() == ""):
|
345 |
-
bundle.relations = [
|
346 |
-
core.RelationDefinition(**r) for r in json.loads(relations).values()
|
347 |
-
]
|
348 |
return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
|
349 |
|
350 |
|
|
|
35 |
params={
|
36 |
"file_format": ops.ParameterGroup(
|
37 |
name="file_format",
|
38 |
+
selector=ops.Parameter(name="file_format", type=FileFormat, default=FileFormat.csv),
|
|
|
|
|
39 |
groups={
|
40 |
"csv": [
|
41 |
ops.Parameter.basic("columns", type=str, default="<from file>"),
|
|
|
43 |
],
|
44 |
"parquet": [],
|
45 |
"json": [],
|
46 |
+
"excel": [ops.Parameter.basic("sheet_name", type=str, default="Sheet1")],
|
|
|
|
|
47 |
},
|
48 |
default=FileFormat.csv,
|
49 |
),
|
|
|
64 |
"""
|
65 |
if file_format == "csv":
|
66 |
names = kwargs.pop("columns", "<from file>")
|
67 |
+
names = pd.api.extensions.no_default if names == "<from file>" else names.split(",")
|
|
|
|
|
68 |
sep = kwargs.pop("separator", "<auto>")
|
69 |
sep = pd.api.extensions.no_default if sep == "<auto>" else sep
|
70 |
df = pd.read_csv(file_path, names=names, sep=sep, **kwargs)
|
|
|
87 |
|
88 |
@op("Import CSV")
|
89 |
@mem.cache
|
90 |
+
def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
|
|
|
|
|
91 |
"""Imports a CSV file."""
|
92 |
return pd.read_csv(
|
93 |
filename,
|
94 |
+
names=pd.api.extensions.no_default if columns == "<from file>" else columns.split(","),
|
|
|
|
|
95 |
sep=pd.api.extensions.no_default if separator == "<auto>" else separator,
|
96 |
)
|
97 |
|
|
|
135 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
136 |
with pl.Config() as cfg:
|
137 |
cfg.set_verbose(True)
|
138 |
+
res = pl.SQLContext(bundle.dfs).execute(query).collect(engine="gpu").to_pandas()
|
|
|
|
|
|
|
|
|
|
|
139 |
# TODO: Currently `collect()` moves the data from cuDF to Polars. Then we convert it to Pandas,
|
140 |
# which (hopefully) puts it back into cuDF. Hopefully we will be able to keep it in cuDF.
|
141 |
else:
|
|
|
196 |
colors = cmap.colors[: len(categories)]
|
197 |
return [
|
198 |
"#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
199 |
+
for r, g, b in [colors[min(len(colors) - 1, categories.get_loc(v))] for v in value]
|
|
|
|
|
200 |
]
|
201 |
|
202 |
|
|
|
233 |
curveness = 0 # Street maps are better with straight streets.
|
234 |
break
|
235 |
else:
|
236 |
+
pos = nx.spring_layout(graph.to_nx(), iterations=max(1, int(10000 / len(nodes))))
|
|
|
|
|
237 |
curveness = 0.3
|
238 |
nodes = nodes.to_records()
|
239 |
+
edges = core.df_for_frontend(graph.dfs["edges"].drop_duplicates(["source", "target"]), 10_000)
|
|
|
|
|
240 |
if color_edges_by:
|
241 |
edges["color"] = _map_color(edges[color_edges_by])
|
242 |
edges = edges.to_records()
|
|
|
270 |
"itemStyle": {"color": n.color} if color_nodes_by else {},
|
271 |
"label": {"show": label_by is not None},
|
272 |
"name": str(getattr(n, label_by, "")) if label_by else None,
|
273 |
+
"value": str(getattr(n, color_nodes_by, "")) if color_nodes_by else None,
|
|
|
|
|
274 |
}
|
275 |
for n in nodes
|
276 |
],
|
|
|
279 |
"source": str(r.source),
|
280 |
"target": str(r.target),
|
281 |
"lineStyle": {"color": r.color} if color_edges_by else {},
|
282 |
+
"value": str(getattr(r, color_edges_by, "")) if color_edges_by else None,
|
|
|
|
|
283 |
}
|
284 |
for r in edges
|
285 |
],
|
|
|
317 |
"""
|
318 |
bundle = bundle.copy()
|
319 |
if not (relations is None or relations.strip() == ""):
|
320 |
+
bundle.relations = [core.RelationDefinition(**r) for r in json.loads(relations).values()]
|
|
|
|
|
321 |
return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
|
322 |
|
323 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py
CHANGED
@@ -210,9 +210,7 @@ def _get_params(func) -> dict | None:
|
|
210 |
continue
|
211 |
params[name] = ops.Parameter.basic(
|
212 |
name=name,
|
213 |
-
default=str(param.default)
|
214 |
-
if type(param.default) in [str, int, float]
|
215 |
-
else None,
|
216 |
type=_type,
|
217 |
)
|
218 |
return params
|
|
|
210 |
continue
|
211 |
params[name] = ops.Parameter.basic(
|
212 |
name=name,
|
213 |
+
default=str(param.default) if type(param.default) in [str, int, float] else None,
|
|
|
|
|
214 |
type=_type,
|
215 |
)
|
216 |
return params
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py
CHANGED
@@ -21,12 +21,8 @@ def reg(name, inputs=[], outputs=None, params=[]):
|
|
21 |
return ops.register_passive_op(
|
22 |
ENV,
|
23 |
name,
|
24 |
-
inputs=[
|
25 |
-
|
26 |
-
],
|
27 |
-
outputs=[
|
28 |
-
ops.Output(name=name, position="top", type="tensor") for name in outputs
|
29 |
-
],
|
30 |
params=params,
|
31 |
)
|
32 |
|
@@ -196,9 +192,7 @@ class ModelConfig:
|
|
196 |
}
|
197 |
|
198 |
|
199 |
-
def build_model(
|
200 |
-
ws: workspace.Workspace, inputs: dict[str, torch.Tensor]
|
201 |
-
) -> ModelConfig:
|
202 |
"""Builds the model described in the workspace."""
|
203 |
catalog = ops.CATALOGS[ENV]
|
204 |
optimizers = []
|
@@ -272,9 +266,7 @@ def build_model(
|
|
272 |
ls.append((torch.nn.Linear(isize, osize), f"{inputs.x} -> {outputs.x}"))
|
273 |
sizes[outputs.x] = osize
|
274 |
case "Activation":
|
275 |
-
f = getattr(
|
276 |
-
torch.nn.functional, p["type"].name.lower().replace(" ", "_")
|
277 |
-
)
|
278 |
ls.append((f, f"{inputs.x} -> {outputs.x}"))
|
279 |
sizes[outputs.x] = sizes.get(inputs.x, 1)
|
280 |
case "MSE loss":
|
@@ -316,7 +308,5 @@ def to_tensors(b: core.Bundle, m: ModelMapping | None) -> dict[str, torch.Tensor
|
|
316 |
tensors = {}
|
317 |
for k, v in m.map.items():
|
318 |
if v.df in b.dfs and v.column in b.dfs[v.df]:
|
319 |
-
tensors[k] = torch.tensor(
|
320 |
-
b.dfs[v.df][v.column].to_list(), dtype=torch.float32
|
321 |
-
)
|
322 |
return tensors
|
|
|
21 |
return ops.register_passive_op(
|
22 |
ENV,
|
23 |
name,
|
24 |
+
inputs=[ops.Input(name=name, position="bottom", type="tensor") for name in inputs],
|
25 |
+
outputs=[ops.Output(name=name, position="top", type="tensor") for name in outputs],
|
|
|
|
|
|
|
|
|
26 |
params=params,
|
27 |
)
|
28 |
|
|
|
192 |
}
|
193 |
|
194 |
|
195 |
+
def build_model(ws: workspace.Workspace, inputs: dict[str, torch.Tensor]) -> ModelConfig:
|
|
|
|
|
196 |
"""Builds the model described in the workspace."""
|
197 |
catalog = ops.CATALOGS[ENV]
|
198 |
optimizers = []
|
|
|
266 |
ls.append((torch.nn.Linear(isize, osize), f"{inputs.x} -> {outputs.x}"))
|
267 |
sizes[outputs.x] = osize
|
268 |
case "Activation":
|
269 |
+
f = getattr(torch.nn.functional, p["type"].name.lower().replace(" ", "_"))
|
|
|
|
|
270 |
ls.append((f, f"{inputs.x} -> {outputs.x}"))
|
271 |
sizes[outputs.x] = sizes.get(inputs.x, 1)
|
272 |
case "MSE loss":
|
|
|
308 |
tensors = {}
|
309 |
for k, v in m.map.items():
|
310 |
if v.df in b.dfs and v.column in b.dfs[v.df]:
|
311 |
+
tensors[k] = torch.tensor(b.dfs[v.df][v.column].to_list(), dtype=torch.float32)
|
|
|
|
|
312 |
return tensors
|
lynxkite-lynxscribe/src/lynxkite_lynxscribe/llm_ops.py
CHANGED
@@ -32,9 +32,7 @@ def chat(*args, **kwargs):
|
|
32 |
|
33 |
chat_client = openai.OpenAI(base_url=LLM_BASE_URL)
|
34 |
kwargs.setdefault("model", LLM_MODEL)
|
35 |
-
key = json.dumps(
|
36 |
-
{"method": "chat", "base_url": LLM_BASE_URL, "args": args, "kwargs": kwargs}
|
37 |
-
)
|
38 |
if key not in LLM_CACHE:
|
39 |
completion = chat_client.chat.completions.create(*args, **kwargs)
|
40 |
LLM_CACHE[key] = [c.message.content for c in completion.choices]
|
@@ -121,9 +119,7 @@ def add_neighbors(nodes, edges, item):
|
|
121 |
|
122 |
@op("Create prompt")
|
123 |
def create_prompt(input, *, save_as="prompt", template: ops.LongStr):
|
124 |
-
assert template,
|
125 |
-
"Please specify the template. Refer to columns using the Jinja2 syntax."
|
126 |
-
)
|
127 |
t = jinja.from_string(template)
|
128 |
prompt = t.render(**input)
|
129 |
return {**input, save_as: prompt}
|
|
|
32 |
|
33 |
chat_client = openai.OpenAI(base_url=LLM_BASE_URL)
|
34 |
kwargs.setdefault("model", LLM_MODEL)
|
35 |
+
key = json.dumps({"method": "chat", "base_url": LLM_BASE_URL, "args": args, "kwargs": kwargs})
|
|
|
|
|
36 |
if key not in LLM_CACHE:
|
37 |
completion = chat_client.chat.completions.create(*args, **kwargs)
|
38 |
LLM_CACHE[key] = [c.message.content for c in completion.choices]
|
|
|
119 |
|
120 |
@op("Create prompt")
|
121 |
def create_prompt(input, *, save_as="prompt", template: ops.LongStr):
|
122 |
+
assert template, "Please specify the template. Refer to columns using the Jinja2 syntax."
|
|
|
|
|
123 |
t = jinja.from_string(template)
|
124 |
prompt = t.render(**input)
|
125 |
return {**input, save_as: prompt}
|