Spaces:
Runtime error
Runtime error
Merge pull request #41 from lynxkite/darabos-ty
Browse files- .github/workflows/test.yaml +15 -27
- .pre-commit-config.yaml +10 -0
- examples/fake_data.py +1 -1
- examples/word2vec.py +1 -1
- lynxkite-app/src/lynxkite_app/crdt.py +17 -5
- lynxkite-app/src/lynxkite_app/main.py +2 -2
- lynxkite-core/src/lynxkite/core/executors/one_by_one.py +6 -4
- lynxkite-core/src/lynxkite/core/ops.py +2 -1
- lynxkite-core/src/lynxkite/core/workspace.py +25 -12
- lynxkite-core/tests/test_one_by_one.py +2 -2
- lynxkite-core/tests/test_ops.py +19 -13
- lynxkite-core/tests/test_simple.py +2 -2
- lynxkite-core/tests/test_workspace.py +6 -5
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py +1 -1
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py +2 -2
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py +9 -4
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/ml_ops.py +1 -1
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py +36 -34
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch/pytorch_core.py +14 -10
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch/pytorch_ops.py +5 -5
- lynxkite-graph-analytics/tests/test_core.py +3 -3
- lynxkite-graph-analytics/tests/test_lynxkite_ops.py +10 -10
- lynxkite-graph-analytics/tests/test_pytorch_model_ops.py +5 -1
- lynxkite-pillow-example/src/lynxkite_pillow_example/__init__.py +9 -9
.github/workflows/test.yaml
CHANGED
|
@@ -6,19 +6,20 @@ on:
|
|
| 6 |
jobs:
|
| 7 |
test:
|
| 8 |
runs-on: ubuntu-latest
|
| 9 |
-
env:
|
| 10 |
-
UV_SYSTEM_PYTHON: 1
|
| 11 |
steps:
|
| 12 |
- uses: actions/checkout@v4
|
| 13 |
|
| 14 |
- name: Install uv
|
| 15 |
-
uses: astral-sh/setup-uv@
|
| 16 |
-
with:
|
| 17 |
-
enable-cache: true
|
| 18 |
|
| 19 |
-
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
- name: Install dependencies
|
| 24 |
run: |
|
|
@@ -33,27 +34,16 @@ jobs:
|
|
| 33 |
- name: Run pre-commits
|
| 34 |
run: |
|
| 35 |
uv pip install pre-commit
|
| 36 |
-
pre-commit run --all-files
|
| 37 |
-
|
| 38 |
-
- name: Run core tests
|
| 39 |
-
run: |
|
| 40 |
-
cd lynxkite-core
|
| 41 |
-
pytest
|
| 42 |
-
|
| 43 |
-
- name: Run app tests
|
| 44 |
-
run: |
|
| 45 |
-
cd lynxkite-app
|
| 46 |
-
pytest
|
| 47 |
|
| 48 |
-
- name: Run
|
| 49 |
run: |
|
| 50 |
-
|
| 51 |
-
pytest
|
| 52 |
|
| 53 |
-
- name:
|
| 54 |
run: |
|
| 55 |
uv pip install mkdocs-material mkdocstrings[python]
|
| 56 |
-
mkdocs build
|
| 57 |
|
| 58 |
- uses: actions/setup-node@v4
|
| 59 |
with:
|
|
@@ -67,9 +57,7 @@ jobs:
|
|
| 67 |
|
| 68 |
- name: Run Playwright tests
|
| 69 |
run: |
|
| 70 |
-
cd lynxkite-app/web
|
| 71 |
-
npm run build
|
| 72 |
-
npm run test
|
| 73 |
|
| 74 |
- uses: actions/upload-artifact@v4
|
| 75 |
name: Upload playwright report
|
|
|
|
| 6 |
jobs:
|
| 7 |
test:
|
| 8 |
runs-on: ubuntu-latest
|
|
|
|
|
|
|
| 9 |
steps:
|
| 10 |
- uses: actions/checkout@v4
|
| 11 |
|
| 12 |
- name: Install uv
|
| 13 |
+
uses: astral-sh/setup-uv@v6
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
- name: Set up Python
|
| 16 |
+
run: uv python install
|
| 17 |
+
|
| 18 |
+
# - name: Debug ty issue
|
| 19 |
+
# run: |
|
| 20 |
+
# uv pip install ty
|
| 21 |
+
# uv pip list
|
| 22 |
+
# uv run python -m ty check
|
| 23 |
|
| 24 |
- name: Install dependencies
|
| 25 |
run: |
|
|
|
|
| 34 |
- name: Run pre-commits
|
| 35 |
run: |
|
| 36 |
uv pip install pre-commit
|
| 37 |
+
uv run pre-commit run --all-files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
- name: Run Python unittests
|
| 40 |
run: |
|
| 41 |
+
uv run pytest --asyncio-mode=auto
|
|
|
|
| 42 |
|
| 43 |
+
- name: Build the documentation
|
| 44 |
run: |
|
| 45 |
uv pip install mkdocs-material mkdocstrings[python]
|
| 46 |
+
uv run mkdocs build
|
| 47 |
|
| 48 |
- uses: actions/setup-node@v4
|
| 49 |
with:
|
|
|
|
| 57 |
|
| 58 |
- name: Run Playwright tests
|
| 59 |
run: |
|
| 60 |
+
uv run bash -c 'cd lynxkite-app/web; npm run build; npm run test'
|
|
|
|
|
|
|
| 61 |
|
| 62 |
- uses: actions/upload-artifact@v4
|
| 63 |
name: Upload playwright report
|
.pre-commit-config.yaml
CHANGED
|
@@ -15,3 +15,13 @@ repos:
|
|
| 15 |
rev: v1.9.4
|
| 16 |
hooks:
|
| 17 |
- id: biome-check
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
rev: v1.9.4
|
| 16 |
hooks:
|
| 17 |
- id: biome-check
|
| 18 |
+
# https://github.com/astral-sh/ty/issues/269
|
| 19 |
+
- repo: local
|
| 20 |
+
hooks:
|
| 21 |
+
- id: ty-check
|
| 22 |
+
name: ty-check
|
| 23 |
+
language: python
|
| 24 |
+
entry: uv run ty check
|
| 25 |
+
pass_filenames: false
|
| 26 |
+
args: [--python=.venv/]
|
| 27 |
+
additional_dependencies: [ty]
|
examples/fake_data.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from lynxkite.core.ops import op
|
| 2 |
-
from faker import Faker
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
faker = Faker()
|
|
|
|
| 1 |
from lynxkite.core.ops import op
|
| 2 |
+
from faker import Faker # ty: ignore[unresolved-import]
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
faker = Faker()
|
examples/word2vec.py
CHANGED
|
@@ -6,7 +6,7 @@ ENV = "LynxKite Graph Analytics"
|
|
| 6 |
|
| 7 |
@op(ENV, "Word2vec for the top 1000 words", slow=True)
|
| 8 |
def word2vec_1000():
|
| 9 |
-
import staticvectors
|
| 10 |
|
| 11 |
model = staticvectors.StaticVectors("neuml/word2vec-quantized")
|
| 12 |
df = pd.read_csv(
|
|
|
|
| 6 |
|
| 7 |
@op(ENV, "Word2vec for the top 1000 words", slow=True)
|
| 8 |
def word2vec_1000():
|
| 9 |
+
import staticvectors # ty: ignore[unresolved-import]
|
| 10 |
|
| 11 |
model = staticvectors.StaticVectors("neuml/word2vec-quantized")
|
| 12 |
df = pd.read_csv(
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
|
@@ -7,8 +7,8 @@ import pathlib
|
|
| 7 |
import fastapi
|
| 8 |
import os.path
|
| 9 |
import pycrdt.websocket
|
| 10 |
-
import pycrdt.store
|
| 11 |
-
import uvicorn
|
| 12 |
import builtins
|
| 13 |
from lynxkite.core import workspace, ops
|
| 14 |
|
|
@@ -59,7 +59,8 @@ class WorkspaceWebsocketServer(pycrdt.websocket.WebsocketServer):
|
|
| 59 |
room = pycrdt.websocket.YRoom(
|
| 60 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 61 |
)
|
| 62 |
-
room
|
|
|
|
| 63 |
|
| 64 |
def on_change(changes):
|
| 65 |
task = asyncio.create_task(workspace_changed(name, changes, ws))
|
|
@@ -106,7 +107,8 @@ class CodeWebsocketServer(WorkspaceWebsocketServer):
|
|
| 106 |
room = pycrdt.websocket.YRoom(
|
| 107 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 108 |
)
|
| 109 |
-
room
|
|
|
|
| 110 |
|
| 111 |
def on_change(changes):
|
| 112 |
asyncio.create_task(code_changed(name, changes, text))
|
|
@@ -163,6 +165,7 @@ def crdt_update(
|
|
| 163 |
ValueError: If the Python object provided is not a dict or list.
|
| 164 |
"""
|
| 165 |
if isinstance(python_obj, dict):
|
|
|
|
| 166 |
for key, value in python_obj.items():
|
| 167 |
if key in non_collaborative_fields:
|
| 168 |
crdt_obj[key] = value
|
|
@@ -179,6 +182,7 @@ def crdt_update(
|
|
| 179 |
else:
|
| 180 |
crdt_obj[key] = value
|
| 181 |
elif isinstance(python_obj, list):
|
|
|
|
| 182 |
for i, value in enumerate(python_obj):
|
| 183 |
if isinstance(value, dict):
|
| 184 |
if i >= len(crdt_obj):
|
|
@@ -220,7 +224,7 @@ last_known_versions = {}
|
|
| 220 |
delayed_executions = {}
|
| 221 |
|
| 222 |
|
| 223 |
-
async def workspace_changed(name: str, changes: pycrdt.MapEvent, ws_crdt: pycrdt.Map):
|
| 224 |
"""Callback to react to changes in the workspace.
|
| 225 |
|
| 226 |
Args:
|
|
@@ -295,6 +299,14 @@ async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
|
|
| 295 |
f.write(contents)
|
| 296 |
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
@contextlib.asynccontextmanager
|
| 299 |
async def lifespan(app):
|
| 300 |
global ws_websocket_server
|
|
|
|
| 7 |
import fastapi
|
| 8 |
import os.path
|
| 9 |
import pycrdt.websocket
|
| 10 |
+
import pycrdt.store.file
|
| 11 |
+
import uvicorn.protocols.utils
|
| 12 |
import builtins
|
| 13 |
from lynxkite.core import workspace, ops
|
| 14 |
|
|
|
|
| 59 |
room = pycrdt.websocket.YRoom(
|
| 60 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 61 |
)
|
| 62 |
+
# We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
|
| 63 |
+
room.ws = ws # ty: ignore[unresolved-attribute]
|
| 64 |
|
| 65 |
def on_change(changes):
|
| 66 |
task = asyncio.create_task(workspace_changed(name, changes, ws))
|
|
|
|
| 107 |
room = pycrdt.websocket.YRoom(
|
| 108 |
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 109 |
)
|
| 110 |
+
# We hang the YDoc pointer on the room, so it only gets garbage collected when the room does.
|
| 111 |
+
room.text = text # ty: ignore[unresolved-attribute]
|
| 112 |
|
| 113 |
def on_change(changes):
|
| 114 |
asyncio.create_task(code_changed(name, changes, text))
|
|
|
|
| 165 |
ValueError: If the Python object provided is not a dict or list.
|
| 166 |
"""
|
| 167 |
if isinstance(python_obj, dict):
|
| 168 |
+
assert isinstance(crdt_obj, pycrdt.Map), "CRDT object must be a Map for a dict input"
|
| 169 |
for key, value in python_obj.items():
|
| 170 |
if key in non_collaborative_fields:
|
| 171 |
crdt_obj[key] = value
|
|
|
|
| 182 |
else:
|
| 183 |
crdt_obj[key] = value
|
| 184 |
elif isinstance(python_obj, list):
|
| 185 |
+
assert isinstance(crdt_obj, pycrdt.Array), "CRDT object must be an Array for a list input"
|
| 186 |
for i, value in enumerate(python_obj):
|
| 187 |
if isinstance(value, dict):
|
| 188 |
if i >= len(crdt_obj):
|
|
|
|
| 224 |
delayed_executions = {}
|
| 225 |
|
| 226 |
|
| 227 |
+
async def workspace_changed(name: str, changes: list[pycrdt.MapEvent], ws_crdt: pycrdt.Map):
|
| 228 |
"""Callback to react to changes in the workspace.
|
| 229 |
|
| 230 |
Args:
|
|
|
|
| 299 |
f.write(contents)
|
| 300 |
|
| 301 |
|
| 302 |
+
ws_websocket_server: WorkspaceWebsocketServer
|
| 303 |
+
code_websocket_server: CodeWebsocketServer
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def get_room(name):
|
| 307 |
+
return ws_websocket_server.get_room(name)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
@contextlib.asynccontextmanager
|
| 311 |
async def lifespan(app):
|
| 312 |
global ws_websocket_server
|
lynxkite-app/src/lynxkite_app/main.py
CHANGED
|
@@ -8,7 +8,7 @@ import pathlib
|
|
| 8 |
import pkgutil
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
from fastapi.middleware.gzip import GZipMiddleware
|
| 11 |
-
import starlette
|
| 12 |
from lynxkite.core import ops
|
| 13 |
from lynxkite.core import workspace
|
| 14 |
from . import crdt
|
|
@@ -136,7 +136,7 @@ async def upload(req: fastapi.Request):
|
|
| 136 |
@app.post("/api/execute_workspace")
|
| 137 |
async def execute_workspace(name: str):
|
| 138 |
"""Trigger and await the execution of a workspace."""
|
| 139 |
-
room = await crdt.
|
| 140 |
ws_pyd = workspace.Workspace.model_validate(room.ws.to_py())
|
| 141 |
await crdt.execute(name, room.ws, ws_pyd)
|
| 142 |
|
|
|
|
| 8 |
import pkgutil
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
from fastapi.middleware.gzip import GZipMiddleware
|
| 11 |
+
import starlette.exceptions
|
| 12 |
from lynxkite.core import ops
|
| 13 |
from lynxkite.core import workspace
|
| 14 |
from . import crdt
|
|
|
|
| 136 |
@app.post("/api/execute_workspace")
|
| 137 |
async def execute_workspace(name: str):
|
| 138 |
"""Trigger and await the execution of a workspace."""
|
| 139 |
+
room = await crdt.get_room(name)
|
| 140 |
ws_pyd = workspace.Workspace.model_validate(room.ws.to_py())
|
| 141 |
await crdt.execute(name, room.ws, ws_pyd)
|
| 142 |
|
lynxkite-core/src/lynxkite/core/executors/one_by_one.py
CHANGED
|
@@ -48,10 +48,10 @@ def register(env: str, cache: bool = True):
|
|
| 48 |
"""
|
| 49 |
if cache:
|
| 50 |
CACHES[env] = {}
|
| 51 |
-
|
| 52 |
else:
|
| 53 |
-
|
| 54 |
-
ops.EXECUTORS[env] = lambda ws: _execute(ws, ops.CATALOGS[env], cache=
|
| 55 |
|
| 56 |
|
| 57 |
def _get_stages(ws, catalog: ops.Catalog):
|
|
@@ -102,7 +102,9 @@ async def _await_if_needed(obj):
|
|
| 102 |
return obj
|
| 103 |
|
| 104 |
|
| 105 |
-
async def _execute(
|
|
|
|
|
|
|
| 106 |
nodes = {n.id: n for n in ws.nodes}
|
| 107 |
contexts = {n.id: Context(node=n) for n in ws.nodes}
|
| 108 |
edges = {n.id: [] for n in ws.nodes}
|
|
|
|
| 48 |
"""
|
| 49 |
if cache:
|
| 50 |
CACHES[env] = {}
|
| 51 |
+
_cache = CACHES[env]
|
| 52 |
else:
|
| 53 |
+
_cache = None
|
| 54 |
+
ops.EXECUTORS[env] = lambda ws: _execute(ws, ops.CATALOGS[env], cache=_cache)
|
| 55 |
|
| 56 |
|
| 57 |
def _get_stages(ws, catalog: ops.Catalog):
|
|
|
|
| 102 |
return obj
|
| 103 |
|
| 104 |
|
| 105 |
+
async def _execute(
|
| 106 |
+
ws: workspace.Workspace, catalog: ops.Catalog, cache: typing.Optional[dict] = None
|
| 107 |
+
):
|
| 108 |
nodes = {n.id: n for n in ws.nodes}
|
| 109 |
contexts = {n.id: Context(node=n) for n in ws.nodes}
|
| 110 |
edges = {n.id: [] for n in ws.nodes}
|
lynxkite-core/src/lynxkite/core/ops.py
CHANGED
|
@@ -6,7 +6,7 @@ import asyncio
|
|
| 6 |
import enum
|
| 7 |
import functools
|
| 8 |
import json
|
| 9 |
-
import importlib
|
| 10 |
import inspect
|
| 11 |
import pathlib
|
| 12 |
import subprocess
|
|
@@ -498,6 +498,7 @@ def install_requirements(req: pathlib.Path):
|
|
| 498 |
|
| 499 |
def run_user_script(script_path: pathlib.Path):
|
| 500 |
spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
|
|
|
|
| 501 |
module = importlib.util.module_from_spec(spec)
|
| 502 |
spec.loader.exec_module(module)
|
| 503 |
|
|
|
|
| 6 |
import enum
|
| 7 |
import functools
|
| 8 |
import json
|
| 9 |
+
import importlib.util
|
| 10 |
import inspect
|
| 11 |
import pathlib
|
| 12 |
import subprocess
|
|
|
|
| 498 |
|
| 499 |
def run_user_script(script_path: pathlib.Path):
|
| 500 |
spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
|
| 501 |
+
assert spec
|
| 502 |
module = importlib.util.module_from_spec(spec)
|
| 503 |
spec.loader.exec_module(module)
|
| 504 |
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""For working with LynxKite workspaces."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
-
from typing import Optional
|
| 5 |
import dataclasses
|
| 6 |
import enum
|
| 7 |
import os
|
|
@@ -10,6 +10,9 @@ import pydantic
|
|
| 10 |
import tempfile
|
| 11 |
from . import ops
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
class BaseConfig(pydantic.BaseModel):
|
| 15 |
model_config = pydantic.ConfigDict(
|
|
@@ -36,8 +39,7 @@ class WorkspaceNodeData(BaseConfig):
|
|
| 36 |
input_metadata: Optional[object] = None
|
| 37 |
error: Optional[str] = None
|
| 38 |
status: NodeStatus = NodeStatus.done
|
| 39 |
-
|
| 40 |
-
# This is ignored when coming back from the frontend.
|
| 41 |
|
| 42 |
@pydantic.model_validator(mode="before")
|
| 43 |
@classmethod
|
|
@@ -47,6 +49,13 @@ class WorkspaceNodeData(BaseConfig):
|
|
| 47 |
data["op_id"] = data["title"]
|
| 48 |
return data
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
class WorkspaceNode(BaseConfig):
|
| 52 |
# Most of these fields are shared with ReactFlow.
|
|
@@ -56,13 +65,13 @@ class WorkspaceNode(BaseConfig):
|
|
| 56 |
position: Position
|
| 57 |
width: Optional[float] = None
|
| 58 |
height: Optional[float] = None
|
| 59 |
-
_crdt: pycrdt.Map
|
| 60 |
|
| 61 |
def publish_started(self):
|
| 62 |
"""Notifies the frontend that work has started on this node."""
|
| 63 |
self.data.error = None
|
| 64 |
self.data.status = NodeStatus.active
|
| 65 |
-
if
|
| 66 |
with self._crdt.doc.transaction():
|
| 67 |
self._crdt["data"]["error"] = None
|
| 68 |
self._crdt["data"]["status"] = NodeStatus.active
|
|
@@ -73,7 +82,7 @@ class WorkspaceNode(BaseConfig):
|
|
| 73 |
self.data.input_metadata = result.input_metadata
|
| 74 |
self.data.error = result.error
|
| 75 |
self.data.status = NodeStatus.done
|
| 76 |
-
if
|
| 77 |
with self._crdt.doc.transaction():
|
| 78 |
try:
|
| 79 |
self._crdt["data"]["status"] = NodeStatus.done
|
|
@@ -109,7 +118,7 @@ class Workspace(BaseConfig):
|
|
| 109 |
env: str = ""
|
| 110 |
nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
|
| 111 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
| 112 |
-
_crdt: pycrdt.Map
|
| 113 |
|
| 114 |
def normalize(self):
|
| 115 |
if self.env not in ops.CATALOGS:
|
|
@@ -194,19 +203,19 @@ class Workspace(BaseConfig):
|
|
| 194 |
if getattr(data, "meta", None) != op:
|
| 195 |
data.meta = op
|
| 196 |
# If the node is connected to a CRDT, update that too.
|
| 197 |
-
if
|
| 198 |
node._crdt["data"]["meta"] = op.model_dump()
|
| 199 |
if node.type != op.type:
|
| 200 |
node.type = op.type
|
| 201 |
-
if
|
| 202 |
node._crdt["type"] = op.type
|
| 203 |
if data.error == "Unknown operation.":
|
| 204 |
data.error = None
|
| 205 |
-
if
|
| 206 |
node._crdt["data"]["error"] = None
|
| 207 |
else:
|
| 208 |
data.error = "Unknown operation."
|
| 209 |
-
if
|
| 210 |
node._crdt["data"]["meta"] = {}
|
| 211 |
node._crdt["data"]["error"] = "Unknown operation."
|
| 212 |
|
|
@@ -226,12 +235,16 @@ class Workspace(BaseConfig):
|
|
| 226 |
kwargs["data"] = WorkspaceNodeData(
|
| 227 |
title=func.__op__.name, op_id=func.__op__.id, params={}
|
| 228 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
kwargs.setdefault("type", "basic")
|
| 230 |
kwargs.setdefault("id", f"{kwargs['data'].title} {random_string}")
|
| 231 |
kwargs.setdefault("position", Position(x=0, y=0))
|
| 232 |
kwargs.setdefault("width", 100)
|
| 233 |
kwargs.setdefault("height", 100)
|
| 234 |
-
node = WorkspaceNode(**kwargs)
|
| 235 |
self.nodes.append(node)
|
| 236 |
return node
|
| 237 |
|
|
|
|
| 1 |
"""For working with LynxKite workspaces."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
from typing import Optional, TYPE_CHECKING
|
| 5 |
import dataclasses
|
| 6 |
import enum
|
| 7 |
import os
|
|
|
|
| 10 |
import tempfile
|
| 11 |
from . import ops
|
| 12 |
|
| 13 |
+
if TYPE_CHECKING:
|
| 14 |
+
from lynxkite.core import ops
|
| 15 |
+
|
| 16 |
|
| 17 |
class BaseConfig(pydantic.BaseModel):
|
| 18 |
model_config = pydantic.ConfigDict(
|
|
|
|
| 39 |
input_metadata: Optional[object] = None
|
| 40 |
error: Optional[str] = None
|
| 41 |
status: NodeStatus = NodeStatus.done
|
| 42 |
+
meta: Optional["ops.Op"] = None
|
|
|
|
| 43 |
|
| 44 |
@pydantic.model_validator(mode="before")
|
| 45 |
@classmethod
|
|
|
|
| 49 |
data["op_id"] = data["title"]
|
| 50 |
return data
|
| 51 |
|
| 52 |
+
@pydantic.model_validator(mode="before")
|
| 53 |
+
@classmethod
|
| 54 |
+
def ignore_meta(cls, data: dict) -> dict:
|
| 55 |
+
"""Metadata is never loaded. We will use fresh metadata."""
|
| 56 |
+
data["meta"] = None
|
| 57 |
+
return data
|
| 58 |
+
|
| 59 |
|
| 60 |
class WorkspaceNode(BaseConfig):
|
| 61 |
# Most of these fields are shared with ReactFlow.
|
|
|
|
| 65 |
position: Position
|
| 66 |
width: Optional[float] = None
|
| 67 |
height: Optional[float] = None
|
| 68 |
+
_crdt: Optional[pycrdt.Map] = None
|
| 69 |
|
| 70 |
def publish_started(self):
|
| 71 |
"""Notifies the frontend that work has started on this node."""
|
| 72 |
self.data.error = None
|
| 73 |
self.data.status = NodeStatus.active
|
| 74 |
+
if self._crdt and "data" in self._crdt:
|
| 75 |
with self._crdt.doc.transaction():
|
| 76 |
self._crdt["data"]["error"] = None
|
| 77 |
self._crdt["data"]["status"] = NodeStatus.active
|
|
|
|
| 82 |
self.data.input_metadata = result.input_metadata
|
| 83 |
self.data.error = result.error
|
| 84 |
self.data.status = NodeStatus.done
|
| 85 |
+
if self._crdt and "data" in self._crdt:
|
| 86 |
with self._crdt.doc.transaction():
|
| 87 |
try:
|
| 88 |
self._crdt["data"]["status"] = NodeStatus.done
|
|
|
|
| 118 |
env: str = ""
|
| 119 |
nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
|
| 120 |
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
| 121 |
+
_crdt: Optional[pycrdt.Map] = None
|
| 122 |
|
| 123 |
def normalize(self):
|
| 124 |
if self.env not in ops.CATALOGS:
|
|
|
|
| 203 |
if getattr(data, "meta", None) != op:
|
| 204 |
data.meta = op
|
| 205 |
# If the node is connected to a CRDT, update that too.
|
| 206 |
+
if node._crdt:
|
| 207 |
node._crdt["data"]["meta"] = op.model_dump()
|
| 208 |
if node.type != op.type:
|
| 209 |
node.type = op.type
|
| 210 |
+
if node._crdt:
|
| 211 |
node._crdt["type"] = op.type
|
| 212 |
if data.error == "Unknown operation.":
|
| 213 |
data.error = None
|
| 214 |
+
if node._crdt:
|
| 215 |
node._crdt["data"]["error"] = None
|
| 216 |
else:
|
| 217 |
data.error = "Unknown operation."
|
| 218 |
+
if node._crdt:
|
| 219 |
node._crdt["data"]["meta"] = {}
|
| 220 |
node._crdt["data"]["error"] = "Unknown operation."
|
| 221 |
|
|
|
|
| 235 |
kwargs["data"] = WorkspaceNodeData(
|
| 236 |
title=func.__op__.name, op_id=func.__op__.id, params={}
|
| 237 |
)
|
| 238 |
+
elif "title" in kwargs:
|
| 239 |
+
kwargs["data"] = WorkspaceNodeData(
|
| 240 |
+
title=kwargs["title"], op_id=kwargs["title"], params=kwargs.get("params", {})
|
| 241 |
+
)
|
| 242 |
kwargs.setdefault("type", "basic")
|
| 243 |
kwargs.setdefault("id", f"{kwargs['data'].title} {random_string}")
|
| 244 |
kwargs.setdefault("position", Position(x=0, y=0))
|
| 245 |
kwargs.setdefault("width", 100)
|
| 246 |
kwargs.setdefault("height", 100)
|
| 247 |
+
node = WorkspaceNode(**kwargs) # ty: ignore[missing-argument]
|
| 248 |
self.nodes.append(node)
|
| 249 |
return node
|
| 250 |
|
lynxkite-core/tests/test_one_by_one.py
CHANGED
|
@@ -13,8 +13,8 @@ async def test_optional_inputs():
|
|
| 13 |
return [a + b for a, b in zip(a, b)] if b else a
|
| 14 |
|
| 15 |
assert maybe_add.__op__.inputs == [
|
| 16 |
-
ops.Input(name="a", type=list[int], position=
|
| 17 |
-
ops.Input(name="b", type=list[int] | None, position=
|
| 18 |
]
|
| 19 |
one_by_one.register("test")
|
| 20 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
|
|
|
| 13 |
return [a + b for a, b in zip(a, b)] if b else a
|
| 14 |
|
| 15 |
assert maybe_add.__op__.inputs == [
|
| 16 |
+
ops.Input(name="a", type=list[int], position=ops.Position.BOTTOM),
|
| 17 |
+
ops.Input(name="b", type=list[int] | None, position=ops.Position.BOTTOM),
|
| 18 |
]
|
| 19 |
one_by_one.register("test")
|
| 20 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
lynxkite-core/tests/test_ops.py
CHANGED
|
@@ -11,10 +11,10 @@ def test_op_decorator_no_params_no_types_default_positions():
|
|
| 11 |
assert add.__op__.name == "add"
|
| 12 |
assert add.__op__.params == []
|
| 13 |
assert add.__op__.inputs == [
|
| 14 |
-
ops.Input(name="a", type=inspect._empty, position=
|
| 15 |
-
ops.Input(name="b", type=inspect._empty, position=
|
| 16 |
]
|
| 17 |
-
assert add.__op__.outputs == [ops.Output(name="result", type=None, position=
|
| 18 |
assert add.__op__.type == "basic"
|
| 19 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
| 20 |
|
|
@@ -29,10 +29,12 @@ def test_op_decorator_custom_positions():
|
|
| 29 |
assert add.__op__.name == "add"
|
| 30 |
assert add.__op__.params == []
|
| 31 |
assert add.__op__.inputs == [
|
| 32 |
-
ops.Input(name="a", type=inspect._empty, position=
|
| 33 |
-
ops.Input(name="b", type=inspect._empty, position=
|
|
|
|
|
|
|
|
|
|
| 34 |
]
|
| 35 |
-
assert add.__op__.outputs == [ops.Output(name="result", type=None, position="bottom")]
|
| 36 |
assert add.__op__.type == "basic"
|
| 37 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
| 38 |
|
|
@@ -45,10 +47,12 @@ def test_op_decorator_with_params_and_types_():
|
|
| 45 |
assert multiply.__op__.name == "multiply"
|
| 46 |
assert multiply.__op__.params == [ops.Parameter(name="param", default="param", type=str)]
|
| 47 |
assert multiply.__op__.inputs == [
|
| 48 |
-
ops.Input(name="a", type=int, position=
|
| 49 |
-
ops.Input(name="b", type=float, position=
|
|
|
|
|
|
|
|
|
|
| 50 |
]
|
| 51 |
-
assert multiply.__op__.outputs == [ops.Output(name="result", type=None, position="right")]
|
| 52 |
assert multiply.__op__.type == "basic"
|
| 53 |
assert ops.CATALOGS["test"]["multiply"] == multiply.__op__
|
| 54 |
|
|
@@ -66,12 +70,14 @@ def test_op_decorator_with_complex_types():
|
|
| 66 |
assert complex_op.__op__.name == "color_op"
|
| 67 |
assert complex_op.__op__.params == []
|
| 68 |
assert complex_op.__op__.inputs == [
|
| 69 |
-
ops.Input(name="color", type=Color, position=
|
| 70 |
-
ops.Input(name="color_list", type=list[Color], position=
|
| 71 |
-
ops.Input(name="color_dict", type=dict[str, Color], position=
|
| 72 |
]
|
| 73 |
assert complex_op.__op__.type == "basic"
|
| 74 |
-
assert complex_op.__op__.outputs == [
|
|
|
|
|
|
|
| 75 |
assert ops.CATALOGS["test"]["color_op"] == complex_op.__op__
|
| 76 |
|
| 77 |
|
|
|
|
| 11 |
assert add.__op__.name == "add"
|
| 12 |
assert add.__op__.params == []
|
| 13 |
assert add.__op__.inputs == [
|
| 14 |
+
ops.Input(name="a", type=inspect._empty, position=ops.Position.LEFT),
|
| 15 |
+
ops.Input(name="b", type=inspect._empty, position=ops.Position.LEFT),
|
| 16 |
]
|
| 17 |
+
assert add.__op__.outputs == [ops.Output(name="result", type=None, position=ops.Position.RIGHT)]
|
| 18 |
assert add.__op__.type == "basic"
|
| 19 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
| 20 |
|
|
|
|
| 29 |
assert add.__op__.name == "add"
|
| 30 |
assert add.__op__.params == []
|
| 31 |
assert add.__op__.inputs == [
|
| 32 |
+
ops.Input(name="a", type=inspect._empty, position=ops.Position.RIGHT),
|
| 33 |
+
ops.Input(name="b", type=inspect._empty, position=ops.Position.TOP),
|
| 34 |
+
]
|
| 35 |
+
assert add.__op__.outputs == [
|
| 36 |
+
ops.Output(name="result", type=None, position=ops.Position.BOTTOM)
|
| 37 |
]
|
|
|
|
| 38 |
assert add.__op__.type == "basic"
|
| 39 |
assert ops.CATALOGS["test"]["add"] == add.__op__
|
| 40 |
|
|
|
|
| 47 |
assert multiply.__op__.name == "multiply"
|
| 48 |
assert multiply.__op__.params == [ops.Parameter(name="param", default="param", type=str)]
|
| 49 |
assert multiply.__op__.inputs == [
|
| 50 |
+
ops.Input(name="a", type=int, position=ops.Position.LEFT),
|
| 51 |
+
ops.Input(name="b", type=float, position=ops.Position.LEFT),
|
| 52 |
+
]
|
| 53 |
+
assert multiply.__op__.outputs == [
|
| 54 |
+
ops.Output(name="result", type=None, position=ops.Position.RIGHT)
|
| 55 |
]
|
|
|
|
| 56 |
assert multiply.__op__.type == "basic"
|
| 57 |
assert ops.CATALOGS["test"]["multiply"] == multiply.__op__
|
| 58 |
|
|
|
|
| 70 |
assert complex_op.__op__.name == "color_op"
|
| 71 |
assert complex_op.__op__.params == []
|
| 72 |
assert complex_op.__op__.inputs == [
|
| 73 |
+
ops.Input(name="color", type=Color, position=ops.Position.LEFT),
|
| 74 |
+
ops.Input(name="color_list", type=list[Color], position=ops.Position.LEFT),
|
| 75 |
+
ops.Input(name="color_dict", type=dict[str, Color], position=ops.Position.LEFT),
|
| 76 |
]
|
| 77 |
assert complex_op.__op__.type == "basic"
|
| 78 |
+
assert complex_op.__op__.outputs == [
|
| 79 |
+
ops.Output(name="result", type=None, position=ops.Position.RIGHT)
|
| 80 |
+
]
|
| 81 |
assert ops.CATALOGS["test"]["color_op"] == complex_op.__op__
|
| 82 |
|
| 83 |
|
lynxkite-core/tests/test_simple.py
CHANGED
|
@@ -12,8 +12,8 @@ async def test_optional_inputs():
|
|
| 12 |
return a + (b or 0)
|
| 13 |
|
| 14 |
assert maybe_add.__op__.inputs == [
|
| 15 |
-
ops.Input(name="a", type=int, position=
|
| 16 |
-
ops.Input(name="b", type=int | None, position=
|
| 17 |
]
|
| 18 |
simple.register("test")
|
| 19 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
|
|
|
| 12 |
return a + (b or 0)
|
| 13 |
|
| 14 |
assert maybe_add.__op__.inputs == [
|
| 15 |
+
ops.Input(name="a", type=int, position=ops.Position.LEFT),
|
| 16 |
+
ops.Input(name="b", type=int | None, position=ops.Position.LEFT),
|
| 17 |
]
|
| 18 |
simple.register("test")
|
| 19 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
lynxkite-core/tests/test_workspace.py
CHANGED
|
@@ -9,12 +9,12 @@ def test_save_load():
|
|
| 9 |
ws.add_node(
|
| 10 |
id="1",
|
| 11 |
type="node_type",
|
| 12 |
-
|
| 13 |
)
|
| 14 |
ws.add_node(
|
| 15 |
id="2",
|
| 16 |
type="node_type",
|
| 17 |
-
|
| 18 |
)
|
| 19 |
ws.edges.append(
|
| 20 |
workspace.WorkspaceEdge(
|
|
@@ -70,17 +70,18 @@ def test_update_metadata():
|
|
| 70 |
ws.add_node(
|
| 71 |
id="1",
|
| 72 |
type="basic",
|
| 73 |
-
|
| 74 |
)
|
| 75 |
ws.add_node(
|
| 76 |
id="2",
|
| 77 |
type="basic",
|
| 78 |
-
|
| 79 |
)
|
| 80 |
ws.update_metadata()
|
|
|
|
| 81 |
assert ws.nodes[0].data.meta.name == "Test Operation"
|
| 82 |
assert ws.nodes[0].data.error is None
|
| 83 |
-
assert
|
| 84 |
assert ws.nodes[1].data.error == "Unknown operation."
|
| 85 |
|
| 86 |
|
|
|
|
| 9 |
ws.add_node(
|
| 10 |
id="1",
|
| 11 |
type="node_type",
|
| 12 |
+
title="Node 1",
|
| 13 |
)
|
| 14 |
ws.add_node(
|
| 15 |
id="2",
|
| 16 |
type="node_type",
|
| 17 |
+
title="Node 2",
|
| 18 |
)
|
| 19 |
ws.edges.append(
|
| 20 |
workspace.WorkspaceEdge(
|
|
|
|
| 70 |
ws.add_node(
|
| 71 |
id="1",
|
| 72 |
type="basic",
|
| 73 |
+
title="Test Operation",
|
| 74 |
)
|
| 75 |
ws.add_node(
|
| 76 |
id="2",
|
| 77 |
type="basic",
|
| 78 |
+
title="Unknown Operation",
|
| 79 |
)
|
| 80 |
ws.update_metadata()
|
| 81 |
+
assert ws.nodes[0].data.meta
|
| 82 |
assert ws.nodes[0].data.meta.name == "Test Operation"
|
| 83 |
assert ws.nodes[0].data.error is None
|
| 84 |
+
assert ws.nodes[1].data.meta is None
|
| 85 |
assert ws.nodes[1].data.error == "Unknown operation."
|
| 86 |
|
| 87 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
| 4 |
import pandas as pd
|
| 5 |
|
| 6 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
| 7 |
-
import cudf.pandas
|
| 8 |
|
| 9 |
cudf.pandas.install()
|
| 10 |
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
|
| 6 |
if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
|
| 7 |
+
import cudf.pandas # ty: ignore[unresolved-import]
|
| 8 |
|
| 9 |
cudf.pandas.install()
|
| 10 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/core.py
CHANGED
|
@@ -227,13 +227,13 @@ def disambiguate_edges(ws: workspace.Workspace):
|
|
| 227 |
for edge in reversed(ws.edges):
|
| 228 |
dst_node = nodes[edge.target]
|
| 229 |
op = catalog.get(dst_node.data.op_id)
|
| 230 |
-
if op.get_input(edge.targetHandle).type == list[Bundle]:
|
| 231 |
# Takes multiple bundles as an input. No need to disambiguate.
|
| 232 |
continue
|
| 233 |
if (edge.target, edge.targetHandle) in seen:
|
| 234 |
i = ws.edges.index(edge)
|
| 235 |
del ws.edges[i]
|
| 236 |
-
if
|
| 237 |
del ws._crdt["edges"][i]
|
| 238 |
seen.add((edge.target, edge.targetHandle))
|
| 239 |
|
|
|
|
| 227 |
for edge in reversed(ws.edges):
|
| 228 |
dst_node = nodes[edge.target]
|
| 229 |
op = catalog.get(dst_node.data.op_id)
|
| 230 |
+
if not op or op.get_input(edge.targetHandle).type == list[Bundle]:
|
| 231 |
# Takes multiple bundles as an input. No need to disambiguate.
|
| 232 |
continue
|
| 233 |
if (edge.target, edge.targetHandle) in seen:
|
| 234 |
i = ws.edges.index(edge)
|
| 235 |
del ws.edges[i]
|
| 236 |
+
if ws._crdt:
|
| 237 |
del ws._crdt["edges"][i]
|
| 238 |
seen.add((edge.target, edge.targetHandle))
|
| 239 |
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py
CHANGED
|
@@ -8,7 +8,8 @@ from collections import deque
|
|
| 8 |
|
| 9 |
from . import core
|
| 10 |
import grandcypher
|
| 11 |
-
import matplotlib
|
|
|
|
| 12 |
import networkx as nx
|
| 13 |
import pandas as pd
|
| 14 |
import polars as pl
|
|
@@ -205,6 +206,7 @@ def _map_color(value):
|
|
| 205 |
else:
|
| 206 |
cmap = matplotlib.cm.get_cmap("Paired")
|
| 207 |
categories = pd.Index(value.unique())
|
|
|
|
| 208 |
colors = cmap.colors[: len(categories)]
|
| 209 |
return [
|
| 210 |
"#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
|
@@ -312,7 +314,7 @@ def view_tables(bundle: core.Bundle, *, _tables_open: str = "", limit: int = 100
|
|
| 312 |
view="graph_creation_view",
|
| 313 |
outputs=["output"],
|
| 314 |
)
|
| 315 |
-
def organize(bundles: list[core.Bundle], *, relations: str =
|
| 316 |
"""Merge multiple inputs and construct graphs from the tables.
|
| 317 |
|
| 318 |
To create a graph, import tables for edges and nodes, and combine them in this operation.
|
|
@@ -322,6 +324,9 @@ def organize(bundles: list[core.Bundle], *, relations: str = None) -> core.Bundl
|
|
| 322 |
bundle.dfs.update(b.dfs)
|
| 323 |
bundle.relations.extend(b.relations)
|
| 324 |
bundle.other.update(b.other)
|
| 325 |
-
if
|
| 326 |
-
bundle.relations = [
|
|
|
|
|
|
|
|
|
|
| 327 |
return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
|
|
|
|
| 8 |
|
| 9 |
from . import core
|
| 10 |
import grandcypher
|
| 11 |
+
import matplotlib.cm
|
| 12 |
+
import matplotlib.colors
|
| 13 |
import networkx as nx
|
| 14 |
import pandas as pd
|
| 15 |
import polars as pl
|
|
|
|
| 206 |
else:
|
| 207 |
cmap = matplotlib.cm.get_cmap("Paired")
|
| 208 |
categories = pd.Index(value.unique())
|
| 209 |
+
assert isinstance(cmap, matplotlib.colors.ListedColormap)
|
| 210 |
colors = cmap.colors[: len(categories)]
|
| 211 |
return [
|
| 212 |
"#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
|
|
|
| 314 |
view="graph_creation_view",
|
| 315 |
outputs=["output"],
|
| 316 |
)
|
| 317 |
+
def organize(bundles: list[core.Bundle], *, relations: str = ""):
|
| 318 |
"""Merge multiple inputs and construct graphs from the tables.
|
| 319 |
|
| 320 |
To create a graph, import tables for edges and nodes, and combine them in this operation.
|
|
|
|
| 324 |
bundle.dfs.update(b.dfs)
|
| 325 |
bundle.relations.extend(b.relations)
|
| 326 |
bundle.other.update(b.other)
|
| 327 |
+
if relations.strip():
|
| 328 |
+
bundle.relations = [
|
| 329 |
+
core.RelationDefinition(**r) # ty: ignore[missing-argument]
|
| 330 |
+
for r in json.loads(relations).values()
|
| 331 |
+
]
|
| 332 |
return ops.Result(output=bundle, display=bundle.to_dict(limit=100))
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/ml_ops.py
CHANGED
|
@@ -216,7 +216,7 @@ def view_vectors(
|
|
| 216 |
metric: UMAPMetric = UMAPMetric.euclidean,
|
| 217 |
):
|
| 218 |
try:
|
| 219 |
-
from cuml.manifold.umap import UMAP
|
| 220 |
except ImportError:
|
| 221 |
from umap import UMAP
|
| 222 |
vec = np.stack(bundle.dfs[table_name][vector_column].to_numpy())
|
|
|
|
| 216 |
metric: UMAPMetric = UMAPMetric.euclidean,
|
| 217 |
):
|
| 218 |
try:
|
| 219 |
+
from cuml.manifold.umap import UMAP # ty: ignore[unresolved-import]
|
| 220 |
except ImportError:
|
| 221 |
from umap import UMAP
|
| 222 |
vec = np.stack(bundle.dfs[table_name][vector_column].to_numpy())
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/networkx_ops.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
"""Automatically wraps all NetworkX functions as LynxKite operations."""
|
| 2 |
|
| 3 |
-
import collections
|
| 4 |
-
import types
|
| 5 |
from lynxkite.core import ops
|
|
|
|
|
|
|
| 6 |
import functools
|
| 7 |
import inspect
|
| 8 |
import networkx as nx
|
| 9 |
-
import re
|
| 10 |
-
|
| 11 |
import pandas as pd
|
|
|
|
|
|
|
| 12 |
|
| 13 |
ENV = "LynxKite Graph Analytics"
|
| 14 |
|
|
@@ -17,20 +17,22 @@ class UnsupportedParameterType(Exception):
|
|
| 17 |
pass
|
| 18 |
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
-
def doc_to_type(name: str, type_hint: str) -> type:
|
| 25 |
type_hint = type_hint.lower()
|
| 26 |
type_hint = re.sub("[(][^)]+[)]", "", type_hint).strip().strip(".")
|
| 27 |
if " " in name or "http" in name:
|
| 28 |
-
return
|
| 29 |
if type_hint.endswith(", optional"):
|
| 30 |
w = doc_to_type(name, type_hint.removesuffix(", optional").strip())
|
| 31 |
-
if w is
|
| 32 |
-
return
|
| 33 |
-
|
|
|
|
| 34 |
if type_hint in [
|
| 35 |
"a digraph or multidigraph",
|
| 36 |
"a graph g",
|
|
@@ -54,15 +56,15 @@ def doc_to_type(name: str, type_hint: str) -> type:
|
|
| 54 |
]:
|
| 55 |
return nx.DiGraph
|
| 56 |
elif type_hint == "node":
|
| 57 |
-
return
|
| 58 |
elif type_hint == '"node (optional)"':
|
| 59 |
-
return
|
| 60 |
elif type_hint == '"edge"':
|
| 61 |
-
return
|
| 62 |
elif type_hint == '"edge (optional)"':
|
| 63 |
-
return
|
| 64 |
elif type_hint in ["class", "data type"]:
|
| 65 |
-
return
|
| 66 |
elif type_hint in ["string", "str", "node label"]:
|
| 67 |
return str
|
| 68 |
elif type_hint in ["string or none", "none or string", "string, or none"]:
|
|
@@ -72,27 +74,27 @@ def doc_to_type(name: str, type_hint: str) -> type:
|
|
| 72 |
elif type_hint in ["bool", "boolean"]:
|
| 73 |
return bool
|
| 74 |
elif type_hint == "tuple":
|
| 75 |
-
return
|
| 76 |
elif type_hint == "set":
|
| 77 |
-
return
|
| 78 |
elif type_hint == "list of floats":
|
| 79 |
-
return
|
| 80 |
elif type_hint == "list of floats or float":
|
| 81 |
return float
|
| 82 |
elif type_hint in ["dict", "dictionary"]:
|
| 83 |
-
return
|
| 84 |
elif type_hint == "scalar or dictionary":
|
| 85 |
return float
|
| 86 |
elif type_hint == "none or dict":
|
| 87 |
-
return
|
| 88 |
elif type_hint in ["function", "callable"]:
|
| 89 |
-
return
|
| 90 |
elif type_hint in [
|
| 91 |
"collection",
|
| 92 |
"container of nodes",
|
| 93 |
"list of nodes",
|
| 94 |
]:
|
| 95 |
-
return
|
| 96 |
elif type_hint in [
|
| 97 |
"container",
|
| 98 |
"generator",
|
|
@@ -104,13 +106,13 @@ def doc_to_type(name: str, type_hint: str) -> type:
|
|
| 104 |
"list or tuple",
|
| 105 |
"list",
|
| 106 |
]:
|
| 107 |
-
return
|
| 108 |
elif type_hint == "generator of sets":
|
| 109 |
-
return
|
| 110 |
elif type_hint == "dict or a set of 2 or 3 tuples":
|
| 111 |
-
return
|
| 112 |
elif type_hint == "set of 2 or 3 tuples":
|
| 113 |
-
return
|
| 114 |
elif type_hint == "none, string or function":
|
| 115 |
return str | None
|
| 116 |
elif type_hint == "string or function" and name == "weight":
|
|
@@ -135,8 +137,8 @@ def doc_to_type(name: str, type_hint: str) -> type:
|
|
| 135 |
elif name == "weight":
|
| 136 |
return str
|
| 137 |
elif type_hint == "object":
|
| 138 |
-
return
|
| 139 |
-
return
|
| 140 |
|
| 141 |
|
| 142 |
def types_from_doc(doc: str) -> dict[str, type]:
|
|
@@ -186,13 +188,13 @@ def wrapped(name: str, func):
|
|
| 186 |
return wrapper
|
| 187 |
|
| 188 |
|
| 189 |
-
def _get_params(func) ->
|
| 190 |
sig = inspect.signature(func)
|
| 191 |
# Get types from docstring.
|
| 192 |
types = types_from_doc(func.__doc__)
|
| 193 |
# Always hide these.
|
| 194 |
for k in ["backend", "backend_kwargs", "create_using"]:
|
| 195 |
-
types[k] =
|
| 196 |
# Add in types based on signature.
|
| 197 |
for k, param in sig.parameters.items():
|
| 198 |
if k in types:
|
|
@@ -203,10 +205,10 @@ def _get_params(func) -> dict | None:
|
|
| 203 |
types[k] = int
|
| 204 |
params = []
|
| 205 |
for name, param in sig.parameters.items():
|
| 206 |
-
_type = types.get(name,
|
| 207 |
-
if _type is
|
| 208 |
raise UnsupportedParameterType(name)
|
| 209 |
-
if _type is
|
| 210 |
continue
|
| 211 |
p = ops.Parameter.basic(
|
| 212 |
name=name,
|
|
|
|
| 1 |
"""Automatically wraps all NetworkX functions as LynxKite operations."""
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from lynxkite.core import ops
|
| 4 |
+
import collections.abc
|
| 5 |
+
import enum
|
| 6 |
import functools
|
| 7 |
import inspect
|
| 8 |
import networkx as nx
|
|
|
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
+
import re
|
| 11 |
+
import types
|
| 12 |
|
| 13 |
ENV = "LynxKite Graph Analytics"
|
| 14 |
|
|
|
|
| 17 |
pass
|
| 18 |
|
| 19 |
|
| 20 |
+
class Failure(str, enum.Enum):
|
| 21 |
+
UNSUPPORTED = "unsupported" # This parameter will be hidden.
|
| 22 |
+
SKIP = "skip" # We have to skip the whole function.
|
| 23 |
|
| 24 |
|
| 25 |
+
def doc_to_type(name: str, type_hint: str) -> type | types.UnionType | Failure:
|
| 26 |
type_hint = type_hint.lower()
|
| 27 |
type_hint = re.sub("[(][^)]+[)]", "", type_hint).strip().strip(".")
|
| 28 |
if " " in name or "http" in name:
|
| 29 |
+
return Failure.UNSUPPORTED # Not a parameter type.
|
| 30 |
if type_hint.endswith(", optional"):
|
| 31 |
w = doc_to_type(name, type_hint.removesuffix(", optional").strip())
|
| 32 |
+
if w is Failure.UNSUPPORTED or w is Failure.SKIP:
|
| 33 |
+
return Failure.SKIP
|
| 34 |
+
assert not isinstance(w, Failure)
|
| 35 |
+
return w | None
|
| 36 |
if type_hint in [
|
| 37 |
"a digraph or multidigraph",
|
| 38 |
"a graph g",
|
|
|
|
| 56 |
]:
|
| 57 |
return nx.DiGraph
|
| 58 |
elif type_hint == "node":
|
| 59 |
+
return Failure.UNSUPPORTED
|
| 60 |
elif type_hint == '"node (optional)"':
|
| 61 |
+
return Failure.SKIP
|
| 62 |
elif type_hint == '"edge"':
|
| 63 |
+
return Failure.UNSUPPORTED
|
| 64 |
elif type_hint == '"edge (optional)"':
|
| 65 |
+
return Failure.SKIP
|
| 66 |
elif type_hint in ["class", "data type"]:
|
| 67 |
+
return Failure.UNSUPPORTED
|
| 68 |
elif type_hint in ["string", "str", "node label"]:
|
| 69 |
return str
|
| 70 |
elif type_hint in ["string or none", "none or string", "string, or none"]:
|
|
|
|
| 74 |
elif type_hint in ["bool", "boolean"]:
|
| 75 |
return bool
|
| 76 |
elif type_hint == "tuple":
|
| 77 |
+
return Failure.UNSUPPORTED
|
| 78 |
elif type_hint == "set":
|
| 79 |
+
return Failure.UNSUPPORTED
|
| 80 |
elif type_hint == "list of floats":
|
| 81 |
+
return Failure.UNSUPPORTED
|
| 82 |
elif type_hint == "list of floats or float":
|
| 83 |
return float
|
| 84 |
elif type_hint in ["dict", "dictionary"]:
|
| 85 |
+
return Failure.UNSUPPORTED
|
| 86 |
elif type_hint == "scalar or dictionary":
|
| 87 |
return float
|
| 88 |
elif type_hint == "none or dict":
|
| 89 |
+
return Failure.SKIP
|
| 90 |
elif type_hint in ["function", "callable"]:
|
| 91 |
+
return Failure.UNSUPPORTED
|
| 92 |
elif type_hint in [
|
| 93 |
"collection",
|
| 94 |
"container of nodes",
|
| 95 |
"list of nodes",
|
| 96 |
]:
|
| 97 |
+
return Failure.UNSUPPORTED
|
| 98 |
elif type_hint in [
|
| 99 |
"container",
|
| 100 |
"generator",
|
|
|
|
| 106 |
"list or tuple",
|
| 107 |
"list",
|
| 108 |
]:
|
| 109 |
+
return Failure.UNSUPPORTED
|
| 110 |
elif type_hint == "generator of sets":
|
| 111 |
+
return Failure.UNSUPPORTED
|
| 112 |
elif type_hint == "dict or a set of 2 or 3 tuples":
|
| 113 |
+
return Failure.UNSUPPORTED
|
| 114 |
elif type_hint == "set of 2 or 3 tuples":
|
| 115 |
+
return Failure.UNSUPPORTED
|
| 116 |
elif type_hint == "none, string or function":
|
| 117 |
return str | None
|
| 118 |
elif type_hint == "string or function" and name == "weight":
|
|
|
|
| 137 |
elif name == "weight":
|
| 138 |
return str
|
| 139 |
elif type_hint == "object":
|
| 140 |
+
return Failure.UNSUPPORTED
|
| 141 |
+
return Failure.SKIP
|
| 142 |
|
| 143 |
|
| 144 |
def types_from_doc(doc: str) -> dict[str, type]:
|
|
|
|
| 188 |
return wrapper
|
| 189 |
|
| 190 |
|
| 191 |
+
def _get_params(func) -> list[ops.Parameter | ops.ParameterGroup]:
|
| 192 |
sig = inspect.signature(func)
|
| 193 |
# Get types from docstring.
|
| 194 |
types = types_from_doc(func.__doc__)
|
| 195 |
# Always hide these.
|
| 196 |
for k in ["backend", "backend_kwargs", "create_using"]:
|
| 197 |
+
types[k] = Failure.SKIP
|
| 198 |
# Add in types based on signature.
|
| 199 |
for k, param in sig.parameters.items():
|
| 200 |
if k in types:
|
|
|
|
| 205 |
types[k] = int
|
| 206 |
params = []
|
| 207 |
for name, param in sig.parameters.items():
|
| 208 |
+
_type = types.get(name, Failure.UNSUPPORTED)
|
| 209 |
+
if _type is Failure.UNSUPPORTED:
|
| 210 |
raise UnsupportedParameterType(name)
|
| 211 |
+
if _type is Failure.SKIP or _type in [nx.Graph, nx.DiGraph]:
|
| 212 |
continue
|
| 213 |
p = ops.Parameter.basic(
|
| 214 |
name=name,
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch/pytorch_core.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import copy
|
| 4 |
import graphlib
|
| 5 |
import io
|
| 6 |
-
import
|
| 7 |
import pydantic
|
| 8 |
from lynxkite.core import ops, workspace
|
| 9 |
import torch
|
|
@@ -36,8 +36,12 @@ def reg(name, inputs=[], outputs=None, params=[], **kwargs):
|
|
| 36 |
return ops.register_passive_op(
|
| 37 |
ENV,
|
| 38 |
name,
|
| 39 |
-
inputs=[
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
params=params,
|
| 42 |
**kwargs,
|
| 43 |
)
|
|
@@ -75,16 +79,16 @@ class ModelMapping(pydantic.BaseModel):
|
|
| 75 |
map: dict[str, ColumnSpec]
|
| 76 |
|
| 77 |
|
| 78 |
-
def _torch_save(data) ->
|
| 79 |
"""Saves PyTorch data (modules, tensors) as a string."""
|
| 80 |
buffer = io.BytesIO()
|
| 81 |
torch.save(data, buffer)
|
| 82 |
return buffer.getvalue()
|
| 83 |
|
| 84 |
|
| 85 |
-
def _torch_load(data:
|
| 86 |
"""Loads PyTorch data (modules, tensors) from a string."""
|
| 87 |
-
buffer = io.BytesIO(data)
|
| 88 |
return torch.load(buffer)
|
| 89 |
|
| 90 |
|
|
@@ -98,7 +102,7 @@ class ModelConfig:
|
|
| 98 |
input_output_names: list[str]
|
| 99 |
loss: torch.nn.Module
|
| 100 |
source_workspace_json: str
|
| 101 |
-
optimizer_parameters: dict[str,
|
| 102 |
optimizer: torch.optim.Optimizer | None = None
|
| 103 |
source_workspace: str | None = None
|
| 104 |
trained: bool = False
|
|
@@ -116,7 +120,7 @@ class ModelConfig:
|
|
| 116 |
values = {k: v for k, v in zip(self.model_outputs, output)}
|
| 117 |
return values
|
| 118 |
|
| 119 |
-
def inference(self, inputs: dict[str, torch.Tensor]) -> dict[str,
|
| 120 |
"""Inference on a single batch."""
|
| 121 |
self.model.eval()
|
| 122 |
return self._forward(inputs)
|
|
@@ -422,7 +426,7 @@ class ModelBuilder:
|
|
| 422 |
op = self.catalog["Optimizer"]
|
| 423 |
cfg["optimizer_parameters"] = op.convert_params(self.nodes[self.optimizer].data.params)
|
| 424 |
cfg["source_workspace_json"] = self.ws.model_dump_json()
|
| 425 |
-
return ModelConfig(**cfg)
|
| 426 |
|
| 427 |
def get_names(self, *ids: list[str]) -> dict[str, str]:
|
| 428 |
"""Returns a mapping from internal IDs to human-readable names."""
|
|
@@ -448,7 +452,7 @@ class ModelBuilder:
|
|
| 448 |
|
| 449 |
|
| 450 |
def to_batch_tensors(
|
| 451 |
-
b: core.Bundle, batch_size: int, batch_index: int, m: ModelMapping
|
| 452 |
) -> dict[str, torch.Tensor]:
|
| 453 |
"""Extracts tensors from a bundle for a specific batch using a model mapping."""
|
| 454 |
tensors = {}
|
|
|
|
| 3 |
import copy
|
| 4 |
import graphlib
|
| 5 |
import io
|
| 6 |
+
import typing
|
| 7 |
import pydantic
|
| 8 |
from lynxkite.core import ops, workspace
|
| 9 |
import torch
|
|
|
|
| 36 |
return ops.register_passive_op(
|
| 37 |
ENV,
|
| 38 |
name,
|
| 39 |
+
inputs=[
|
| 40 |
+
ops.Input(name=name, position=ops.Position.BOTTOM, type="tensor") for name in inputs
|
| 41 |
+
],
|
| 42 |
+
outputs=[
|
| 43 |
+
ops.Output(name=name, position=ops.Position.TOP, type="tensor") for name in outputs
|
| 44 |
+
],
|
| 45 |
params=params,
|
| 46 |
**kwargs,
|
| 47 |
)
|
|
|
|
| 79 |
map: dict[str, ColumnSpec]
|
| 80 |
|
| 81 |
|
| 82 |
+
def _torch_save(data) -> bytes:
|
| 83 |
"""Saves PyTorch data (modules, tensors) as a string."""
|
| 84 |
buffer = io.BytesIO()
|
| 85 |
torch.save(data, buffer)
|
| 86 |
return buffer.getvalue()
|
| 87 |
|
| 88 |
|
| 89 |
+
def _torch_load(data: bytes) -> typing.Any:
|
| 90 |
"""Loads PyTorch data (modules, tensors) from a string."""
|
| 91 |
+
buffer = io.BytesIO(data)
|
| 92 |
return torch.load(buffer)
|
| 93 |
|
| 94 |
|
|
|
|
| 102 |
input_output_names: list[str]
|
| 103 |
loss: torch.nn.Module
|
| 104 |
source_workspace_json: str
|
| 105 |
+
optimizer_parameters: dict[str, typing.Any]
|
| 106 |
optimizer: torch.optim.Optimizer | None = None
|
| 107 |
source_workspace: str | None = None
|
| 108 |
trained: bool = False
|
|
|
|
| 120 |
values = {k: v for k, v in zip(self.model_outputs, output)}
|
| 121 |
return values
|
| 122 |
|
| 123 |
+
def inference(self, inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
|
| 124 |
"""Inference on a single batch."""
|
| 125 |
self.model.eval()
|
| 126 |
return self._forward(inputs)
|
|
|
|
| 426 |
op = self.catalog["Optimizer"]
|
| 427 |
cfg["optimizer_parameters"] = op.convert_params(self.nodes[self.optimizer].data.params)
|
| 428 |
cfg["source_workspace_json"] = self.ws.model_dump_json()
|
| 429 |
+
return ModelConfig(**cfg) # ty: ignore[missing-argument]
|
| 430 |
|
| 431 |
def get_names(self, *ids: list[str]) -> dict[str, str]:
|
| 432 |
"""Returns a mapping from internal IDs to human-readable names."""
|
|
|
|
| 452 |
|
| 453 |
|
| 454 |
def to_batch_tensors(
|
| 455 |
+
b: core.Bundle, batch_size: int, batch_index: int, m: ModelMapping
|
| 456 |
) -> dict[str, torch.Tensor]:
|
| 457 |
"""Extracts tensors from a bundle for a specific batch using a model mapping."""
|
| 458 |
tensors = {}
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch/pytorch_ops.py
CHANGED
|
@@ -117,7 +117,7 @@ def neural_ode_mlp(
|
|
| 117 |
|
| 118 |
@op("Attention", outputs=["outputs", "weights"])
|
| 119 |
def attention(query, key, value, *, embed_dim=1024, num_heads=1, dropout=0.0):
|
| 120 |
-
return torch.nn.
|
| 121 |
|
| 122 |
|
| 123 |
@op("LayerNorm", outputs=["outputs", "weights"])
|
|
@@ -250,8 +250,8 @@ reg(
|
|
| 250 |
ops.register_passive_op(
|
| 251 |
ENV,
|
| 252 |
"Repeat",
|
| 253 |
-
inputs=[ops.Input(name="input", position=
|
| 254 |
-
outputs=[ops.Output(name="output", position=
|
| 255 |
params=[
|
| 256 |
ops.Parameter.basic("times", 1, int),
|
| 257 |
ops.Parameter.basic("same_weights", False, bool),
|
|
@@ -261,8 +261,8 @@ ops.register_passive_op(
|
|
| 261 |
ops.register_passive_op(
|
| 262 |
ENV,
|
| 263 |
"Recurrent chain",
|
| 264 |
-
inputs=[ops.Input(name="input", position=
|
| 265 |
-
outputs=[ops.Output(name="output", position=
|
| 266 |
params=[],
|
| 267 |
)
|
| 268 |
|
|
|
|
| 117 |
|
| 118 |
@op("Attention", outputs=["outputs", "weights"])
|
| 119 |
def attention(query, key, value, *, embed_dim=1024, num_heads=1, dropout=0.0):
|
| 120 |
+
return torch.nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout)
|
| 121 |
|
| 122 |
|
| 123 |
@op("LayerNorm", outputs=["outputs", "weights"])
|
|
|
|
| 250 |
ops.register_passive_op(
|
| 251 |
ENV,
|
| 252 |
"Repeat",
|
| 253 |
+
inputs=[ops.Input(name="input", position=ops.Position.TOP, type="tensor")],
|
| 254 |
+
outputs=[ops.Output(name="output", position=ops.Position.BOTTOM, type="tensor")],
|
| 255 |
params=[
|
| 256 |
ops.Parameter.basic("times", 1, int),
|
| 257 |
ops.Parameter.basic("same_weights", False, bool),
|
|
|
|
| 261 |
ops.register_passive_op(
|
| 262 |
ENV,
|
| 263 |
"Recurrent chain",
|
| 264 |
+
inputs=[ops.Input(name="input", position=ops.Position.TOP, type="tensor")],
|
| 265 |
+
outputs=[ops.Output(name="output", position=ops.Position.BOTTOM, type="tensor")],
|
| 266 |
params=[],
|
| 267 |
)
|
| 268 |
|
lynxkite-graph-analytics/tests/test_core.py
CHANGED
|
@@ -20,19 +20,19 @@ async def test_multi_input_box():
|
|
| 20 |
ws.add_node(
|
| 21 |
id="1",
|
| 22 |
type="node_type",
|
| 23 |
-
|
| 24 |
position=workspace.Position(x=0, y=0),
|
| 25 |
)
|
| 26 |
ws.add_node(
|
| 27 |
id="2",
|
| 28 |
type="node_type",
|
| 29 |
-
|
| 30 |
position=workspace.Position(x=0, y=0),
|
| 31 |
)
|
| 32 |
ws.add_node(
|
| 33 |
id="3",
|
| 34 |
type="node_type",
|
| 35 |
-
|
| 36 |
position=workspace.Position(x=0, y=0),
|
| 37 |
)
|
| 38 |
ws.edges = [
|
|
|
|
| 20 |
ws.add_node(
|
| 21 |
id="1",
|
| 22 |
type="node_type",
|
| 23 |
+
title="Create Bundle",
|
| 24 |
position=workspace.Position(x=0, y=0),
|
| 25 |
)
|
| 26 |
ws.add_node(
|
| 27 |
id="2",
|
| 28 |
type="node_type",
|
| 29 |
+
title="Create Bundle",
|
| 30 |
position=workspace.Position(x=0, y=0),
|
| 31 |
)
|
| 32 |
ws.add_node(
|
| 33 |
id="3",
|
| 34 |
type="node_type",
|
| 35 |
+
title="Multi input op",
|
| 36 |
position=workspace.Position(x=0, y=0),
|
| 37 |
)
|
| 38 |
ws.edges = [
|
lynxkite-graph-analytics/tests/test_lynxkite_ops.py
CHANGED
|
@@ -12,7 +12,7 @@ async def test_execute_operation_not_in_catalog():
|
|
| 12 |
ws.add_node(
|
| 13 |
id="1",
|
| 14 |
type="node_type",
|
| 15 |
-
|
| 16 |
position=workspace.Position(x=0, y=0),
|
| 17 |
)
|
| 18 |
await execute(ws)
|
|
@@ -78,25 +78,25 @@ async def test_execute_operation_inputs_correct_cast():
|
|
| 78 |
ws.add_node(
|
| 79 |
id="1",
|
| 80 |
type="node_type",
|
| 81 |
-
|
| 82 |
position=workspace.Position(x=0, y=0),
|
| 83 |
)
|
| 84 |
ws.add_node(
|
| 85 |
id="2",
|
| 86 |
type="node_type",
|
| 87 |
-
|
| 88 |
position=workspace.Position(x=100, y=0),
|
| 89 |
)
|
| 90 |
ws.add_node(
|
| 91 |
id="3",
|
| 92 |
type="node_type",
|
| 93 |
-
|
| 94 |
position=workspace.Position(x=200, y=0),
|
| 95 |
)
|
| 96 |
ws.add_node(
|
| 97 |
id="4",
|
| 98 |
type="node_type",
|
| 99 |
-
|
| 100 |
position=workspace.Position(x=300, y=0),
|
| 101 |
)
|
| 102 |
ws.edges = [
|
|
@@ -136,19 +136,19 @@ async def test_multiple_inputs():
|
|
| 136 |
ws.add_node(
|
| 137 |
id="one",
|
| 138 |
type="cool",
|
| 139 |
-
|
| 140 |
position=workspace.Position(x=0, y=0),
|
| 141 |
)
|
| 142 |
ws.add_node(
|
| 143 |
id="two",
|
| 144 |
type="cool",
|
| 145 |
-
|
| 146 |
position=workspace.Position(x=100, y=0),
|
| 147 |
)
|
| 148 |
ws.add_node(
|
| 149 |
id="smaller",
|
| 150 |
type="cool",
|
| 151 |
-
|
| 152 |
position=workspace.Position(x=200, y=0),
|
| 153 |
)
|
| 154 |
ws.edges = [
|
|
@@ -188,8 +188,8 @@ async def test_optional_inputs():
|
|
| 188 |
return a + (b or 0)
|
| 189 |
|
| 190 |
assert maybe_add.__op__.inputs == [
|
| 191 |
-
ops.Input(name="a", type=int, position=
|
| 192 |
-
ops.Input(name="b", type=int | None, position=
|
| 193 |
]
|
| 194 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
| 195 |
a = ws.add_node(one)
|
|
|
|
| 12 |
ws.add_node(
|
| 13 |
id="1",
|
| 14 |
type="node_type",
|
| 15 |
+
title="Non existing op",
|
| 16 |
position=workspace.Position(x=0, y=0),
|
| 17 |
)
|
| 18 |
await execute(ws)
|
|
|
|
| 78 |
ws.add_node(
|
| 79 |
id="1",
|
| 80 |
type="node_type",
|
| 81 |
+
title="Create Bundle",
|
| 82 |
position=workspace.Position(x=0, y=0),
|
| 83 |
)
|
| 84 |
ws.add_node(
|
| 85 |
id="2",
|
| 86 |
type="node_type",
|
| 87 |
+
title="Bundle to Graph",
|
| 88 |
position=workspace.Position(x=100, y=0),
|
| 89 |
)
|
| 90 |
ws.add_node(
|
| 91 |
id="3",
|
| 92 |
type="node_type",
|
| 93 |
+
title="Graph to Bundle",
|
| 94 |
position=workspace.Position(x=200, y=0),
|
| 95 |
)
|
| 96 |
ws.add_node(
|
| 97 |
id="4",
|
| 98 |
type="node_type",
|
| 99 |
+
title="Dataframe to Bundle",
|
| 100 |
position=workspace.Position(x=300, y=0),
|
| 101 |
)
|
| 102 |
ws.edges = [
|
|
|
|
| 136 |
ws.add_node(
|
| 137 |
id="one",
|
| 138 |
type="cool",
|
| 139 |
+
title="One",
|
| 140 |
position=workspace.Position(x=0, y=0),
|
| 141 |
)
|
| 142 |
ws.add_node(
|
| 143 |
id="two",
|
| 144 |
type="cool",
|
| 145 |
+
title="Two",
|
| 146 |
position=workspace.Position(x=100, y=0),
|
| 147 |
)
|
| 148 |
ws.add_node(
|
| 149 |
id="smaller",
|
| 150 |
type="cool",
|
| 151 |
+
title="Smaller?",
|
| 152 |
position=workspace.Position(x=200, y=0),
|
| 153 |
)
|
| 154 |
ws.edges = [
|
|
|
|
| 188 |
return a + (b or 0)
|
| 189 |
|
| 190 |
assert maybe_add.__op__.inputs == [
|
| 191 |
+
ops.Input(name="a", type=int, position=ops.Position.LEFT),
|
| 192 |
+
ops.Input(name="b", type=int | None, position=ops.Position.LEFT),
|
| 193 |
]
|
| 194 |
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
| 195 |
a = ws.add_node(one)
|
lynxkite-graph-analytics/tests/test_pytorch_model_ops.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from lynxkite.core import workspace
|
| 2 |
from lynxkite_graph_analytics.pytorch import pytorch_core
|
| 3 |
import torch
|
|
@@ -11,7 +12,8 @@ def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str]]):
|
|
| 11 |
del data["title"]
|
| 12 |
ws.add_node(
|
| 13 |
id=id,
|
| 14 |
-
|
|
|
|
| 15 |
)
|
| 16 |
ws.edges = [
|
| 17 |
workspace.WorkspaceEdge(
|
|
@@ -27,10 +29,12 @@ def make_ws(env, nodes: dict[str, dict], edges: list[tuple[str, str]]):
|
|
| 27 |
|
| 28 |
|
| 29 |
def summarize_layers(m: pytorch_core.ModelConfig) -> str:
|
|
|
|
| 30 |
return "".join(str(e)[:2] for e in m.model)
|
| 31 |
|
| 32 |
|
| 33 |
def summarize_connections(m: pytorch_core.ModelConfig) -> str:
|
|
|
|
| 34 |
return " ".join(
|
| 35 |
"".join(n[0] for n in c.param_names) + "->" + "".join(n[0] for n in c.return_names)
|
| 36 |
for c in m.model._children
|
|
|
|
| 1 |
+
import torch_geometric.nn as pyg_nn
|
| 2 |
from lynxkite.core import workspace
|
| 3 |
from lynxkite_graph_analytics.pytorch import pytorch_core
|
| 4 |
import torch
|
|
|
|
| 12 |
del data["title"]
|
| 13 |
ws.add_node(
|
| 14 |
id=id,
|
| 15 |
+
title=title,
|
| 16 |
+
params=data,
|
| 17 |
)
|
| 18 |
ws.edges = [
|
| 19 |
workspace.WorkspaceEdge(
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def summarize_layers(m: pytorch_core.ModelConfig) -> str:
|
| 32 |
+
assert isinstance(m.model, pyg_nn.Sequential)
|
| 33 |
return "".join(str(e)[:2] for e in m.model)
|
| 34 |
|
| 35 |
|
| 36 |
def summarize_connections(m: pytorch_core.ModelConfig) -> str:
|
| 37 |
+
assert isinstance(m.model, pyg_nn.Sequential)
|
| 38 |
return " ".join(
|
| 39 |
"".join(n[0] for n in c.param_names) + "->" + "".join(n[0] for n in c.return_names)
|
| 40 |
for c in m.model._children
|
lynxkite-pillow-example/src/lynxkite_pillow_example/__init__.py
CHANGED
|
@@ -20,48 +20,48 @@ def open_image(*, filename: str):
|
|
| 20 |
|
| 21 |
|
| 22 |
@op("Save image")
|
| 23 |
-
def save_image(image: Image, *, filename: str):
|
| 24 |
with fsspec.open(filename, "wb") as f:
|
| 25 |
image.save(f)
|
| 26 |
|
| 27 |
|
| 28 |
@op("Crop")
|
| 29 |
-
def crop(image: Image, *, top: int, left: int, bottom: int, right: int):
|
| 30 |
return image.crop((left, top, right, bottom))
|
| 31 |
|
| 32 |
|
| 33 |
@op("Flip horizontally")
|
| 34 |
-
def flip_horizontally(image: Image):
|
| 35 |
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
| 36 |
|
| 37 |
|
| 38 |
@op("Flip vertically")
|
| 39 |
-
def flip_vertically(image: Image):
|
| 40 |
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
| 41 |
|
| 42 |
|
| 43 |
@op("Blur")
|
| 44 |
-
def blur(image: Image, *, radius: float = 5):
|
| 45 |
return image.filter(ImageFilter.GaussianBlur(radius))
|
| 46 |
|
| 47 |
|
| 48 |
@op("Detail")
|
| 49 |
-
def detail(image: Image):
|
| 50 |
return image.filter(ImageFilter.DETAIL)
|
| 51 |
|
| 52 |
|
| 53 |
@op("Edge enhance")
|
| 54 |
-
def edge_enhance(image: Image):
|
| 55 |
return image.filter(ImageFilter.EDGE_ENHANCE)
|
| 56 |
|
| 57 |
|
| 58 |
@op("To grayscale")
|
| 59 |
-
def to_grayscale(image: Image):
|
| 60 |
return image.convert("L")
|
| 61 |
|
| 62 |
|
| 63 |
@op("View image", view="image")
|
| 64 |
-
def view_image(image: Image):
|
| 65 |
buffered = io.BytesIO()
|
| 66 |
image.save(buffered, format="webp")
|
| 67 |
b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
@op("Save image")
|
| 23 |
+
def save_image(image: Image.Image, *, filename: str):
|
| 24 |
with fsspec.open(filename, "wb") as f:
|
| 25 |
image.save(f)
|
| 26 |
|
| 27 |
|
| 28 |
@op("Crop")
|
| 29 |
+
def crop(image: Image.Image, *, top: int, left: int, bottom: int, right: int):
|
| 30 |
return image.crop((left, top, right, bottom))
|
| 31 |
|
| 32 |
|
| 33 |
@op("Flip horizontally")
|
| 34 |
+
def flip_horizontally(image: Image.Image):
|
| 35 |
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
| 36 |
|
| 37 |
|
| 38 |
@op("Flip vertically")
|
| 39 |
+
def flip_vertically(image: Image.Image):
|
| 40 |
return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
| 41 |
|
| 42 |
|
| 43 |
@op("Blur")
|
| 44 |
+
def blur(image: Image.Image, *, radius: float = 5):
|
| 45 |
return image.filter(ImageFilter.GaussianBlur(radius))
|
| 46 |
|
| 47 |
|
| 48 |
@op("Detail")
|
| 49 |
+
def detail(image: Image.Image):
|
| 50 |
return image.filter(ImageFilter.DETAIL)
|
| 51 |
|
| 52 |
|
| 53 |
@op("Edge enhance")
|
| 54 |
+
def edge_enhance(image: Image.Image):
|
| 55 |
return image.filter(ImageFilter.EDGE_ENHANCE)
|
| 56 |
|
| 57 |
|
| 58 |
@op("To grayscale")
|
| 59 |
+
def to_grayscale(image: Image.Image):
|
| 60 |
return image.convert("L")
|
| 61 |
|
| 62 |
|
| 63 |
@op("View image", view="image")
|
| 64 |
+
def view_image(image: Image.Image):
|
| 65 |
buffered = io.BytesIO()
|
| 66 |
image.save(buffered, format="webp")
|
| 67 |
b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|