Skip to content

Instantly share code, notes, and snippets.

@knkillname
Created August 21, 2015 14:13
Show Gist options
  • Save knkillname/5a789cfd0011a6255ea7 to your computer and use it in GitHub Desktop.
Save knkillname/5a789cfd0011a6255ea7 to your computer and use it in GitHub Desktop.
## Author: Mario Abarca (knkillname)
## Email: asma@uaem.mx
## Date: 25 Feb 2015
## Language: Python 3
##
## Summary: A class representing bigraph objects given by a quasi-Cartan
## Matrix. Includes elementary operations, csv formating, and ploting.
from array import array
from os.path import splitext
from itertools import chain
from math import pi, sqrt, sin, cos
from cmath import rect
from random import uniform, choice, getrandbits, randint
from collections import deque
from tkinter import *
from tkinter import ttk
class bigraph:
__slots__ = ('_adj', '_vert', '_idx')
def __init__(self):
self._adj = []
self._vert = [] #vert[i]: name of vertex i
self._idx = dict() #idx[v]: index of vertex v
def __len__(self):
return len(self._adj)
@property
def adj(self):
return self._adj
def vertices(self):
return iter(self._vert)
def _edges(self):
A = self._adj
for i in range(len(A) - 1):
for j in range(i + 1, len(A)):
if i != j and A[i][j] != 0:
dotted = (A[i][j] > 0)
direction = abs(A[j][i]) - abs(A[i][j])
if direction > 0:
e = '$>'
elif direction == 0:
e = '$$'
else: e = '<$'
if dotted:
e = e.replace('$', '.')
else: e = e.replace('$', '-')
yield (i, e, j)
def edges(self):
for (i, e, j) in self._edges():
yield (self._vert[i], e, self._vert[j])
def _add_vertex(self, name):
n = len(self._adj)
self._idx[name] = n
self._vert.append(name)
for row in self._adj:
row.append(0)
self._adj.append(array('b', (0 for k in range(n + 1))))
self._adj[n][n] = 2
return n
def _add_edge(self, u, v, e):
i, j = self._idx[u], self._idx[v]
if e == '--':
self._adj[i][j] -= 1
self._adj[j][i] -= 1
elif e == '->':
self._adj[i][j] -= 1
self._adj[j][i] -= 2
elif e == '<-':
self._adj[i][j] -= 2
self._adj[j][i] -= 1
elif e == '..':
self._adj[i][j] += 1
self._adj[j][i] += 1
elif e == '.>':
self._adj[i][j] += 1
self._adj[j][i] += 2
elif e == '<.':
self._adj[i][j] += 2
self._adj[j][i] += 1
else: raise ValueError
def add(self, path):
sentinel, item = object(), iter(path)
u = next(item)
if u not in self._idx: self._add_vertex(u)
while True:
e = next(item, sentinel)
if e == sentinel:
break
if not isinstance(e, str):
raise TypeError
v = next(item)
if u == v: raise ValueError
if v not in self._idx: self._add_vertex(v)
self._add_edge(u, v, e)
u = v
def delete(self, u):
i = self._idx[u]
del self._adj[i]
for row in self._adj:
del row[i]
del self._vert[i]
for (j, v) in enumerate(self._vert):
self._idx[v] = j
del self._idx[u]
def shrink(self, X, v):
if v not in self._idx:
self._add_vertex(v)
for u in X:
if u == v: continue
i = self._idx[u]
j = self._idx[v]
for (k, w) in enumerate(self._adj[i]):
if k != j and w != 0:
self._adj[k][j] += self._adj[k][i]
self._adj[j][k] += self._adj[i][k]
self.delete(u)
def neighbors(self, u):
i = self._idx[u]
for (j, w) in enumerate(self._adj[i]):
if w != 0:
yield self._vert[j]
def __str__(self, sort = False):
A, n, idx, vert = self._adj, len(self._adj), self._idx, self._vert
if n*n > 0:
I = list(idx[v] for v in sorted(self._idx)) if sort else range(n)
N = [str(vert[i]) for i in I]
B = [[str(A[i][j]) for j in I] for i in I]
p = [max(max(len(B[i][j]) for i in I), len(N[j])) for j in I]
N = [N[j].center(p[j]) for j in I]
B = [[B[i][j].center(p[j]) for j in I] for i in I]
B = [' '.join(B[i]) for i in I]
N = ' ' + ' '.join(N)
else: return '[]'
if n > 1:
B[0] = ''.join(['┌', B[0] ,'┐'])
B[1 : n - 1] = [''.join(['│', B[i] ,'│']) for i in range(1, n - 1)]
B[n - 1] = ''.join(['└', B[n - 1] ,'┘'])
return '\n'.join([N, '\n'.join(row for row in B)])
else: return '\n'.join([N, ''.join(['[', B[0] ,']'])])
def _embed(self, pos = None, temp = 1.0, steps = None):
#Uses Fruchterman-Reingold force embedder
n = len(self._adj)
if pos == None:
pos = [rect(temp/2, uniform(-pi, pi)) for u in range(n)]
if steps == None:
steps = n*(n - 1) + 1
cool = temp/steps
for i in range(steps):
disp = [complex(0.0, 0.0) for v in range(n)]
for u in range(n - 1):
for v in range(u + 1, n):
D = pos[v] - pos[u]
rf = D/(D.real**2 + D.imag**2)
disp[v] += rf
disp[u] -= rf
if self._adj[u][v] != 0:
D = pos[v] - pos[u]
af = abs(D)*D
disp[u] += af
disp[v] -= af
for v in range(n):
pos[v] += (disp[v]/abs(disp[v]))*min(abs(disp[v]), temp)
temp -= cool
return pos
def embedding(self, start_pos = None, raw_output = False,
temp = 1.0, steps = None):
idx = self._idx
if len(self._adj) == 0: return dict()
if start_pos != None:
pos = [None]*len(self._adj)
for (v, (x, y)) in start_pos.items():
pos[idx[v]] = complex(x, y)
start_pos = pos
pos = self._embed(start_pos, temp, steps)
if raw_output:
return pos
return {self._vert[i]:(p.real, p.imag) for (i, p) in enumerate(pos)}
def _edge_partition(self):
#Separates the edges in four types: 0. Nondirected solid edges
#1. Directed solid edges 2. Nondirected dotted edges and
#3. Directed dotted edges
A = self._adj
n = len(A)
edges = [[], [], [], []]
for i in range(n - 1):
for j in range(i + 1, n):
if A[i][j] == 0: continue
edge_type = 2*int(A[i][j] > 0) + int(A[i][j] != A[j][i])
if abs(A[i][j]) < abs(A[j][i]):
(u, v) = (i, j)
else: (u, v) = (j, i)
edges[edge_type].append((u, v))
return edges
def tex(self, edge_length = 1.0, pos = None):
def coords(z):
return (round(z.real, 2), round(z.imag, 2))
V = self._vert
n = len(V)
s = ' \\node (v{}) at {} {{{}}};'
if pos == None:
pos = self._embed()
nodes = '\n'.join(s.format(i, coords(pos[i]), V[i])\
for i in range(n))
else:
pos = [pos[v] for v in self._vert]
nodes = '\n'.join(s.format(i, pos[i], V[i]) for i in range(n))
edges = self._edge_partition()
for t in range(4):
edges[t] = ', '.join('v{}/v{}'.format(u, v) for (u, v) in edges[t])
return '''\\begin{{tikzpicture}}[scale = {0}, every node/.style={{circle, inner sep=1pt}}]
{1}
\\foreach \\from/\\to in {{{2}}}
\\draw (\\from) -- (\\to);
\\foreach \\from/\\to in {{{3}}}
\\draw[->] (\\from) -- (\\to);
\\foreach \\from/\\to in {{{4}}}
\\draw[dotted] (\\from) -- (\\to);
\\foreach \\from/\\to in {{{5}}}
\\draw[->, dotted] (\\from) -- (\\to);
\\end{{tikzpicture}}'''.format(edge_length, nodes, *edges)
@staticmethod
def load(file_name):
(path, ext) = splitext(file_name)
if not ext: ext = '.csv'
file = open(path + ext)
vert = [eval(item) for item in (file.readline().strip()).split(',')]
adj = file.readlines()
file.close()
n = len(vert)
if len(adj) != n:
raise ValueError('File does not contain a valid bigraph')
for k in range(n):
adj[k] = array('b', map(int, (adj[k].strip()).split(',')))
if len(adj[k]) != n or adj[k][k] != 2:
raise ValueError('File does not contain a valid bigraph')
G = bigraph()
G._adj = adj
G._vert = vert
G._idx = {u:i for (i, u) in enumerate(vert)}
return G
def save(self, file_name):
lines = [','.join(str(item) for item in row) for row in self._adj]
(path, ext) = splitext(file_name)
if not ext: ext = '.csv'
file = open(path + ext, 'w')
file.write(','.join(map(repr, self._vert)))
file.write('\n')
file.write('\n'.join(lines))
file.close()
def plot(self, typeface = 'times', size = 14, border = 8, thickness = 1,
pos = None):
def coords(z):
return (z.real, z.imag)
def redraw(event):
canvas.delete('all')
k = min((event.width - blft - brgt)/w,
(event.height - btop - bbot)/h)
p = [k*z + b for z in pos]
for (i, e, j) in elist:
x1, y1 = coords(p[i])
x2, y2 = coords(p[j])
if '-' in e:
canvas.create_line(x1, y1, x2, y2)
else:
canvas.create_line(x1, y1, x2, y2, dash = (2,))
if ('>' in e) or ('<' in e):
if '<' in e: i, j = j, i
arg = p[i] - p[j]
arg = arg/abs(arg)
ort = complex(arg.imag, -arg.real)
ti0 = p[j] + rad[j]*arg
ti1 = ti0 + (0.5*size)*arg + (0.25*size)*ort
ti2 = ti0 + (0.25*size)*arg
ti3 = ti0 + (0.5*size)*arg - (0.25*size)*ort
x0, y0 = coords(ti0)
x1, y1 = coords(ti1)
x2, y2 = coords(ti2)
x3, y3 = coords(ti3)
canvas.create_polygon(x0, y0, x1, y1, x2, y2, x3, y3,
fill = 'black', outline = '')
for i in range(n):
x, y = coords(p[i])
canvas.create_oval(x - rad[i], y - rad[i],
x + rad[i], y + rad[i],
fill = 'lightgray', outline = '')
canvas.create_text(x, y, text = vlist[i],
font = (typeface, size))
if len(self._adj) == 0: raise RuntimeError('Nothing to plot!')
if pos == None:
pos = self._embed()
else:
C = lambda z: complex(z[0], z[1])
pos = [C(pos[v]) for v in self._vert]
vlist = list(self._vert)
elist = list(self._edges())
n, m = len(vlist), len(elist)
ilft, irgt, itop, ibot = 0, 0, 0, 0
for i in range(n):
if pos[i].real < pos[ilft].real:
ilft = i
if pos[i].real > pos[irgt].real:
irgt = i
if pos[i].imag < pos[itop].imag:
itop = i
if pos[i].imag > pos[ibot].imag:
ibot = i
d = complex(pos[ilft].real, pos[itop].imag)
for i in range(n):
pos[i] -= d
w = pos[irgt].real
h = pos[ibot].imag
root = Tk()
root.title('Plot')
root.columnconfigure(0, weight = 1)
root.rowconfigure(0, weight = 1)
canvas = Canvas(root, background = 'white')
rad = [0.0 for i in range(n)]
aux = canvas.create_text(0, 0, fill = 'white', font = (typeface, size))
for i in range(n):
canvas.itemconfig(aux, text = vlist[i])
x1, y1, x2, y2 = canvas.bbox(aux)
rad[i] = 0.5*sqrt((x2 - x1)**2 + (y2 - y1)**2)
canvas.delete(aux)
del aux
blft, btop, brgt, bbot = (rad[i] + border \
for i in (ilft, itop, irgt, ibot))
b = complex(blft, btop)
k = 5*size
canvas.config(width = k*w + blft + brgt, height = k*h + btop + bbot)
canvas.grid(column = 0, row = 0, sticky=(N, W, E, S))
canvas.bind('<Configure>', redraw)
root.bind('<Return>', lambda e: root.destroy())
root.mainloop()
def _flation(self, s, r):
A = self._adj
V = range(len(A))
row = [-A[r][s]*A[s][k] for k in V]
col = [-A[s][r]*A[k][s] for k in V]
A[r][r] += A[s][s]*A[s][r]*A[r][s]
for k in V:
A[r][k] += row[k]
A[k][r] += col[k]
def flation(self, s, r):
self._flation(self._idx[s], self._idx[r])
def inversion(self, r):
A, r = self._adj, self._idx[r]
n = len(A)
for k in range(n):
A[k][r] *= -1
A[r][k] *= -1
def permutation(self, s, r):
i, j = self._idx[s], self._idx[r]
self._idx[s], self._idx[r] = j, i
self._vert[i], self._vert[j] = r, s
def frame(self):
F = bigraph()
F._vert = self._vert[:]
F._idx = self._idx.copy()
F._adj = [array('b', (-(a % 2) for a in row)) for row in self._adj]
return F
@staticmethod
def finite_A(n):
B = bigraph()
path = lambda i: '--' if i % 2 else i//2 + 1
B.add(path(i) for i in range(2*n - 1))
return B
@staticmethod
def finite_B(n):
B = bigraph.finite_A(n - 1)
B.add([n - 1, '->', n])
return B
@staticmethod
def finite_C(n):
B = bigraph.finite_A(n - 1)
B.add([n - 1, '<-', n])
return B
@staticmethod
def finite_D(n):
B = bigraph.finite_A(n)
B.add([2, '..', 1, '--', 3])
return B
@staticmethod
def finite_E(n):
B = bigraph.finite_A(n)
B.add([2, '..', 1, '--', 4])
return B
@staticmethod
def finite_F(n):
B = bigraph.finite_A(n)
B.add([3, '..', 2, '->', 3])
return B
def reduce(self, plot = False):
A, n, v = self._adj, len(self._adj), self._vert
while True:
dotted = [(i, j) for (i, e, j) in self._edges() if A[i][j] > 0]
if not dotted: break
(s, r) = choice(dotted)
if bool(getrandbits(1)):
s, r = r, s
self._flation(s, r)
yield v[s], v[r]
def treeify(self):
A, n, vname = self._adj, len(self._adj), self._vert
V = [i for (i, u) in sorted(enumerate(self._vert),
key = lambda t: t[1])]
def dfs_chrodless_cycle(s):
nonlocal A
nonlocal V
nonlocal n
visited = [False for v in V]
parent = [None for v in V]
dist = [float('inf') for v in V]
visited[s], dist[s] = True, 0
Q = deque([s])
flag = False
while len(Q) > 0:
u = Q.popleft()
for v in V:
if bool(A[u][v] % 2) and u != v:
if not visited[v]:
visited[v] = True
parent[v] = u
dist[v] = dist[u] + 1
Q.append(v)
elif parent[u] != v:
flag = True
break
if flag: break
if not flag: return None
P = deque()
if dist[v] > dist[u]:
P.append(v)
v = parent[v]
while u != v:
P.appendleft(u)
P.append(v)
u, v = parent[u], parent[v]
P.appendleft(u)
return list(P)
r = V[0]
while True:
p = dfs_chrodless_cycle(r)
if p is None: break
for k in range(1, len(p) - 1):
self._flation(p[k], p[k + 1])
yield list(vname[u] for u in p[1:])
if __name__ == '__main__':
G = bigraph()
G.add([1, '..', 2, '--', 3, '.>', 4, '<-' ,2])
for row in G._adj:
print(list(row))
print(G.tex())
for e in G.edges():
print(e)
G.plot(size = 12)
## Author: Mario Abarca (knkillname)
## Email: asma@uaem.mx
## Date: 18 Mar 2015
## Language: Python 3
##
## Summary: A simple command-line utility for interacting with bigraphs.
import re
import os
from cmd import Cmd
from bigraph import bigraph
from collections import namedtuple
token = namedtuple('token', ['typ', 'val'])
class arg_parser:
tokens = {'int': r'\d+',
'edge': r'(\-\-)|(\->)|(<\-)|(\.\.)|(\.>)|(<\.)',
'id': r'[A-F]',
'skip': r'\s+'}
def __init__(self):
tok_rex = '|'.join('(?P<{}>{})'.format(t, r) \
for (t, r) in self.tokens.items())
self.token_match = re.compile(tok_rex).match
self.whitespace = re.compile('\s+')
def tokenize(self, arg):
mo = self.token_match(arg)
pos = 0
while mo is not None:
typ = mo.lastgroup
if typ != 'skip':
yield token(typ, mo.group(typ))
pos = mo.end()
mo = self.token_match(arg, pos)
if pos != len(arg):
raise RuntimeError('Unexpected character')
def intseq(self, arg):
for tok in self.tokenize(arg):
if tok.typ != 'int': raise RuntimeError('Unexpected token')
yield int(tok.val)
def path(self, arg):
vert = True
for tok in self.tokenize(arg):
if vert and tok.typ == 'int':
yield int(tok.val)
elif not vert and tok.typ == 'edge':
yield tok.val
else: raise RuntimeError('Unexpected token')
vert = not vert
if vert: raise RuntimeError('Invalid sintax')
def splitargs(self, args):
return self.whitespace.split(args.strip())
def cartype(self, args):
toks = list(self.tokenize(args))
if len(toks) != 2 or toks[0].typ != 'id' or toks[1].typ != 'int':
raise RuntimeError('Unexpected token')
return (toks[0].val, int(toks[1].val))
class interactive(Cmd):
intro = 'interactive.py 3.2 Type «?» for help.'
prompt = '> '
nosintax = '* Unknown sintax: {}'
def check_graphical(self):
V = list(self.B.vertices())
A = self.B.adj
n = len(A)
self.graficable = True
for u in range(1, n):
for v in range(u):
if A[u][v]*A[v][u] > 4:
print('Multiple edges at {}──{}.'.format(V[u], V[v]))
self.graficable = False
if (A[u][v] == 0) != (A[v][u] == 0):
print('Undefined edge at {}──{}.'.format(V[u], V[v]))
self.graficable = False
def __init__(self):
super().__init__()
self.B = bigraph()
self.pos = None
self.parse = arg_parser()
def do_neighbors(self, args):
'''Sintax: neighbors v
Shows the neighbors of vertex v in the current bigraph.'''
try:
u = int(args.strip())
except RuntimeError: return self.default('neighbors', args)
if u in self.B.vertices():
print(' '.join(map(str, self.B.neighbors(u))))
else: print('{} does not belong to this bigraph'.format(u))
def do_new(self, args):
'''Sintaxis: new [id]
Loads an empty bigraph. Optional argument id can take values A, B, C, D, E or F
followed by a positive integer. Examples:
new → loads a new, empty bigraph
new E6 → loads the Dynkin graph E6'''
if args:
try:
args = self.parse.cartype(args)
except: return self.default('new', args)
cons = {'A':bigraph.finite_A,
'B':bigraph.finite_B,
'C':bigraph.finite_C,
'D':bigraph.finite_D,
'E':bigraph.finite_E,
'F':bigraph.finite_F}
self.B = cons[args[0]](args[1])
else: self.B = bigraph()
self.pos = None
self.graficable = True
def do_quit(self, args):
'''Sintax: quit'''
if args == '':
return True
else: self.default('quit', args)
def do_tex(self, args):
'''Sintax: tex
Prints out a LaTeX listing which uses TikZ package to draw the bigraph'''
print(self.B.tex(pos = self.pos))
def do_load(self, args):
'''Sintax: load file
Loads a given csv file (no extension needed) as the quasi-Cartan matrix
representing a bigraph. Example: load ejemplo'''
try:
self.B = bigraph.load(args)
self.pos = None
except: print('Error al abrir {}'.format(repr(args)))
def do_save(self, args):
'''Sintaxis: save file
Saves the current bigraph.
Example: save thisbigraph'''
try:
self.B.save(args)
except: print('Error al guardar {}'.format(repr(args)))
def do_plot(self, args):
'''Sintax: plot [frame]
Shows the current bigraph on screen. If used with "frame" the plot draws
dotted edges as solid ones.'''
if self.graficable:
if self.B.vertices():
args = self.parse.splitargs(args)
self.pos = self.B.embedding(start_pos = self.pos)
if args == ['frame']:
self.B.frame().plot(pos = self.pos)
else: self.B.plot(pos = self.pos)
else: print('Nothing to graph.')
else: print('The current matrix is not graphical.')
def do_adj(self, args):
'''Sintax: adj [maxima|python]
Shows the quasi-Cartan (adjacency matrix) of the current bigraph.'''
if not args:
print(self.B)
else:
adj = self.B.adj
if args == 'maxima':
print('matrix({})'.format(', '.join(str(list(row)) for row in adj)))
elif args == 'python':
print(adj)
def do_T(self, args):
'''Sintax: T v₁ v₂ v₃ … vₖ
Applies the flation transformation from v₁ to v₂, from v₂ to v₃, etc.'''
try:
v = list(self.parse.intseq(args))
except RuntimeError: v = []
if len(v) < 2:
self.default('T', args)
else:
B = self.B
if any(u not in self.B.vertices() for u in v):
print('All vertices must belong to the bigraph.')
return False
for k in range(len(v) - 1):
B.flation(v[k], v[k + 1])
self.check_graphical()
def do_I(self, args):
'''Sintax: I v₁ v₂ v₃ … vₖ
Applies the sign-inversion operation over v₁, v₂, v₃, … vₖ.'''
try:
v = list(self.parse.intseq(args))
except RuntimeError: v = []
if len(v) < 1:
self.default('I', args)
else:
if any(u not in self.B.vertices() for u in v):
print('All vertices must belong to the bigraph.')
return False
B = self.B
for u in v:
B.inversion(u)
def do_P(self, args):
'''Sintax: P v₁ v₂
Intercchanges the labels of vertices v₁ and v₂.'''
try:
u, v = self.parse.intseq(args)
except: return self.default('P', args)
if not {u, v} <= self.B.vertices():
print('The vertices must belong to the bigraph.')
return False
else: self.B.permutation(u, v)
def do_S(self, args):
'''Sintax: S v₁ v₂ v₃ … vₖ
Shrinks the subgraph given by the vertices v₁ v₂ v₃ … vₖ into a single vertex
with label vₖ.'''
try:
v = list(self.parse.intseq(args))
except RuntimeError: v = []
if len(v) < 2:
self.default('S', args)
else:
if any(u not in self.B.vertices() for u in v):
print('All vertices must belong to the bigraph.')
else:
self.B.shrink(v, v[-1])
if self.pos is not None:
for k in range(len(v) - 1):
del self.pos[v[k]]
self.check_graphical()
def do_add(self, args):
'''Sintax: add v₀ e₁ v₁ e₂ v₂ e₃ v₃ … eₖ vₖ
Adds a path to the bigraph. vertices must be natural numbers and y the edges
can be of four types:
-- : solid edge
.. : dotted edge
-> : solid arc (also <-)
.> : doted arc (also <.)
Example: add 1--2..3.>4<-5'''
if args:
try:
path = list(self.parse.path(args))
except RuntimeError: return self.default('add', args)
self.B.add(path)
self.pos = None
self.check_graphical()
else:
print('Nothing to add.')
def do_del(self, args):
'''Sintax: del v₁ v₂ v₃ … vₖ
Deletes the vertices v₁, v₂, v₃, …, vₖ from the bigraph.'''
try:
u = int(args.strip())
except: return self.default('del', args)
if u in self.B.vertices():
self.B.delete(u)
del self.pos[u]
else: print('{} does not belong to the bigraph.'.format(u))
def default(self, *args):
print(self.nosintax.format(' '.join(args)))
def do_reduce(self, args):
'''Sintax: reduce [plot] [adj] [tex]
Aplies the inflations method to the current bigraph.'''
flags = frozenset(self.parse.splitargs(args))
if not flags <= frozenset(['plot', 'tex', 'adj', '']):
return self.default('reduce', args)
bplot = 'plot' in flags
btex = 'tex' in flags
badj = 'adj' in flags
T = iter(self.B.reduce())
while True:
if badj: self.do_adj('')
if bplot: self.do_plot('')
if btex: self.do_tex('')
edge = next(T, None)
if edge == None: break
print('> T {} {}'.format(*edge))
self.check_graphical()
def do_treeify(self, args):
'''Sintax: treeify [plot] [adj] [tex]
Applies an heuristic flations method which tries to compute the tree form of
the current bigraph.'''
flags = frozenset(self.parse.splitargs(args))
if not flags <= frozenset(['plot', 'tex', 'adj', '']):
return self.default('treeify', args)
bplot = 'plot' in flags
btex = 'tex' in flags
badj = 'adj' in flags
T = iter(self.B.treeify())
while True:
if badj: self.do_adj('')
if bplot: self.do_plot('')
if btex: self.do_tex('')
path = next(T, None)
if path == None: break
print('> T ' + ' '.join(map(str, path)))
self.check_graphical()
if __name__ == '__main__':
os.system('clear')
interactive().cmdloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment