diff --git a/.gitignore b/.gitignore index c7f5d98..bb0b9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ dist/ # CSV Files *.csv + +# virtualenv +env diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..873f5bc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +global-include *.kv *.ini *.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..3839248 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +Persimmon ![travis](https://travis-ci.org/AlvarBer/Persimmon.svg?branch=master) +=================== + +![Final aspect](docs/images/final_aspect.png) + +What is it? +----------- +Persimmon is a visual dataflow language for creating sklearn pipelines. + +It represents functions as blocks, inputs and outputs are presented as pins, +and type safety is enforced when the connection is being made. + +![Type safety](docs/images/type_safety.gif) + +How to install? +--------------- +If you have pip (Python 3.5+) you can simply type + +`$> pip install persimmon` + +For windows self-contained executables can be found on the [releases page]. + + +![Full use](docs/images/full_use.gif) + + +[releases page]: https://github.com/AlvarBer/Persimmon/releases diff --git a/Readme.md b/Readme.md deleted file mode 100644 index feb55ca..0000000 --- a/Readme.md +++ /dev/null @@ -1,35 +0,0 @@ -Project Definition ![travis](https://travis-ci.org/AlvarBer/Persimmon.svg?branch=master) -================== - -Problem -------- -Machine Learning is a multi-disciplinary subject. Because of this learners come from a variety of backgrounds, -and the mastering process can be though, specially when some of these backgrounds don't have strong programming foundations. - - - -Approaches ---------- -A visual interface for programming could help easing the steep learning curve, focusing on the methods, the intuition and -their connections instead on the technicals details of the underlying library. - -Possible Outcomes ------------------ -* Greater engagement than traditional coding -* Similar engagement than traditional coding -* Less engagement than traditional coding - -Each of these outcomes can occur for the same project based on the user background. - -Experimentation ---------------- -The ideal experimentation would include trials with subjects from the following backgrounds -* Statistics -* Mahematics -* Physics/Engineering -* CS -* Others (Business, etc...) - -Trials would be based on -* A/B Testing based on different interfaces paradigms (Including no visual interface at all) -* Focus groups with iterative feedback that can influence development diff --git a/docs/Makefile b/docs/Makefile index 34b0534..1e061e0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,8 +1,8 @@ -PDF := persimmon.pdf # PDF Main Target +PDF := persimmon.pdf # PDF Default Target MARKDOWN := introduction.md literature.md workflow.md milestones.md \ risk.md interface.md implementation.md types.md \ evaluation.md postmortem.md # Markdown files -MARKDOWN_COMPLUTENSE := introduction.md focus.md literature.md \ +MARKDOWN_COMPLU := introduction.md focus.md literature.md \ workflow.md milestones.md risk.md interface.md implementation.md \ types.md analysis.md postmortem.md APPENDICES := packages.md how.md eval_form.md # Appendix after bibliography @@ -13,7 +13,7 @@ TEMPLATE := template.tex # LaTeX template for producing PDF # Add src prefix to markdown files MARKDOWN := $(addprefix src/, $(MARKDOWN)) -MARKDOWN_COMPLUTENSE := $(addprefix src/, $(MARKDOWN_COMPLUTENSE)) +MARKDOWN_COMPLU := $(addprefix src/, $(MARKDOWN_COMPLU)) APPENDICES := $(addprefix src/, $(APPENDICES)) GRAPHS := $(wildcard graphs/*.tex) # Latex diagrams @@ -26,35 +26,60 @@ APPENDIX := appendix.tex all: $(PDF) +# Default pdf, no information about university, no extra complutense chapters, +# no cool font $(PDF): $(MARKDOWN) $(APPENDIX) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) pandoc --smart --standalone --latex-engine xelatex --template $(TEMPLATE) \ --bibliography $(BIBLIOGRAPHY) --csl $(CSL) --table-of-contents \ --top-level-division chapter --highlight-style breezedark \ --include-in-header question.tex \ - --metadata geometry:top=2.5cm,left=4cm,right=2.5cm,bottom=2.5cm \ - --metadata date:"$(shell date +%Y/%m/%d)" \ - --metadata sansfont:"Helvetica Neue LT Com" \ $(METADATA) $(MARKDOWN) --include-after-body $(APPENDIX) -o $@ -hertfordshire: $(MARKDOWN) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) +# Hertfordshire digital submission +herts: $(MARKDOWN) $(APPENDIX) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) pandoc --smart --standalone --latex-engine xelatex --template $(TEMPLATE) \ --bibliography $(BIBLIOGRAPHY) --csl $(CSL) --table-of-contents \ --top-level-division chapter --highlight-style breezedark \ --include-in-header question.tex \ --metadata geometry:top=2.5cm,left=4cm,right=2.5cm,bottom=2.5cm \ --metadata sansfont:"Helvetica Neue LT Com" \ - $(METADATA) $(MARKDOWN) -o pandoc_herts.pdf - - -complutense: $(MARKDOWN_COMPLUTENSE) $(APPENDIX) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) + --metadata institute:"University of Hertfordshire" \ + --metadata school:"School of Computer Science" \ + --metadata degree:"Modular BSc Honours in Computer Science" \ + --metadata degreecode:"6COM1054 - Artificial Intelligence Project" \ + --metadata supervisor:"Dr.Mariana Lilley" \ + $(METADATA) $(MARKDOWN) --include-after-body $(APPENDIX) \ + -o persimmon_herts.pdf + +# Hertfordshire print submission +herts_print: $(MARKDOWN) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) + pandoc --smart --standalone --latex-engine xelatex --template $(TEMPLATE) \ + --bibliography $(BIBLIOGRAPHY) --csl $(CSL) --table-of-contents \ + --top-level-division chapter --highlight-style breezedark \ + --metadata geometry:top=2.5cm,left=4cm,right=2.5cm,bottom=2.5cm \ + --metadata sansfont:"Helvetica Neue LT Com" \ + --metadata institute:"University of Hertfordshire" \ + --metadata school:"School of Computer Science" \ + --metadata degree:"Modular BSc Honours in Computer Science" \ + --metadata degreecode:"6COM1054 - Artificial Intelligence Project" \ + --metadata supervisor:"Dr.Mariana Lilley" \ + $(METADATA) $(MARKDOWN) -o persimmon_herts_print.pdf + +# Complutense digital submission +complu: $(MARKDOWN_COMPLU) $(APPENDIX) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) pandoc --smart --standalone --latex-engine xelatex --template $(TEMPLATE) \ --bibliography $(BIBLIOGRAPHY) --csl $(CSL) --table-of-contents \ --top-level-division chapter --highlight-style breezedark \ - --metadata title:"A scikit-learn visual programming interface" \ - --metadata date:"Director:Manuel Freire Moran, Codirector: Pablo Moreno Ger" \ - --metadata sansfont:"Helvetica Neue LT Com" --metadata keywords:"Serious Learning","Visual Programming","Dataflow Programming" \ - --metadata titlepic:images/fdi.png $(METADATA) $(MARKDOWN_COMPLUTENSE) \ - --include-after-body $(APPENDIX) -o $(PDF) + --include-in-header question.tex \ + --metadata sansfont:"Helvetica Neue LT Com" \ + --metadata institute:"Universidad Complutense" \ + --metadata school:"Facultad de Informática" \ + --metadata degree:"Computer Engineering" \ + --metadata degreecode:"Tecnología Específica de Computación" \ + --metadata supervisor:"Manuel Freire Moran" \ + --metadata cosupervisor:"Pablo Moreno Ger" \ + --metadata titlepic:images/fdi.png $(METADATA) $(MARKDOWN_COMPLU) \ + --include-after-body $(APPENDIX) -o persimmon_complu.pdf # For standalone images images/%.pdf: graphs/%.tex @@ -62,6 +87,7 @@ images/%.pdf: graphs/%.tex @mv $*.pdf images/ @rm -f $*.log $*.aux +# Appendices $(APPENDIX): $(APPENDICES) pandoc --smart --no-tex-ligatures --top-level-division chapter $(APPENDICES) -o $@ @@ -71,8 +97,8 @@ APPENDIX_TRAVIS := appendix_travis.tex travis: $(MARKDOWN) $(APPENDIX_TRAVIS) $(TEMPLATE) $(IMAGES) $(BIBLIOGRAPHY) $(CSL) $(METADATA) pandoc --smart --standalone --latex-engine xelatex --template $(TEMPLATE) \ --bibliography $(BIBLIOGRAPHY) --csl $(CSL) --table-of-contents \ - --chapters --highlight-style breezedark \ - --metadata date:"$(shell date +%Y/%m/%d)" \ + --chapters --highlight-style espresso \ + --include-in-header question.tex \ $(METADATA) $(MARKDOWN) --include-after-body $(APPENDIX_TRAVIS) \ -o $(PDF) @@ -81,5 +107,5 @@ $(APPENDIX_TRAVIS): $(APPENDICES) clean: - rm -f images/*.pdf $(PDF) *.log *.aux appendix* + rm -f images/*.pdf *.pdf *.log *.aux appendix* diff --git a/docs/images/final_aspect.png b/docs/images/final_aspect.png new file mode 100644 index 0000000..574de3c Binary files /dev/null and b/docs/images/final_aspect.png differ diff --git a/docs/images/full_use.gif b/docs/images/full_use.gif new file mode 100644 index 0000000..608b7b4 Binary files /dev/null and b/docs/images/full_use.gif differ diff --git a/docs/images/fully_automatic_workflow.png b/docs/images/fully_automatic_workflow.png deleted file mode 100644 index 078900c..0000000 Binary files a/docs/images/fully_automatic_workflow.png and /dev/null differ diff --git a/docs/images/type_safety.gif b/docs/images/type_safety.gif new file mode 100644 index 0000000..83b787f Binary files /dev/null and b/docs/images/type_safety.gif differ diff --git a/docs/images/weka.jpeg b/docs/images/weka.jpeg deleted file mode 100644 index 9892545..0000000 Binary files a/docs/images/weka.jpeg and /dev/null differ diff --git a/docs/metadata.yaml b/docs/metadata.yaml index a566d24..40a0771 100644 --- a/docs/metadata.yaml +++ b/docs/metadata.yaml @@ -1,18 +1,9 @@ --- -author: | - Á. Bermejo - Supervised by Mariana Lilley -title: | - University of Hertfordshire - School of Computer Science - Modular BSc Honours in Computer Science - 6COM105 - Artificial Intelligence Project -subtitle: | - Final Report - April 2017 - Persimmon -#date: 2017-03-01 +author: Álvaro Bermejo García +title: Persimmon +subtitle: A visual dataflow language for machine learning +date: \today documentclass: scrreprt colorlinks: True lof: True diff --git a/docs/question.tex b/docs/question.tex new file mode 100644 index 0000000..f3489e8 --- /dev/null +++ b/docs/question.tex @@ -0,0 +1,44 @@ +\def\question#1\par#2\par{\hbox to \hsize +{\vbox{\hsize=0.72\hsize #1\dotfill}\quad#2\hfil}\medskip\goodbreak} + +\newdimen\scalewidth +\scalewidth=0.3\hsize + +\def\xfamiliar{\xscalee{not familiar at all}{very familiar}} +\def\xagree{\xscalee{strongly disagree}{agree completely}} +\def\boxes{\fiveboxes{}{}{}{}{}\ignorespaces} +\def\xscale#1#2{% + \setbox0=\hbox{\boxes}% + \setbox1=\hbox to \wd0{\small\strut\hfill #2 $\to$}% + \setbox2=\hbox to \wd0{\small\strut $\gets$ #1 \hfill}% + \vbox{\vbox to 0pt{\vss\box1\box2\kern2pt}\vbox{\box0}}} + +\def\xdifficulty{\xscalee{very difficult}{very easy}} +\def\xscalee#1#2{% + \setbox0=\hbox{\boxees}% + \setbox1=\hbox to \wd0{\small\strut\hfill #2 $\to$}% + \setbox2=\hbox to \wd0{\small\strut $\gets$ #1 \hfill}% + \vbox{\vbox to 0pt{\vss\box1\box2\kern2pt}\vbox{\box0}}} +\def\boxees{\sevenboxes{}{}{}{}{}{}{}\ignorespaces} + +\def\boxit#1{\hbox{\lower0.7ex\vbox{\hrule\hbox{\vrule\kern1pt + \vbox{\kern1pt\hbox to 1.4em + {\small\strut\hfil #1\hfil}\kern1pt}\kern1pt\vrule}\hrule}}} + +\def\fiveboxes#1#2#3#4#5{\hbox to\scalewidth + {\boxit{#1}\hfil\boxit{#2}\hfil\boxit{#3}\hfil% + \boxit{#4}\hfil\boxit{#5}}} + +\def\sevenboxes#1#2#3#4#5#6#7{\hbox to\scalewidth + {\boxit{#1}\hfil\boxit{#2}\hfil\boxit{#3}\hfil% + \boxit{#4}\hfil\boxit{#5}\hfil\boxit{#6}\hfil\boxit{#7}}} + +\def\freequestion#1\par{#1\par\nobreak + \begingroup\nobreak + \advance\leftskip by 2pc + \hrule width 0pt height 1.7\baselineskip\hrulefill + \hrule width 0pt height 1.7\baselineskip\hrulefill + \par + \medskip + \endgroup + } diff --git a/docs/src/risk.md b/docs/src/risk.md index 426330a..6193982 100644 --- a/docs/src/risk.md +++ b/docs/src/risk.md @@ -49,7 +49,7 @@ refocus on ever-changing requirements. In the case of an unreachable goal partial objectives could established that would be easier to archive, splitting the main goal into several smaller goals, making it easier to at least accomplish some, if not all. -This is explored on the milestones chapter. +This is explored on the [milestones chapter](#tree). Performance issues can be countered reducing the data used for processing, making it more of a proof of concept while retaining the validity of the diff --git a/docs/src/types.md b/docs/src/types.md index 416e029..4dcdeb8 100644 --- a/docs/src/types.md +++ b/docs/src/types.md @@ -33,7 +33,7 @@ compile time (on the literature referred as write time). The two languages ----------------- -As seen on the previous sections and the implementation chapter Python and +As seen on the previous sections and the [implementation chapter](#implementation) Python and Persimmon are essentially two different languages, but just how different are they? diff --git a/docs/src/workflow.md b/docs/src/workflow.md index 3bcb13d..d27b125 100644 --- a/docs/src/workflow.md +++ b/docs/src/workflow.md @@ -61,7 +61,7 @@ interface being limited on the number of blocks, as not to allow the graphs to become inscrutable, and as mentioned on the introduction this also allows making assumptions about the interface which reduce the complexity such as not needing an explicit flow line, more on the explicit flow line can be read -in the implementation chapter. +in the [implementation chapter](#implementation). [^plc]: Programable Logic Controllers are industrial digital computers used for diff --git a/docs/template.tex b/docs/template.tex index 1fa3528..55fbe56 100644 --- a/docs/template.tex +++ b/docs/template.tex @@ -54,8 +54,8 @@ \IfFileExists{upquote.sty}{\usepackage{upquote}}{} % use microtype if available \IfFileExists{microtype.sty}{% -\usepackage{microtype} -\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts + \usepackage{microtype} + \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts }{} $if(geometry)$ \usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry} @@ -85,18 +85,18 @@ breaklinks=true} \urlstyle{same} % don't use monospace font for urls $if(lang)$ -\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex - \usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel} -$if(babel-newcommands)$ - $babel-newcommands$ -$endif$ -\else - \usepackage{polyglossia} - \setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$} -$for(polyglossia-otherlangs)$ - \setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$} -$endfor$ -\fi + \ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex + \usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel} + $if(babel-newcommands)$ + $babel-newcommands$ + $endif$ + \else + \usepackage{polyglossia} + \setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$} + $for(polyglossia-otherlangs)$ + \setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$} + $endfor$ + \fi $endif$ $if(natbib)$ \usepackage{natbib} @@ -183,7 +183,7 @@ % set default figure placement to htbp \makeatletter -\def\fps@figure{htbp} + \def\fps@figure{htbp} \makeatother $for(header-includes)$ @@ -191,31 +191,18 @@ $endfor$ $if(title)$ - $if(titlepic)$ - \usepackage{titlepic} - \titlepic{\includegraphics[width=\textwidth]{$titlepic$}} - $endif$ - \title{$title$$if(thanks)$\thanks{$thanks$}$endif$$if(subtitle)$% - \subtitle{$subtitle$}$endif$$if(institute)$\institute{$institute$}% - $endif$} - %\makeatletter - %\def\maketitle{ - % \newpage - % \null - % \vskip 2em% - % {\sffamily\Huge\bfseries \@title}\par% - % {\@author}\par}% - %\makeatother - % Let's make titles better - %\newfontfamily{\titlefont} - % [UprightFont = {* 43 Light Extended}, - % BoldFont = {* 107 Extra Black Condensed}]{Helvetica Neue LT Com} - %\usepackage{titling} - %\renewcommand{\maketitlehooka}{\sffamily} -%$endif$ -%$if(subtitle)$ - %\providecommand{\subtitle}[1]{} - %\subtitle{$subtitle$} +% $if(titlepic)$ +% \usepackage{titlepic} +% \titlepic{\includegraphics[width=\textwidth]{$titlepic$}} +% $endif$ +% \title{$title$$if(thanks)$\thanks{$thanks$}$endif$$if(subtitle)$% +% \subtitle{$subtitle$}$endif$$if(institute)$\institute{$institute$}% +% $endif$} + \title{$title$} +$endif$ +$if(subtitle)$ + \providecommand{\subtitle}[1]{} + \subtitle{$subtitle$} $endif$ $if(author)$ \author{$for(author)$$author$$sep$ \and $endfor$} @@ -224,7 +211,9 @@ \providecommand{\institute}[1]{} \institute{$for(institute)$$institute$$sep$ \and $endfor$} $endif$ -\date{$date$} +$if(date)$ + \date{$date$} +$endif$ % Make sections great again \usepackage{titlesec} @@ -232,45 +221,86 @@ \titleformat*{\section}{\sffamily\LARGE\bfseries} \titleformat*{\subsection}{\sffamily\Large\bfseries} \titleformat*{\subsubsection}{\sffamily\large\bfseries} -\titleformat*{\paragraph}{\sffamily\large\bfseries} -\titleformat*{\subparagraph}{\sffamily\large\bfseries} -\renewenvironment{abstract}{% +% Better abstract +\renewenvironment{abstract}{ \clearpage\small \null\vfil - \begin{center}% + \begin{center} {\bfseries \abstractname\vspace{-.5em}\vspace{0pt}}% - \end{center}% + \end{center} \quotation - } +} -\begin{document} - %$if(title)$ - % \maketitle - %$endif$ - %\begin{titlepage} - %\end{titlepage} +% Title page +\makeatletter +\renewcommand\maketitle{ \begin{titlepage} \centering - {\scshape\Large University of hertfordshire \par} + $if(institute)$ + {\scshape\Large $institute$ \par} + %{\scshape\Large \@institute \par} + $else$ + \null + $endif$ \vspace{0.25cm} - {\scshape\large School of Computer Science \par} + $if(school)$ + {\scshape\large $school$ \par} + $else$ + \null + $endif$ \vspace{1cm} - {\large\bfseries Modular BSc Honours in Computer Science \par} + $if(degree)$ + {\bfseries\large $degree$ \par} + $else$ + \null + $endif$ \vspace{1.5cm} - {\large\itshape 6COM1054 - Artificial Intelligence Project \par} + $if(degreecode)$ + {\itshape\large $degreecode$ \par} + $else$ + \null + $endif$ \vspace{2cm} - {\large Final Report\par} - {\large \today\par} + $if(date)$ + {\large \@date \par} + $else$ + \null + $endif$ \vfill - {\scshape\Large Persimmon: A visual dataflow language for machine learning pipelines \par} + {\scshape\Large \@title \par} + $if(subtitle)$ + {\scshape\Large \@subtitle \par} + $else$ + \null + $endif$ \vfill - { Á. Bermejo \par} + $if(author)$ + {\@author \par} + $else$ + \null + $endif$ \vspace{0.25cm} - { Supervised by Dr.Mariana Lilley \par} + $if(supervisor)$ + {Supervised by $supervisor$\par} + $else$ + \null + $endif$ + $if(cosupervisor)$ + {Cosupervised by $cosupervisor$\par} + $else$ + \null + $endif$ \vfill \end{titlepage} +} +\makeatother + +\begin{document} + $if(title)$ + \maketitle + $endif$ $if(abstract)$ \begin{abstract} $abstract$ diff --git a/persimmon/__main__.py b/persimmon/__main__.py index 170483a..a53f46e 100644 --- a/persimmon/__main__.py +++ b/persimmon/__main__.py @@ -1,8 +1,13 @@ -import os import sys -if hasattr(sys, '_MEIPASS') or getattr(sys, 'frozen', False): +if (len(sys.argv) > 1 and + (sys.argv[1] == '-d' or sys.argv[1] == '--debug')): + import coloredlogs + coloredlogs.DEFAULT_DATE_FORMAT = '%H:%M:%S' + coloredlogs.DEFAULT_LOG_FORMAT = '[%(asctime)s] %(name)s - %(message)s' + coloredlogs.install(level='DEBUG') +if hasattr(sys, '_MEIPASS'): + import os os.chdir(sys._MEIPASS) from persimmon.view import ViewApp - ViewApp().run() diff --git a/persimmon/backend/backend.py b/persimmon/backend/backend.py index a9f3ac8..2d197d2 100644 --- a/persimmon/backend/backend.py +++ b/persimmon/backend/backend.py @@ -1,6 +1,10 @@ from collections import deque, namedtuple +import threading +import logging +logger = logging.getLogger(__name__) + # backend types InputEntry = namedtuple('InputEntry', ['origin', 'pin', 'block']) BlockEntry = namedtuple('BlockEntry', ['inputs', 'function', 'outputs']) @@ -8,13 +12,23 @@ IR = namedtuple('IR', ['blocks', 'inputs', 'outputs']) def execute_graph(ir: IR, blackboard): + threading.Thread(target=execute_graph_parallel, + args=(ir, blackboard)).start() + +def execute_graph_parallel(ir: IR, blackboard): + """ Execution algorithm, introduces all blocks on a set, when a block is + executed it is taken out of the set until the set is empty. """ unexplored = set(ir.blocks.keys()) # All blocks are unexplored at start seen = {} # All output pins along their respectives values while unexplored: - unexplored, seen = execute_block(unexplored.pop(), ir, blackboard, unexplored, seen) - print('Done executing') + unexplored, seen = execute_block(unexplored.pop(), ir, blackboard, + unexplored, seen) + logger.info('Execution done') def execute_block(current: int, ir: IR, blackboard, unexplored: set, seen: {}) -> (set, {}): + """ Execute a block, if any dependency is not yet executed we + recurse into it first. """ + logger.debug('executing block {}'.format(current)) current_block = ir.blocks[current] for in_pin in map(lambda x: ir.inputs[x], current_block.inputs): origin = in_pin.origin @@ -26,6 +40,7 @@ def execute_block(current: int, ir: IR, blackboard, unexplored: set, seen: {}) - current_block.function() blackboard.on_block_executed(current) + logger.debug('block {} executed'.format(current)) for out_id in current_block.outputs: seen[out_id] = ir.outputs[out_id].pin.val diff --git a/persimmon/border.png b/persimmon/border.png new file mode 100644 index 0000000..c0a04b6 Binary files /dev/null and b/persimmon/border.png differ diff --git a/persimmon/connections.png b/persimmon/connections.png index 998723c..ac8b920 100644 Binary files a/persimmon/connections.png and b/persimmon/connections.png differ diff --git a/persimmon/libs/garden/garden.notification/LICENSE b/persimmon/libs/garden/garden.notification/LICENSE deleted file mode 100644 index ae712fb..0000000 --- a/persimmon/libs/garden/garden.notification/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Kivy Garden - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/persimmon/libs/garden/garden.notification/README.md b/persimmon/libs/garden/garden.notification/README.md deleted file mode 100644 index dee40fe..0000000 --- a/persimmon/libs/garden/garden.notification/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# garden.notification -:name_badge: A floating popup-like notification - - - -This widget provides a browser-like - -notification with all of the basic Kivy features available. You can use it -either in its default state, where is only basic title and message with some of -the color configuration, or you can input your own message layout in kv -language. The message string is available through ``app.message`` property. -For more such properties, read the code. - -## Features: - -- message scrolls -- available icon option -- title will be shortened if too long -- callback after the notification disappears -- stacking multiple notifs on top of each other -- markup turned on in title and message by default -- kv language input - -## TODO: - -- Ubuntu's Unity & OSX window hide implementation - (needed for hiding the window another python interpreter creates) -- grab window focus back - each notification steals focus from the main window - (linux & OSX) -- position relatively to the taskbar (or at least not on top of it) -- forbid notification to print Kivy initialisation logs to output - unless asked for it - -## Example: - -``` -from kivy.app import App -from functools import partial -from kivy.uix.button import Button -from kivy.resources import resource_find -from kivy.garden.notification import Notification - - -class Notifier(Button): - def __init__(self, **kwargs): - super(Notifier, self).__init__(**kwargs) - self.bind(on_release=self.show_notification) - - def printer(self, *args): - print(args) - - def show_notification(self, *args): - # open default notification - Notification().open( - title='Kivy Notification', - message='Hello from the other side?', - timeout=5, - icon=resource_find('data/logo/kivy-icon-128.png'), - on_stop=partial(self.printer, 'Notification closed') - ) - - # open notification with layout in kv - Notification().open( - title='Kivy Notification', - message="I'm a Button!", - kv="Button:\n text: app.message" - ) - - -class KivyNotification(App): - def build(self): - return Notifier() - - -if __name__ == '__main__': - KivyNotification().run() -``` diff --git a/persimmon/libs/garden/garden.notification/__init__.py b/persimmon/libs/garden/garden.notification/__init__.py deleted file mode 100644 index b48ff1e..0000000 --- a/persimmon/libs/garden/garden.notification/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -''' -# Copyright (c) 2013 Peter Badida (KeyWeeUsr) - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -.. |notification| replace:: notification -.. _notification: https://developer.mozilla.org/en-US/docs/Web/API/notification - -This widget provides a browser-like |notification|_ with all of the Kivy -features available. You can use it either in its default state, where is -only basic title and message with some of the color configuration, or -you can input your own message layout in kv language. The message string -is available through ``app.message`` property. - -Features: -- message scrolls -- available icon option -- title will be shortened if too long -- callback after the notification disappears -- stacking multiple notifs on top of each other -- markup turned on in title and message by default -- kv language input - -TODO: -- Ubuntu's Unity & OSX window hide implementation - (needed for hiding the window another python interpreter creates) -- grab window focus back - each notification steals focus from the main window - (linux & OSX) -- position relatively to the taskbar (or at least not on top of it) -- forbid notification to print Kivy initialisation logs to output - unless asked for it -''' - -import os -import sys -import threading -from subprocess import Popen -from os.path import dirname, abspath, join -from kivy.app import App - - -class Notification(object): - def open(self, title='Title', message='Message', icon=None, - width=300, height=100, offset_x=10, offset_y=40, - timeout=15, timeout_close=True, color=None, - line_color=None, background_color=None, parent_title=None, - stack=True, stack_offset_y=10, on_stop=None, kv=None): - - app = App.get_running_app() - - # set default colors - if not color: - color = (0, 0, 0, 1) - if not line_color: - line_color = (.2, .64, .81, .5) - if not background_color: - background_color = (.92, .92, .92, 1) - - # stacking on top of each other - if stack and not hasattr(app, '_gardennotification_count'): - app._gardennotification_count = 1 - elif stack and hasattr(app, '_gardennotification_count'): - inc = app._gardennotification_count - app._gardennotification_count += 1 - offset_y += (height + stack_offset_y) * inc - else: - app._gardennotification_count = 0 - - # Window name necessary for win32 api - if not parent_title: - parent_title = app.get_application_name() - - self.path = dirname(abspath(__file__)) - - # subprocess callback after the App dies - def popen_back(callback, on_stop, args): - # str(dict) is used for passing arguments to the child notification - # it might trigger an issue on some OS if a length of X characters - # is exceeded in the called console string, e.g. too long path - # to the interpreter executable, notification file, etc. - # https://support.microsoft.com/en-us/help/830473 - os.environ['KIVY_NO_FILELOG'] = '1' - p = Popen([ - sys.executable, - join(self.path, 'notification.py'), - args - ]) - p.wait() - callback() - if on_stop: - on_stop() - return - - # open Popen in Thread to wait for exit status - # then decrement the stacking variable - t = threading.Thread( - target=popen_back, args=( - self._decrement, - on_stop, - str({ - 'title': title, - 'message': message, - 'icon': icon, - 'kv': kv, - 'width': width, - 'height': height, - 'offset_x': offset_x, - 'offset_y': offset_y, - 'timeout': timeout, - 'timeout_close': timeout_close, - 'line_color': line_color, - 'color': color, - 'background_color': background_color, - 'parent_title': parent_title, - }))) - t.start() - - def _decrement(self): - app = App.get_running_app() - if hasattr(app, '_gardennotification_count'): - app._gardennotification_count -= 1 diff --git a/persimmon/libs/garden/garden.notification/notification.kv b/persimmon/libs/garden/garden.notification/notification.kv deleted file mode 100644 index 728f5e1..0000000 --- a/persimmon/libs/garden/garden.notification/notification.kv +++ /dev/null @@ -1,71 +0,0 @@ -#:import dp kivy.metrics.dp -#:import stopTouchApp kivy.base.stopTouchApp - -BoxLayout: - canvas: - Color: - rgba: app.background_color - Rectangle: - size: self.size - pos: self.pos - orientation: 'vertical' - - GridLayout: - cols: 2 - size_hint_y: 0.2 - canvas: - Color: - rgba: app.line_color - - Line: - points: - [self.pos[0], self.pos[1] + dp(1), - self.pos[0] + self.width, - self.pos[1] + dp(1)] - - Label: - color: app.color - text_size: - [self.width - dp(10), - self.height] - text: app.notif_title - halign: 'left' - markup: True - shorten: True - shorten_from: 'right' - - Button: - background_normal: - 'atlas://data/images/defaulttheme/bubble_btn' - color: app.color - size_hint_x: None - width: self.height - text: 'X' - on_release: stopTouchApp() - - GridLayout: - id: container - cols: 2 - size_hint_y: 0.8 - - FloatLayout: - size_hint_x: 0.3 - Image: - source: app.notif_icon - size_hint: (None, None) - width: dp(64) - height: dp(64) - pos_hint: {'center': (0.5, 0.5)} - - ScrollView: - Label: - color: app.color - text: app.message - text_size: - [self.width, - self.height] - size_hint_y: None - padding: (5, 5) - text_size: self.width, None - height: self.texture_size[1] - markup: True diff --git a/persimmon/libs/garden/garden.notification/notification.py b/persimmon/libs/garden/garden.notification/notification.py deleted file mode 100644 index e72dbff..0000000 --- a/persimmon/libs/garden/garden.notification/notification.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import sys -import traceback -from ast import literal_eval -from kivy.utils import platform -from subprocess import check_output -from os.path import dirname, abspath, join - -# waiting for https://github.com/kivy/plyer/pull/201 -if platform == 'win': - import ctypes - import win32gui - from win32con import ( - SW_HIDE, SW_SHOW, - GWL_EXSTYLE, WS_EX_TOOLWINDOW - ) - u32 = ctypes.windll.user32 - RESOLUTION = (u32.GetSystemMetrics(0), u32.GetSystemMetrics(1)) -elif platform == 'linux': - o = check_output('xrandr').decode('utf-8') - start = o.find('current') + 7 - end = o.find(', maximum') - RESOLUTION = [int(n) for n in o[start:end].split('x')] -elif platform == 'osx': - o = check_output(['system_profiler', 'SPDisplaysDataType']) - start = o.find('Resolution: ') - end = o.find('\n', start=start) - o = o[start:end].strip().split(' ') - RESOLUTION = (int(o[1]), int(o[3])) -else: - raise NotImplementedError("Not a desktop platform!") - - -KWARGS = literal_eval(sys.argv[1]) -WIDTH = KWARGS['width'] -HEIGHT = KWARGS['height'] -OFFSET = ( - KWARGS['offset_x'], - KWARGS['offset_y'] -) - -# set window from Notification.open arguments -from kivy.config import Config -Config.set('graphics', 'resizable', 0) -Config.set('graphics', 'borderless', 1) -Config.set('graphics', 'position', 'custom') -Config.set('graphics', 'width', WIDTH) -Config.set('graphics', 'height', HEIGHT) -Config.set( - 'graphics', 'left', - RESOLUTION[0] - WIDTH - OFFSET[0] -) -Config.set( - 'graphics', 'top', - RESOLUTION[1] - HEIGHT - OFFSET[1] -) - -from kivy.app import App -from kivy.clock import Clock -from kivy.lang import Builder -from kivy.logger import Logger -from kivy.properties import StringProperty, ListProperty - - -class Notification(App): - title = StringProperty(KWARGS['title'].replace(' ', '') + str(os.getpid())) - notif_title = StringProperty(KWARGS['title']) - message = StringProperty(KWARGS['message']) - notif_icon = StringProperty(KWARGS['icon']) - background_color = ListProperty(KWARGS['background_color']) - line_color = ListProperty(KWARGS['line_color']) - color = ListProperty(KWARGS['color']) - - def build(self): - if not self.notif_icon: - self.notif_icon = self.get_application_icon() - Clock.schedule_once(self._hide_window, 0) - if KWARGS['timeout_close']: - Clock.schedule_once(self.stop, KWARGS['timeout']) - if KWARGS['kv']: - path = dirname(abspath(__file__)) - kv = Builder.load_file(join(path, 'notification.kv')) - kv.ids.container.clear_widgets() - kv.ids.container.add_widget( - Builder.load_string(KWARGS['kv']) - ) - return kv - - def _hide_window(self, *args): - if platform == 'win': - self._hide_w32_window() - elif platform in ('linux', 'osx'): - self._hide_x11_window() - - def _hide_w32_window(self): - try: - w32win = win32gui.FindWindow(None, self.title) - win32gui.ShowWindow(w32win, SW_HIDE) - win32gui.SetWindowLong( - w32win, - GWL_EXSTYLE, - win32gui.GetWindowLong( - w32win, GWL_EXSTYLE) | WS_EX_TOOLWINDOW - ) - win32gui.ShowWindow(w32win, SW_SHOW) - self._return_focus_w32() - except Exception: - tb = traceback.format_exc() - Logger.error( - 'Notification: An error occured in {}\n' - '{}'.format(self.title, tb) - ) - - def _hide_x11_window(self): - try: - # Ubuntu's Unity for some reason ignores this if there are - # multiple windows stacked to a single icon on taskbar. - # Unity probably calls the same thing to stack the running - # programs to a single icon, which makes this command worthless. - x11_command = [ - 'xprop', '-name', '{}'.format(self.title), '-f', - '_NET_WM_STATE', '32a', '-set', '_NET_WM_STATE', - '_NET_WM_STATE_SKIP_TASKBAR' - ] - check_output(x11_command) - except Exception as e: - tb = traceback.format_exc() - Logger.error( - 'Notification: An error occured in {}\n' - '{}'.format(self.title, tb) - ) - - def _return_focus_w32(self): - w32win = win32gui.FindWindow(None, KWARGS['parent_title']) - win32gui.SetForegroundWindow(w32win) - - -if __name__ == '__main__': - Notification().run() diff --git a/persimmon/libs/garden/garden.notification/screenshot.png b/persimmon/libs/garden/garden.notification/screenshot.png deleted file mode 100644 index 787921d..0000000 Binary files a/persimmon/libs/garden/garden.notification/screenshot.png and /dev/null differ diff --git a/persimmon/view/blocks/__init__.py b/persimmon/view/blocks/__init__.py index 3adf1d9..62ef5ab 100644 --- a/persimmon/view/blocks/__init__.py +++ b/persimmon/view/blocks/__init__.py @@ -1,5 +1,7 @@ from .block import Block from .svmblock import SVMBlock +from .knnblock import KNNBlock +from .sgdblock import SGDBlock from .fitblock import FitBlock from .dictblock import DictBlock from .printblock import PrintBlock @@ -10,4 +12,4 @@ from .gridsearchblock import GridSearchBlock from .randomforestblock import RandomForestBlock from .crossvalidationblock import CrossValidationBlock - +from .tssplitblock import TSSplitBlock diff --git a/persimmon/view/blocks/block.kv b/persimmon/view/blocks/block.kv index ad1de2e..d860d28 100644 --- a/persimmon/view/blocks/block.kv +++ b/persimmon/view/blocks/block.kv @@ -5,20 +5,26 @@ drag_rectangle: self.x, self.y, self.width, self.height drag_timeout: 10000000 drag_distance: 0 - canvas.before: + canvas: Color: rgb: .2, .2, .2 Rectangle: pos: self.pos size: self.size - + #canvas.before: + #Color: + #rgb: 1, 1, 1 + #BorderImage: + #pos: self.x - 5, self.y - 5 + #size: self.width + 10, self.height + 10 + #source: 'tex2.png' # Children Label: pos: root.x, root.y + root.height - 20 size_hint: None, None height: '20dp' width: root.width - text: root.block_label + text: root.title canvas.before: Color: rgb: root.block_color diff --git a/persimmon/view/blocks/block.py b/persimmon/view/blocks/block.py index e602289..1fa281b 100644 --- a/persimmon/view/blocks/block.py +++ b/persimmon/view/blocks/block.py @@ -1,16 +1,22 @@ +# Persimmon stuff +from persimmon.view.util import Type, BlockType, Pin, AbstractWidget +# kivy stuff from kivy.uix.floatlayout import FloatLayout from kivy.uix.behaviors import DragBehavior from kivy.properties import ListProperty, StringProperty, ObjectProperty from kivy.lang import Builder - -from persimmon.view.util import Type, BlockType +from kivy.graphics import BorderImage, Color +from kivy.uix.image import Image +# Types are fun +from typing import Optional +from abc import abstractmethod Builder.load_file('view/blocks/block.kv') -class Block(DragBehavior, FloatLayout): +class Block(DragBehavior, FloatLayout, metaclass=AbstractWidget): block_color = ListProperty([1, 1, 1]) - block_label = StringProperty('label') + title = StringProperty() inputs = ObjectProperty() outputs = ObjectProperty() input_pins = ListProperty() @@ -28,29 +34,44 @@ def __init__(self, **kwargs): for pin in self.outputs.children: self.output_pins.append(pin) pin.block = self - self.tainted_msg = '' + self.tainted_msg = 'Block {} has unconnected inputs'.format(self.title) self._tainted = False + self.kindled = None + self.border_texture = Image(source='border.png').texture @property def tainted(self): - return self._tainted + # TODO: Check for orphanhood is not necessary + return (self._tainted or (not self.is_orphan() and + any(in_pin.origin == None for in_pin in self.input_pins))) @tainted.setter def tainted(self, value): self._tainted = value - def in_pin(self, x, y): + def is_orphan(self) -> bool: + """ Tells if a block is orphan, i.e. whether it has any connection """ + for in_pin in self.input_pins: + if in_pin.origin: + return False + for out_pin in self.output_pins: + if out_pin.destinations: + return False + return True + + def in_pin(self, x: float, y: float) -> Optional[Pin]: + """ Checks if a position collides with any of the pins in the block. + """ for pin in self.input_pins + self.output_pins: if pin.collide_point(x, y): return pin return None + @abstractmethod def function(self): - pass - - def pin_relative_position(self, pin): - return pin.center + raise NotImplementedError + # Kivy touch events override def on_touch_down(self, touch): pin = self.in_pin(*touch.pos) if pin: # if touch is on pin let them handle @@ -66,3 +87,26 @@ def on_touch_up(self, touch): result = super().on_touch_up(touch) return result + def kindle(self): + """ Praise the sun \[T]/ """ + with self.canvas.before: + Color(1, 1, 1) + self.kindled = BorderImage(pos=(self.x - 5, self.y - 5), + size=(self.width + 10, + self.height + 10), + texture=self.border_texture) + self.fbind('pos', self._bind_border) + + def unkindle(self): + """ Reverts the border image. """ + if self.kindled: + self.canvas.before.remove(self.kindled) + self.funbind('pos', self._bind_border) + self.kindled = None + else: + logger.warning('Called unkindle on a block not kindled') + + # Auxiliary functions + def _bind_border(self, block, new_pos): + """ Bind border to position. """ + self.kindled.pos = new_pos[0] - 5, new_pos[1] - 5 diff --git a/persimmon/view/blocks/crossvalidationblock.kv b/persimmon/view/blocks/crossvalidationblock.kv index 5fa1a3b..aac134c 100644 --- a/persimmon/view/blocks/crossvalidationblock.kv +++ b/persimmon/view/blocks/crossvalidationblock.kv @@ -1,5 +1,5 @@ : - block_label: 'Cross Validation' + title: 'Cross Validation' block_color: root.b.MODEL_SELECTION.value inputs: inputs outputs: outputs diff --git a/persimmon/view/blocks/csvinblock.kv b/persimmon/view/blocks/csvinblock.kv index a2a980a..b44fdb6 100644 --- a/persimmon/view/blocks/csvinblock.kv +++ b/persimmon/view/blocks/csvinblock.kv @@ -1,6 +1,6 @@ : out_1: out_1 - block_label: 'Read .csv' + title: 'Read .csv' block_color: root.b.IO.value outputs: outputs Button: diff --git a/persimmon/view/blocks/csvinblock.py b/persimmon/view/blocks/csvinblock.py index f479c60..600916a 100644 --- a/persimmon/view/blocks/csvinblock.py +++ b/persimmon/view/blocks/csvinblock.py @@ -21,13 +21,10 @@ def __init__(self, **kwargs): # This binds two properties together self.file_dialog.bind(file_chosen=self.setter('file_chosen')) self.tainted = True - self.tainted_msg = 'File not chosen in block {}!'.format(self.block_label) + self.tainted_msg = 'File not chosen in block {}!'.format(self.title) def function(self): self.out_1.val = pd.read_csv(self.file_chosen, header=0) def on_file_chosen(self, instance, value): - if value.endswith('.csv'): - self.tainted = False - else: - self.tainted = True + self.tainted = not value.endswith('.csv') diff --git a/persimmon/view/blocks/csvoutblock.kv b/persimmon/view/blocks/csvoutblock.kv index 510e0b0..6e99fa4 100644 --- a/persimmon/view/blocks/csvoutblock.kv +++ b/persimmon/view/blocks/csvoutblock.kv @@ -1,6 +1,6 @@ : in_1: in_1 - block_label: 'Write .csv' + title: 'Write .csv' block_color: root.b.IO.value inputs: inputs Button: diff --git a/persimmon/view/blocks/csvoutblock.py b/persimmon/view/blocks/csvoutblock.py index 03e0021..4975534 100644 --- a/persimmon/view/blocks/csvoutblock.py +++ b/persimmon/view/blocks/csvoutblock.py @@ -3,6 +3,7 @@ from kivy.properties import ObjectProperty, StringProperty from kivy.lang import Builder + import numpy as np import pandas as pd @@ -21,8 +22,7 @@ def __init__(self, **kwargs): # Let's bind two together self.file_dialog.bind(file_chosen=self.setter('path')) self.tainted = True - self.tainted_msg = 'File not chosen in block {}!'.format(self.block_label) - + self.tainted_msg = 'File not chosen in block {}!'.format(self.title) def function(self): if type(self.in_1.val) == np.ndarray: @@ -30,7 +30,5 @@ def function(self): self.in_1.val.to_csv(path_or_buf=self.path, index=False) def on_path(self, instance, value): - if value.endswith('.csv'): - self.tainted = False - else: - self.tainted = True + self.tainted = not value.endswith('.csv') + diff --git a/persimmon/view/blocks/dictblock.kv b/persimmon/view/blocks/dictblock.kv index 77d6480..463456f 100644 --- a/persimmon/view/blocks/dictblock.kv +++ b/persimmon/view/blocks/dictblock.kv @@ -1,5 +1,5 @@ : - block_label: 'Dictionary' + title: 'Dictionary' block_color: root.b.STATE.value outputs: outputs dict_out: dict_out diff --git a/persimmon/view/blocks/dictblock.py b/persimmon/view/blocks/dictblock.py index c155a47..71b5342 100644 --- a/persimmon/view/blocks/dictblock.py +++ b/persimmon/view/blocks/dictblock.py @@ -15,7 +15,8 @@ class DictBlock(Block): def __init__(self, **kwargs): super().__init__(**kwargs) self.tainted = True - self.tainted_msg = 'Dictionary not inputed on block {}!'.format(self.block_label) + self.tainted_msg = ('Dictionary not inputed on block {}!' + .format(self.title)) # TODO: Perform this check at unfocus time @Block.tainted.getter @@ -26,12 +27,13 @@ def tainted(self): self.tainted = False else: self.tainted = True - self.tainted_msg = 'Block {} requires a dictionary, not a {}!'.format( - self.block_label, type(string).__name__) + self.tainted_msg = ('Block {} requires a dictionary, not a {}!' + .format(self.title, + type(string).__name__)) except Exception: self.tainted = True - self.tainted_msg = 'Invalid input on block {}'.format( - self.block_label) + self.tainted_msg = ('Invalid input on block {}' + .format(self.title)) return self._tainted def function(self): diff --git a/persimmon/view/blocks/fitblock.kv b/persimmon/view/blocks/fitblock.kv index 5c6de15..7d899c0 100644 --- a/persimmon/view/blocks/fitblock.kv +++ b/persimmon/view/blocks/fitblock.kv @@ -1,5 +1,5 @@ : - block_label: 'Fit' + title: 'Fit' block_color: root.b.FIT_AND_PREDICT.value inputs: inputs outputs: outputs diff --git a/persimmon/view/blocks/gridsearchblock.kv b/persimmon/view/blocks/gridsearchblock.kv index 479c4ea..09d419f 100644 --- a/persimmon/view/blocks/gridsearchblock.kv +++ b/persimmon/view/blocks/gridsearchblock.kv @@ -1,5 +1,5 @@ : - block_label: 'Grid Search' + title: 'Grid Search' block_color: root.b.MODEL_SELECTION.value inputs: inputs outputs: outputs diff --git a/persimmon/view/blocks/knnblock.kv b/persimmon/view/blocks/knnblock.kv new file mode 100644 index 0000000..29eecc6 --- /dev/null +++ b/persimmon/view/blocks/knnblock.kv @@ -0,0 +1,12 @@ +: + title: 'KNN' + block_color: root.b.CLASSIFICATOR.value + outputs: outputs + est_out: est_out + Widget: + id: outputs + OutputPin: + id: est_out + x: root.x + root.width - 10 - 5 + y: root.y + root.height / 2 + _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/knnblock.py b/persimmon/view/blocks/knnblock.py new file mode 100644 index 0000000..643ae60 --- /dev/null +++ b/persimmon/view/blocks/knnblock.py @@ -0,0 +1,16 @@ +from persimmon.view.blocks import Block +from persimmon.view.util import OutputPin + +from kivy.properties import ObjectProperty +from kivy.lang import Builder + +from sklearn.neighbors import KNeighborsClassifier + + +Builder.load_file('view/blocks/knnblock.kv') + +class KNNBlock(Block): + est_out = ObjectProperty() + + def function(self): + self.est_out.val = KNeighborsClassifier() diff --git a/persimmon/view/blocks/predictblock.kv b/persimmon/view/blocks/predictblock.kv index e01e101..1ff9dbc 100644 --- a/persimmon/view/blocks/predictblock.kv +++ b/persimmon/view/blocks/predictblock.kv @@ -1,5 +1,5 @@ : - block_label: 'Predict' + title: 'Predict' block_color: root.b.FIT_AND_PREDICT.value inputs: inputs outputs: outputs diff --git a/persimmon/view/blocks/printblock.kv b/persimmon/view/blocks/printblock.kv index f7ff1fc..0182fd9 100644 --- a/persimmon/view/blocks/printblock.kv +++ b/persimmon/view/blocks/printblock.kv @@ -1,6 +1,6 @@ : in_1: in_1 - block_label: 'Print' + title: 'Print' block_color: root.b.IO.value inputs: inputs Widget: diff --git a/persimmon/view/blocks/randomforestblock.kv b/persimmon/view/blocks/randomforestblock.kv index 89c6795..7ca63cc 100644 --- a/persimmon/view/blocks/randomforestblock.kv +++ b/persimmon/view/blocks/randomforestblock.kv @@ -1,8 +1,8 @@ : - out_1: out_1 - block_label: 'Random Forest' + title: 'Random Forest' block_color: root.b.CLASSIFICATOR.value outputs: outputs + out_1: out_1 Widget: id: outputs OutputPin: diff --git a/persimmon/view/blocks/sgdblock.kv b/persimmon/view/blocks/sgdblock.kv new file mode 100644 index 0000000..9e60958 --- /dev/null +++ b/persimmon/view/blocks/sgdblock.kv @@ -0,0 +1,12 @@ +: + title: 'Stochastic Gradient Descent' + block_color: root.b.CLASSIFICATOR.value + outputs: outputs + est_out: est_out + Widget: + id: outputs + OutputPin: + id: est_out + x: root.x + root.width - 10 - 5 + y: root.y + root.height / 2 + _type: root.t.CLASSIFICATOR diff --git a/persimmon/view/blocks/sgdblock.py b/persimmon/view/blocks/sgdblock.py new file mode 100644 index 0000000..0de9149 --- /dev/null +++ b/persimmon/view/blocks/sgdblock.py @@ -0,0 +1,15 @@ +from persimmon.view.blocks import Block +from persimmon.view.util import OutputPin + +from kivy.properties import ObjectProperty +from kivy.lang import Builder + +from sklearn.linear_model import SGDClassifier + +Builder.load_file('view/blocks/sgdblock.kv') + +class SGDBlock(Block): + est_out = ObjectProperty() + + def function(self): + self.est_out.val = SGDClassifier() diff --git a/persimmon/view/blocks/svmblock.kv b/persimmon/view/blocks/svmblock.kv index 9ae83ca..b2eca48 100644 --- a/persimmon/view/blocks/svmblock.kv +++ b/persimmon/view/blocks/svmblock.kv @@ -1,6 +1,6 @@ : + title: 'SVM' out_1: out_1 - block_label: 'SVM' block_color: root.b.CLASSIFICATOR.value outputs: outputs Widget: diff --git a/persimmon/view/blocks/tenfoldblock.kv b/persimmon/view/blocks/tenfoldblock.kv index 46f9c70..6724279 100644 --- a/persimmon/view/blocks/tenfoldblock.kv +++ b/persimmon/view/blocks/tenfoldblock.kv @@ -1,6 +1,6 @@ : + title: 'K-fold' out_1: out_1 - block_label: '10-fold' block_color: root.b.CROSS_VALIDATOR.value outputs: outputs Widget: diff --git a/persimmon/view/blocks/tenfoldblock.py b/persimmon/view/blocks/tenfoldblock.py index 33bc7b0..2034367 100644 --- a/persimmon/view/blocks/tenfoldblock.py +++ b/persimmon/view/blocks/tenfoldblock.py @@ -13,4 +13,4 @@ class TenFoldBlock(Block): out_1 = ObjectProperty() def function(self): - self.out_1.val = KFold(n_splits=10) + self.out_1.val = KFold() diff --git a/persimmon/view/blocks/tssplitblock.kv b/persimmon/view/blocks/tssplitblock.kv new file mode 100644 index 0000000..6e463e8 --- /dev/null +++ b/persimmon/view/blocks/tssplitblock.kv @@ -0,0 +1,12 @@ +: + title: 'Time Series Split' + out_1: out_1 + block_color: root.b.CROSS_VALIDATOR.value + outputs: outputs + Widget: + id: outputs + OutputPin: + id: out_1 + x: root.x + root.width - 10 - 5 + y: root.y + root.height / 2 + _type: root.t.CROSS_VALIDATOR diff --git a/persimmon/view/blocks/tssplitblock.py b/persimmon/view/blocks/tssplitblock.py new file mode 100644 index 0000000..90dfe4d --- /dev/null +++ b/persimmon/view/blocks/tssplitblock.py @@ -0,0 +1,16 @@ +from persimmon.view.util import InputPin, OutputPin +from persimmon.view.blocks import Block + +from kivy.lang import Builder +from kivy.properties import ObjectProperty + +from sklearn.model_selection import TimeSeriesSplit + + +Builder.load_file('view/blocks/tssplitblock.kv') + +class TSSplitBlock(Block): + out_1 = ObjectProperty() + + def function(self): + self.out_1.val = TimeSeriesSplit() diff --git a/persimmon/view/util/__init__.py b/persimmon/view/util/__init__.py index be09402..46ac646 100644 --- a/persimmon/view/util/__init__.py +++ b/persimmon/view/util/__init__.py @@ -2,7 +2,7 @@ from .connection import Connection from .filedialog import FileDialog from .notification import Notification -from .types import Type, BlockType +from .types import Type, BlockType, AbstractWidget from .pin import Pin from .inpin import InputPin diff --git a/persimmon/view/util/connection.py b/persimmon/view/util/connection.py index 75b4d7c..305da05 100644 --- a/persimmon/view/util/connection.py +++ b/persimmon/view/util/connection.py @@ -1,12 +1,14 @@ +# Kivy stuff from kivy.uix.widget import Widget from kivy.lang import Builder from kivy.properties import ObjectProperty, ListProperty from kivy.graphics import Color, Ellipse, Line -from functools import partial from kivy.clock import Clock -from time import sleep +# Numpy for sin import numpy as np +# Others from math import pi +import logging """ @@ -25,6 +27,8 @@ #points: root.lin """ +logger = logging.getLogger(__name__) + class Connection(Widget): start = ObjectProperty(allownone=True) end = ObjectProperty(allownone=True) @@ -140,7 +144,7 @@ def uncircle_pin(self, pin): elif pin == self.end: self.canvas.before.remove(self.end_cr) else: - print('Attempted to uncircle pin without circle') + logger.error('Attempted to uncircle pin without circle') def circle_bind(self, pin, new_pos): if pin == self.start: @@ -148,7 +152,7 @@ def circle_bind(self, pin, new_pos): elif pin == self.end: self.end_cr.pos = pin.pos else: - print('No circle associated with pin') + logger.error('No circle associated with pin') def line_bind(self, pin, new_pos): if pin == self.start: @@ -156,14 +160,14 @@ def line_bind(self, pin, new_pos): elif pin == self.end: self.lin.points = self.lin.points[:2] + pin.center else: - print('No line associated with pin') + logger.error('No line associated with pin') def warn(self): self.warned = True self.canvas.before.remove(self.lin) with self.canvas.before: Color(1, 0, 0) - self.lin = Line(points=self.lin.points, width=2) + self.lin = Line(points=self.lin.points, width=3) def unwarn(self): self.warned = False @@ -174,6 +178,7 @@ def unwarn(self): def pulse(self): self.it = self._change_width() + next(self.it) Clock.schedule_interval(lambda _: next(self.it), 0.05) # 20 FPS def stop_pulse(self): diff --git a/persimmon/view/util/inpin.py b/persimmon/view/util/inpin.py index 55f92d0..99f31e0 100644 --- a/persimmon/view/util/inpin.py +++ b/persimmon/view/util/inpin.py @@ -1,13 +1,16 @@ from persimmon.view.util import Pin, Connection from kivy.properties import ObjectProperty +import logging +logger = logging.getLogger(__name__) + class InputPin(Pin): origin = ObjectProperty(allownone=True) def on_touch_down(self, touch): if self.collide_point(*touch.pos) and touch.button == 'left': - print('Creating connection') + logger.info('Creating connection') touch.ud['cur_line'] = Connection(start=self, color=self.color) self.origin = touch.ud['cur_line'] @@ -21,18 +24,18 @@ def on_touch_up(self, touch): if ('cur_line' in touch.ud.keys() and touch.button == 'left' and self.collide_point(*touch.pos)): if touch.ud['cur_line'].end and self.typesafe(touch.ud['cur_line'].end): - print('Establishing connection') + logger.info('Establishing connection') touch.ud['cur_line'].finish_connection(self) self.origin = touch.ud['cur_line'] else: - print('Deleting connection') + logger.info('Deleting connection') touch.ud['cur_line'].delete_connection(self.block.parent.parent) return True else: return False - def on_connection_delete(self, connection): + def on_connection_delete(self, connection: Connection): self.origin = None - def typesafe(self, other): + def typesafe(self, other: Pin) -> bool: return super().typesafe(other) and self.origin == None diff --git a/persimmon/view/util/outpin.py b/persimmon/view/util/outpin.py index 74ea001..1103c7c 100644 --- a/persimmon/view/util/outpin.py +++ b/persimmon/view/util/outpin.py @@ -1,13 +1,17 @@ from persimmon.view.util import Pin, Connection from kivy.properties import ObjectProperty, ListProperty +import logging + + +logger = logging.getLogger(__name__) class OutputPin(Pin): destinations = ListProperty() def on_touch_down(self, touch): if self.collide_point(*touch.pos) and touch.button == 'left': - print('Creating connection') + logger.info('Creating connection') touch.ud['cur_line'] = Connection(end=self, color=self.color) self.destinations.append(touch.ud['cur_line']) @@ -18,15 +22,14 @@ def on_touch_down(self, touch): return False def on_touch_up(self, touch): - #print('on touch up') if ('cur_line' in touch.ud.keys() and touch.button == 'left' and self.collide_point(*touch.pos)): if touch.ud['cur_line'].start and self.typesafe(touch.ud['cur_line'].start): - print('Establishing connection') + logger.info('Establishing connection') touch.ud['cur_line'].finish_connection(self) self.destinations.append(touch.ud['cur_line']) else: - print('Deleting connection') + logger.info('Deleting connection') touch.ud['cur_line'].delete_connection(self.block.parent.parent) return True else: diff --git a/persimmon/view/util/pin.py b/persimmon/view/util/pin.py index 8654035..cea652a 100644 --- a/persimmon/view/util/pin.py +++ b/persimmon/view/util/pin.py @@ -3,12 +3,13 @@ from kivy.properties import ObjectProperty from kivy.lang import Builder from kivy.graphics import Color, Ellipse, Line -from persimmon.view.util import Type +from persimmon.view.util import Type, AbstractWidget +from abc import abstractmethod Builder.load_file('view/util/pin.kv') -class Pin(CircularButton): +class Pin(CircularButton, metaclass=AbstractWidget): val = ObjectProperty(None, force_dispatch=True) block = ObjectProperty() ellipse = ObjectProperty() @@ -19,17 +20,20 @@ def on__type(self, instance, value): """ If the kv lang was a bit smarted this would not be needed """ self.color = value.value - + + @abstractmethod def on_touch_down(self, touch): - pass - + raise NotImplementedError + + @abstractmethod def on_touch_up(self, touch): - pass - - def on_connection_delete(self, connection): - pass + raise NotImplementedError + + @abstractmethod + def on_connection_delete(self, connection: Connection): + raise NotImplementedError - def typesafe(self, other): + def typesafe(self, other: 'Pin') -> bool: if ((self._type == Type.ANY or other._type == Type.ANY) and self.block != other.block and self.__class__ != other.__class__): return True # Anything is possible with ANY diff --git a/persimmon/view/util/types.py b/persimmon/view/util/types.py index 0cbc37e..5bee449 100644 --- a/persimmon/view/util/types.py +++ b/persimmon/view/util/types.py @@ -1,6 +1,13 @@ from enum import Enum +from abc import ABCMeta +from kivy.uix.widget import WidgetMetaclass +class AbstractWidget(ABCMeta, WidgetMetaclass): + """ Necessary because python meta classes do not support multiple + inheritance. """ + pass + class Type(Enum): ANY = 0.9, 0.9, 0.9 DATAFRAME = .667, .224, .224 diff --git a/persimmon/view/view.kv b/persimmon/view/view.kv index a2d7c9f..8da4438 100644 --- a/persimmon/view/view.kv +++ b/persimmon/view/view.kv @@ -21,19 +21,25 @@ FloatLayout: size_hint: None, None size: '100dp', '100dp' pos: 0, 0 - on_release: blackboard.process() + on_release: blackboard.execute_graph() Button: text: 'See Relations' size_hint: None, None size: '100dp', '100dp' pos: 200, 0 - on_release: blackboard.see_relations() + on_release: + blackboard.popup.title = 'Relations' + blackboard.popup.message = blackboard.get_relations(); + blackboard.popup.open() Button: text: 'See IR' size_hint: None, None size: '100dp', '100dp' pos: 300, 0 - on_release: blackboard.see_ir() + on_release: + blackboard.popup.title = 'IR' + blackboard.popup.message = str(blackboard.to_ir()) + blackboard.popup.open() TabbedPanel: pos_hint: {'top': 1} size_hint_y: None @@ -67,8 +73,10 @@ FloatLayout: on_release: blackboard.blocks.add_widget(b.SVMBlock(pos=(300, 250))) Button: text: 'KNN' + on_release: blackboard.blocks.add_widget(b.KNNBlock(pos=(300, 250))) Button: text: 'SGD' + on_release: blackboard.blocks.add_widget(b.SGDBlock(pos=(300, 250))) TabbedPanelItem: text: 'Fit & Predict' BoxLayout: @@ -101,6 +109,7 @@ FloatLayout: on_release: blackboard.blocks.add_widget(b.TenFoldBlock(pos=(300, 250))) Button: text: 'Time Series Split' + on_release: blackboard.blocks.add_widget(b.TSSplitBlock(pos=(300, 250))) TabbedPanelItem: text: 'State' BoxLayout: @@ -110,8 +119,8 @@ FloatLayout: text: 'Dictionary' on_release: blackboard.blocks.add_widget(b.DictBlock(pos=(300, 250))) - : blocks: blocks Widget: id: blocks + diff --git a/persimmon/view/view.py b/persimmon/view/view.py index 10a5afe..95dd62a 100644 --- a/persimmon/view/view.py +++ b/persimmon/view/view.py @@ -1,33 +1,33 @@ +# Persimmon imports +from persimmon.view import blocks +from persimmon.view.util import Notification +import persimmon.backend as backend +# Kivy imports from kivy.app import App -# Widgets +from kivy.lang import Builder +from kivy.clock import Clock, mainthread +from kivy.config import Config +from kivy.properties import ObjectProperty +from kivy.core.window import Window +# Kivy Widgets +from kivy.uix.popup import Popup +from kivy.uix.label import Label from kivy.uix.image import Image from kivy.uix.widget import Widget from kivy.uix.button import Button -from kivy.uix.popup import Popup -from kivy.uix.label import Label from kivy.uix.tabbedpanel import TabbedPanel from kivy.uix.floatlayout import FloatLayout from kivy.uix.scatterlayout import ScatterLayout -# Properties -from kivy.properties import (ObjectProperty, NumericProperty, StringProperty, - ListProperty) -# Miscelaneous -from kivy.config import Config -from kivy.graphics import Color, Ellipse, Line, Rectangle, Bezier -from kivy.core.window import Window -from functools import partial - -from persimmon.view import blocks -from persimmon.view.util import (CircularButton, InputPin, OutputPin, - Notification) - +# Others +from functools import partial, reduce from collections import deque -import persimmon.backend as backend -from kivy.lang import Builder -from kivy.clock import Clock -import threading +import logging +from typing import Optional +from persimmon.view.blocks import Block +from itertools import chain +logger = logging.getLogger(__name__) Config.read('config.ini') class ViewApp(App): @@ -43,11 +43,90 @@ def build(self): class BlackBoard(ScatterLayout): blocks = ObjectProperty() - popup = ObjectProperty(Notification(title='')) + popup = ObjectProperty(Notification()) + + def execute_graph(self): + """ Tries to execute the graph, if some block is tainted it prevents + the execution, if not it starts running the backend. """ + logger.debug('Checking taint') + # Check if any block is tainted + if any(map(lambda block: block.tainted, self.blocks.children)): + # Get tainted block + tainted_block = reduce(lambda l, r: l if l.tainted else r, + self.blocks.children) + logger.debug('Some block is tainted') + self.popup.title = 'Warning' + self.popup.message = tainted_block.tainted_msg + self.popup.open() + else: + logger.debug('No block is tainted') + for block in self.blocks.children: + if block.kindled: + block.unkindle() + backend.execute_graph(self.to_ir(), self) + + def get_relations(self) -> str: + """ Gets the relations between pins as a string. """ + # generator expressions are cool + ins = ('{} -> {}\n'.format(block.title, in_pin.origin.end.block.title) + for block in self.blocks.children + for in_pin in block.input_pins if in_pin.origin) + outs = ('{} <- {}\n'.format(block.title, destination.start.block.title) + for block in self.blocks.children + for out_pin in block.output_pins + for destination in out_pin.destinations) + + return ''.join(chain(ins, outs)) + + def to_ir(self) -> backend.IR: + """ Transforms the relations between blocks into an intermediate + representation in O(n), n being the number of pins. """ + ir_blocks = {} + ir_inputs = {} + ir_outputs = {} + logger.debug('Transforming to IR') + for block in self.blocks.children: + if block.is_orphan(): # Ignore orphaned blocks + continue + block_hash = id(block) + block_inputs, block_outputs = [], [] + avoid = False + for in_pin in block.input_pins: + pin_hash = id(in_pin) + block_inputs.append(pin_hash) + other = id(in_pin.origin.end) # Always origin + ir_inputs[pin_hash] = backend.InputEntry(origin=other, + pin=in_pin, + block=block_hash) + for out_pin in block.output_pins: + pin_hash = id(out_pin) + block_outputs.append(pin_hash) + dest = list(map(id, out_pin.destinations)) + ir_outputs[pin_hash] = backend.OutputEntry(destinations=dest, + pin=out_pin, + block=block_hash) + ir_blocks[block_hash] = backend.BlockEntry(inputs=block_inputs, + function=block.function, + outputs=block_outputs) + self.block_hashes = ir_blocks + return backend.IR(blocks=ir_blocks, inputs=ir_inputs, outputs=ir_outputs) + + #@mainthread Concurrency bug? + def on_block_executed(self, block_hash: int): + """ Callback that kindles a block, pulses future connections and + stops the pulse of past connections. """ + block_idx = list(map(id, self.blocks.children)).index(block_hash) + block = self.blocks.children[block_idx] + block.kindle() + logger.debug('Kindling block {}'.format(block.__class__.__name__)) - def __init__(self, **kwargs): - super().__init__(**kwargs) + # Python list comprehensions can be nested forwards, but also backwards + # http://rhodesmill.org/brandon/2009/nested-comprehensions/ + [connection.pulse() for out_pin in block.output_pins + for connection in out_pin.destinations] + [in_pin.origin.stop_pulse() for in_pin in block.input_pins] + # Touch events override def on_touch_move(self, touch): if touch.button == 'left' and 'cur_line' in touch.ud.keys(): #print(self.get_root_window().mouse_pos) @@ -56,8 +135,9 @@ def on_touch_move(self, touch): else: return super().on_touch_move(touch) - # TODO: Move dragging info into blackboard class instead of touch global def on_touch_up(self, touch): + """ Inherited from + https://github.com/kivy/kivy/blob/master/kivy/uix/scatter.py#L590. """ if self.disabled: return @@ -78,8 +158,9 @@ def on_touch_up(self, touch): del self._last_touch_pos[touch] self._touches.remove(touch) - if ('cur_line' in touch.ud.keys() and touch.button == 'left'): - print('Delete connection') + # if no connection was made + if 'cur_line' in touch.ud.keys() and touch.button == 'left': + logger.info('Connection was not finished') touch.ud['cur_line'].delete_connection(self) return True @@ -87,109 +168,23 @@ def on_touch_up(self, touch): if self.collide_point(x, y): return True - def in_block(self, x, y): + def in_block(self, x: float, y: float) -> Optional[Block]: + """ Check if a position hits a block. """ for block in self.blocks.children: if block.collide_point(x, y): return block return None - def see_relations(self): - string = '' - for block in self.blocks.children: - if block.inputs: - for pin in block.inputs.children: - if pin.origin: - string += '{} -> {}\n'.format(block.block_label, - pin.origin.end.block.block_label) - if block.outputs: - for pin in block.outputs.children: - for destination in pin.destinations: - string += '{} <- {}\n'.format(block.block_label, - destination.start.block.block_label) - - self.popup.title = 'Block Relations' - self.popup.message = string - self.popup.open() - - def to_ir(self): - """ Transforms the relations between blocks into an intermediate - representation in O(n), n being the number of pins. """ - ir_blocks = {} - ir_inputs = {} - ir_outputs = {} - for block in self.blocks.children: - block_hash = id(block) - block_inputs, block_outputs = [], [] - avoid = False - if block.inputs: - for pin in block.inputs.children: - pin_hash = id(pin) - block_inputs.append(pin_hash) - if pin.origin: - other = id(pin.origin.end) - else: - avoid = True - break # This means we are not connected - ir_inputs[pin_hash] = backend.InputEntry(origin=other, - pin=pin, - block=block_hash) - if block.outputs and not avoid: - for pin in block.outputs.children: - pin_hash = id(pin) - block_outputs.append(pin_hash) - dest = [] - if pin.destinations: - for d in pin.destinations: - dest.append(id(d.start)) - ir_outputs[pin_hash] = backend.OutputEntry(destinations=dest, - pin=pin, - block=block_hash) - if not avoid: - ir_blocks[block_hash] = backend.BlockEntry(inputs=block_inputs, - function=block.function, - outputs=block_outputs) - self.block_hashes = ir_blocks - return backend.IR(blocks=ir_blocks, inputs=ir_inputs, outputs=ir_outputs) - - def process(self): - tainted, tainted_msg = self.check_taint() - if tainted: - self.popup.title = 'Warning' - self.popup.message = tainted_msg - self.popup.open() - else: - threading.Thread(target=backend.execute_graph, - args=(self.to_ir(), self)).start() - - # TODO: Merge this check with block tainted property - def check_taint(self): - for block in self.blocks.children: - if block.inputs: - orphaned = [x.origin == None for x in block.inputs.children] - if not all(orphaned) and any(orphaned): - return True, 'Block "{}" has unconnected inputs!'.format( - block.block_label) - if block.tainted: - return True, block.tainted_msg - return False, '' - - def on_block_executed(self, block_hash): - block_idx = list(map(id, self.blocks.children)).index(block_hash) - block = self.blocks.children[block_idx] - if block.outputs: - for out_pin in block.outputs.children: - for connection in out_pin.destinations: - connection.pulse() - Clock.schedule_once(lambda _: connection.stop_pulse(), 2) - def spawnprint(self): - if not any(map(lambda b: b.__class__ == blocks.PrintBlock, + """ Spawns a print block only if no print block is currently present. + """ + if any(map(lambda b: b.__class__ == blocks.PrintBlock, self.blocks.children)): - self.blocks.add_widget(blocks.PrintBlock(pos=(300, 250))) - else: self.popup.title = 'Warning' self.popup.message = 'Only one print block allowed!' self.popup.open() + else: + self.blocks.add_widget(blocks.PrintBlock(pos=(300, 250))) if __name__ == '__main__': ViewApp().run() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py index 1695807..b3ca6c4 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,17 @@ from setuptools import setup, find_packages -setup(name = 'persimmon', - version = '0.5', - description = 'A visual programming interface for sklearn', - url = 'http://github.com/alvarber/Persimmon', - author = 'Álvaro Bermejo', - author_email = 'alvaro.garcia95@hotmail.com', - license = 'MIT', - packages = find_packages(), - zip_safe = False) +setup(name='persimmon', + description='A visual dataflow language for sklearn', + author='Álvaro Bermejo', + author_email='alvaro.garcia95@hotmail.com', + version='0.8', + url='http://github.com/alvarber/Persimmon', + download_url='https://github.com/AlvarBer/Persimmon/archive/v0.8-beta.tar.gz', + license='MIT', + packages=find_packages(), + include_package_data=True, + setup_requires=['Cython'], + install_requires=['Cython', 'Kivy', 'scipy', 'scikit-learn', 'pandas', + 'coloredlogs'], + zip_safe=False)