Spaces:
Running
Running
File size: 5,558 Bytes
ca01fa3 54e01be 0c44583 ca01fa3 a18645a d994c06 ca01fa3 d994c06 54e01be d994c06 21590fa d994c06 54e01be d994c06 54e01be d994c06 ca01fa3 d994c06 3010d5b ca01fa3 9e91869 21590fa 9e91869 21590fa 9e91869 21590fa b6d30cb a18645a d994c06 a18645a d994c06 a18645a ca01fa3 3010d5b a06b506 3010d5b ca01fa3 b6d30cb 3010d5b ca01fa3 3010d5b d994c06 3010d5b ca01fa3 a18645a 0c44583 b6d30cb a18645a b6d30cb a18645a 0c44583 3010d5b ca01fa3 d994c06 21590fa 0c44583 3010d5b ca01fa3 d994c06 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
'''API for implementing LynxKite operations.'''
import dataclasses
import enum
import functools
import inspect
import networkx as nx
import pandas as pd
import typing
ALL_OPS = {}
PARAM_TYPE = type[typing.Any]
@dataclasses.dataclass
class Parameter:
'''Defines a parameter for an operation.'''
name: str
default: any
type: PARAM_TYPE = None
@staticmethod
def options(name, options):
return Parameter(name, options[0], enum.Enum(f'OptionsFor{name}', options))
def __post_init__(self):
if self.default is inspect._empty:
self.default = None
if self.type is None or self.type is inspect._empty:
self.type = type(self.default)
def to_json(self):
if isinstance(self.type, type) and issubclass(self.type, enum.Enum):
t = {'enum': list(self.type.__members__.keys())}
else:
t = str(self.type)
return {
'name': self.name,
'default': self.default,
'type': t,
}
@dataclasses.dataclass
class Op:
func: callable
name: str
params: dict[str, Parameter]
inputs: dict # name -> type
outputs: dict # name -> type
type: str # The UI to use for this operation.
sub_nodes: list = None # If set, these nodes can be placed inside the operation's node.
def __call__(self, *inputs, **params):
# Convert parameters.
for p in params:
if p in self.params:
if self.params[p].type == int:
params[p] = int(params[p])
elif self.params[p].type == float:
params[p] = float(params[p])
# Convert inputs.
inputs = list(inputs)
for i, (x, t) in enumerate(zip(inputs, self.inputs.values())):
if t == nx.Graph and isinstance(x, Bundle):
inputs[i] = x.to_nx()
elif t == Bundle and isinstance(x, nx.Graph):
inputs[i] = Bundle.from_nx(x)
res = self.func(*inputs, **params)
return res
def to_json(self):
return {
'type': self.type,
'data': {
'title': self.name,
'inputs': {i: str(type) for i, type in self.inputs.items()},
'outputs': {o: str(type) for o, type in self.outputs.items()},
'params': [p.to_json() for p in self.params.values()],
},
'sub_nodes': [sub.to_json() for sub in self.sub_nodes.values()] if self.sub_nodes else None,
}
@dataclasses.dataclass
class RelationDefinition:
'''Defines a set of edges.'''
df: str # The DataFrame that contains the edges.
source_column: str # The column in the edge DataFrame that contains the source node ID.
target_column: str # The column in the edge DataFrame that contains the target node ID.
source_table: str # The DataFrame that contains the source nodes.
target_table: str # The DataFrame that contains the target nodes.
source_key: str # The column in the source table that contains the node ID.
target_key: str # The column in the target table that contains the node ID.
@dataclasses.dataclass
class Bundle:
'''A collection of DataFrames and other data.
Can efficiently represent a knowledge graph (homogeneous or heterogeneous) or tabular data.
It can also carry other data, such as a trained model.
'''
dfs: dict = dataclasses.field(default_factory=dict) # name -> DataFrame
relations: list[RelationDefinition] = dataclasses.field(default_factory=list)
other: dict = None
@classmethod
def from_nx(cls, graph: nx.Graph):
edges = nx.to_pandas_edgelist(graph)
d = dict(graph.nodes(data=True))
nodes = pd.DataFrame(d.values(), index=d.keys())
nodes['id'] = nodes.index
return cls(
dfs={'edges': edges, 'nodes': nodes},
relations=[
RelationDefinition(
df='edges',
source_column='source',
target_column='target',
source_table='nodes',
target_table='nodes',
source_key='id',
target_key='id',
)
]
)
def to_nx(self):
graph = nx.from_pandas_edgelist(self.dfs['edges'])
nx.set_node_attributes(graph, self.dfs['nodes'].set_index('id').to_dict('index'))
return graph
def nx_node_attribute_func(name):
'''Decorator for wrapping a function that adds a NetworkX node attribute.'''
def decorator(func):
@functools.wraps(func)
def wrapper(graph: nx.Graph, **kwargs):
graph = graph.copy()
attr = func(graph, **kwargs)
nx.set_node_attributes(graph, attr, name)
return graph
return wrapper
return decorator
def op(name, *, view='basic', sub_nodes=None):
'''Decorator for defining an operation.'''
def decorator(func):
sig = inspect.signature(func)
# Positional arguments are inputs.
inputs = {
name: param.annotation
for name, param in sig.parameters.items()
if param.kind != param.KEYWORD_ONLY}
params = {}
for n, param in sig.parameters.items():
if param.kind == param.KEYWORD_ONLY:
params[n] = Parameter(n, param.default, param.annotation)
outputs = {'output': 'yes'} if view == 'basic' else {} # Maybe more fancy later.
op = Op(func, name, params=params, inputs=inputs, outputs=outputs, type=view)
if sub_nodes is not None:
op.sub_nodes = sub_nodes
op.type = 'sub_flow'
ALL_OPS[name] = op
return func
return decorator
def no_op(*args, **kwargs):
if args:
return args[0]
return Bundle()
def register_passive_op(name, inputs={'input': Bundle}, outputs={'output': Bundle}, params=[]):
'''A passive operation has no associated code.'''
op = Op(no_op, name, params={p.name: p for p in params}, inputs=inputs, outputs=outputs, type='basic')
ALL_OPS[name] = op
|