From bc41fbe5442e9bcf2ab4fb08533d72d69ab1163c Mon Sep 17 00:00:00 2001 From: Manuel Guidon <33161876+mguidon@users.noreply.github.com> Date: Wed, 13 Dec 2017 22:30:47 +0100 Subject: [PATCH] Workflow (#35) Issue #12: Prototype that pipelines computational services --- demos/workflow/Makefile | 18 + demos/workflow/README.md | 86 +++ demos/workflow/director/.gitignore | 2 + demos/workflow/director/Dockerfile | 13 + demos/workflow/director/Dockerfile-prod | 14 + demos/workflow/director/director.py | 213 ++++++ .../director/templates/layouts/index.html | 39 ++ demos/workflow/director/worker.py | 10 + demos/workflow/docker-compose-swarm.yml | 98 +++ demos/workflow/docker-compose.yml | 67 ++ demos/workflow/images/comp.backend.png | Bin 0 -> 57513 bytes demos/workflow/scripts/deploy.sh | 1 + demos/workflow/scripts/do_forward.sh | 6 + demos/workflow/scripts/pf | 133 ++++ .../workflow/scripts/swarm-node-vbox-setup.sh | 67 ++ .../scripts/swarm-node-vbox-teardown.sh | 7 + demos/workflow/sidecar/Dockerfile | 12 + demos/workflow/sidecar/Dockerfile-prod | 17 + demos/workflow/sidecar/README.md | 31 + demos/workflow/sidecar/sidecar.py | 191 +++++ demos/workflow/solver/Dockerfile | 17 + demos/workflow/solver/build-scripts/build.py | 66 ++ .../workflow/solver/build-scripts/input.json | 14 + .../workflow/solver/build-scripts/output.json | 4 + .../solver/build-scripts/settings.json | 16 + demos/workflow/solver/code/libtiny.o | Bin 0 -> 23424 bytes demos/workflow/solver/code/main.cpp | 71 ++ demos/workflow/solver/code/tinyexpr.c | 653 ++++++++++++++++++ demos/workflow/solver/code/tinyexpr.h | 86 +++ .../workflow/solver/simcore.io/do_postprocess | 2 + .../workflow/solver/simcore.io/do_preprocess | 7 + demos/workflow/solver/simcore.io/do_process | 10 + demos/workflow/solver/simcore.io/postprocess | 2 + demos/workflow/solver/simcore.io/preprocess | 2 + demos/workflow/solver/simcore.io/process | 2 + demos/workflow/solver/simcore.io/run | 5 + 36 files changed, 1982 insertions(+) create mode 100644 demos/workflow/Makefile create mode 100644 demos/workflow/README.md create mode 100644 demos/workflow/director/.gitignore create mode 100644 demos/workflow/director/Dockerfile create mode 100644 demos/workflow/director/Dockerfile-prod create mode 100644 demos/workflow/director/director.py create mode 100644 demos/workflow/director/templates/layouts/index.html create mode 100644 demos/workflow/director/worker.py create mode 100644 demos/workflow/docker-compose-swarm.yml create mode 100644 demos/workflow/docker-compose.yml create mode 100755 demos/workflow/images/comp.backend.png create mode 100755 demos/workflow/scripts/deploy.sh create mode 100755 demos/workflow/scripts/do_forward.sh create mode 100755 demos/workflow/scripts/pf create mode 100755 demos/workflow/scripts/swarm-node-vbox-setup.sh create mode 100755 demos/workflow/scripts/swarm-node-vbox-teardown.sh create mode 100644 demos/workflow/sidecar/Dockerfile create mode 100644 demos/workflow/sidecar/Dockerfile-prod create mode 100644 demos/workflow/sidecar/README.md create mode 100644 demos/workflow/sidecar/sidecar.py create mode 100644 demos/workflow/solver/Dockerfile create mode 100644 demos/workflow/solver/build-scripts/build.py create mode 100644 demos/workflow/solver/build-scripts/input.json create mode 100644 demos/workflow/solver/build-scripts/output.json create mode 100644 demos/workflow/solver/build-scripts/settings.json create mode 100644 demos/workflow/solver/code/libtiny.o create mode 100644 demos/workflow/solver/code/main.cpp create mode 100755 demos/workflow/solver/code/tinyexpr.c create mode 100644 demos/workflow/solver/code/tinyexpr.h create mode 100644 demos/workflow/solver/simcore.io/do_postprocess create mode 100644 demos/workflow/solver/simcore.io/do_preprocess create mode 100644 demos/workflow/solver/simcore.io/do_process create mode 100644 demos/workflow/solver/simcore.io/postprocess create mode 100644 demos/workflow/solver/simcore.io/preprocess create mode 100644 demos/workflow/solver/simcore.io/process create mode 100644 demos/workflow/solver/simcore.io/run diff --git a/demos/workflow/Makefile b/demos/workflow/Makefile new file mode 100644 index 00000000..113d604c --- /dev/null +++ b/demos/workflow/Makefile @@ -0,0 +1,18 @@ +# Build example compuational service +# author: Manuel Guidon + +build: + python solver/build-scripts/build.py --imagename=sidecar-solver --version=1.1 --registry=masu.speag.com --namespace=comp.services + docker build -t masu.speag.com/comp.backend/simcore.director:1.0 -f ./director/Dockerfile-prod ./director + docker build -t masu.speag.com/comp.backend/simcore.sidecar:1.0 -f ./sidecar/Dockerfile-prod ./sidecar + +publish: + python solver/build-scripts/build.py --imagename=sidecar-solver --version=1.1 --registry=masu.speag.com --namespace=comp.services --publish + docker push masu.speag.com/comp.backend/simcore.director:1.0 + docker push masu.speag.com/comp.backend/simcore.sidecar:1.0 + +demo: + echo "this should launch the demo (detached if possible)" + +stop: + echo "this should stop the demo" diff --git a/demos/workflow/README.md b/demos/workflow/README.md new file mode 100644 index 00000000..f7ab33de --- /dev/null +++ b/demos/workflow/README.md @@ -0,0 +1,86 @@ +# Workflow + +An experimental project for designing a complete workflow which includes pipeline extraction, job scheduling, result storage and display in the framework of a scalable docker swarm + +## Description +The computational backend of simcore consists of a director that exposes all available services to the user via the frontend. It is also responsible to convert pipelines created by the user into inter-dependant jobs that can be scheduled asynchronously. +Scheduling is done using the python celery framework with rabbitMQ as a broker and MongoDB as a backend to store data. The celery workers are implemented as sidedcars that themselves can run on-shot docker containers with the actual compuational services. The full stack can be deployed in a docker swarm. + +## Serivces, APIs and Documentation + +### Director Service +Entry point for frontend + +``` +localhost:8010/run_pipeline (POST) +localhost:8010/calc/0.0/1.0/100/"sin(x)" +``` +returns urls with job status and result + +### Flower Service +Visualization of underlying celery task queue +``` +localhost:5555 +``` + +### Mongo Express +Visualization of underlying MongoDB + +``` +localhost:8081 +``` + +### RabbitMQ +Visualization of broker +``` +localhost:15672 +``` + +### Visualizer +Visualization of docker swarm +``` +localhost:5000 +``` + +### Registry +Visualization of simcore docker registry +``` +masu.speag.com:5001 +``` + + +### Pipeline descriptor +Example: +``` +{ + "input": + [ + { + "name": "N", + "value": 10 + }, + { + "name": "xmin", + "value": -1.0 + }, + { + "name": "xmax", + "value": 1.0 + }, + { + "name": "func", + "value": "exp(x)*sin(x)" + } + ], + "container": + { + "name": "masu.speag.com/comp.services/sidecar-solver", + "tag": "1.1" + } +} + +``` + +### Orchestration diagram +![workflow](images/comp.backend.png) + diff --git a/demos/workflow/director/.gitignore b/demos/workflow/director/.gitignore new file mode 100644 index 00000000..f0ff5347 --- /dev/null +++ b/demos/workflow/director/.gitignore @@ -0,0 +1,2 @@ +# examples with 3rd party code +example_images/comp.services/ diff --git a/demos/workflow/director/Dockerfile b/demos/workflow/director/Dockerfile new file mode 100644 index 00000000..e88ee1e7 --- /dev/null +++ b/demos/workflow/director/Dockerfile @@ -0,0 +1,13 @@ +FROM continuumio/miniconda3 +MAINTAINER Manuel Guidon + +RUN conda install flask plotly pymongo numpy +RUN conda install -c conda-forge celery +RUN pip install docker + + +EXPOSE 8010 + +WORKDIR /work + +CMD ["python", "director.py"] diff --git a/demos/workflow/director/Dockerfile-prod b/demos/workflow/director/Dockerfile-prod new file mode 100644 index 00000000..0d84f217 --- /dev/null +++ b/demos/workflow/director/Dockerfile-prod @@ -0,0 +1,14 @@ +FROM continuumio/miniconda +MAINTAINER Manuel Guidon + +RUN conda install flask plotly pymongo numpy +RUN conda install -c conda-forge celery +RUN pip install docker + +EXPOSE 8010 + +WORKDIR /work +ADD *.py /work/ +ADD ./templates /work/templates + +CMD ["python", "director.py"] diff --git a/demos/workflow/director/director.py b/demos/workflow/director/director.py new file mode 100644 index 00000000..b7109b9d --- /dev/null +++ b/demos/workflow/director/director.py @@ -0,0 +1,213 @@ +from flask import Flask, make_response, request, url_for, render_template +import json +import hashlib +from pymongo import MongoClient +import gridfs +from bson import ObjectId +from werkzeug.exceptions import NotFound, ServiceUnavailable +import requests +from worker import celery +from celery.result import AsyncResult +import celery.states as states +from celery import signature +import numpy as np +import docker +import sys +import plotly + +app = Flask(__name__) + + +task_info = {} + +def create_graph(x,y): + graph = [ + dict( + data=[ + dict( + x=x, + y=y, + type='scatter' + ), + ], + layout=dict( + title='scatter plot' + ) + ) + ] + return graph + + +def nice_json(arg): + response = make_response(json.dumps(arg, sort_keys = True, indent=4)) + response.headers['Content-type'] = "application/json" + return response + +def output_exists(output_hash): + db_client = MongoClient("mongodb://database:27017/") + output_database = db_client.output_database + output_collections = output_database.output_collections + exists = output_collections.find_one({"_hash" : str(output_hash)}) + return not exists is None + +def parse_input_data(data): + if "input" in data: + inp = data["input"] + data_hash = hashlib.sha256(json.dumps(inp, sort_keys=True).encode('utf-8')).hexdigest() + db_client = MongoClient("mongodb://database:27017/") + input_database = db_client.input_database + input_collections = input_database.input_collections + exists = input_collections.find_one({"_hash" : str(data_hash)}) + if exists == None: + cp_data = data.copy() + cp_data["_hash"] = [str(data_hash)] + input_collections.insert_one(cp_data) + + return [data_hash, not exists is None] + +def parse_container_data(data): + if "container" in data: + container = data["container"] + container_name = container["name"] + container_tag = container['tag'] + client = docker.from_env(version='auto') + client.login(registry="masu.speag.com/v2", username="z43", password="z43") + img = client.images.pull(container_name, tag=container_tag) + container_hash = str(img.id).split(':')[1] + return container_hash + +def start_computation(data): + try: + # req = requests.post("http://sidecar:8000/setup", json = data) + # req2 = requests.get("http://sidecar:8000/preprocess") + # req3 = requests.get("http://sidecar:8000/process") + # req4 = requests.get("http://sidecar:8000/postprocess") +# print data +# sys.stdout.flush() + req = requests.post("http://sidecar:8000/run", json = data) + + except requests.exceptions.ConnectionError: + raise ServiceUnavailable("The computational service is unavailable.") + +@app.route('/add//') +def add(param1,param2): + task = celery.send_task('mytasks.add', args=[param1, param2], kwargs={}) + return "check status of {id} ".format(id=task.id, + url=url_for('check_task',id=task.id,_external=True)) + +@app.route('/check/') +def check_task(id): + res = celery.AsyncResult(id) + if res.state==states.PENDING: + return res.state + else: + db_client = MongoClient("mongodb://database:27017/") + output_database = db_client.output_database + output_collections = output_database.output_collections + exists = output_collections.find_one({"_hash" : str(res.result)}) + if exists is not None: + file_db = db_client.file_db + fs = gridfs.GridFS(file_db) + for file_id in exists["ids"]: + data = fs.get(file_id).read() + data_array = np.fromstring(data, sep="\t") + x = data_array[0::2] + y = data_array[1::2] + graph = create_graph(x, y) + ids = ['graph-{}'.format(i) for i, _ in enumerate(graph)] + # objects to their JSON equivalents + graphJSON = json.dumps(graph, cls=plotly.utils.PlotlyJSONEncoder) + + return render_template('layouts/index.html', + ids=ids, + graphJSON=graphJSON) + + return str(res.result) + + +@app.route("/services", methods=['GET']) +def services(): + return nice_json(registered_services) + +@app.route("/service/", methods=['GET']) +def service(id): + return nice_json(registered_services[id]) + +@app.route("/task/", methods=['Get']) +def task(id): + return "42" + +@app.route("/run_pipeline", methods=['POST']) +def run_pipeline(): + data = request.get_json() + hashstr = "" + [input_hash, input_exists] = parse_input_data(data) + + container_hash = parse_container_data(data) + + combined = hashlib.sha256() + combined.update(input_hash.encode('utf-8')) + combined.update(container_hash.encode('utf-8')) + output_hash = combined.hexdigest() + + output_ready = output_exists(output_hash) + task = celery.send_task('mytasks.run', args=[data], kwargs={}) + + return "check status of {id} ".format(id=task.id, + url=url_for('check_task',id=task.id,_external=True)) + + +# @app.route("/calc", methods=['GET']) +@app.route('/calc////') +def calc(x_min, x_max, N, f): + #ata = request.get_json() + + data_str = """{ + "input": + [ + { + "name": "N", + "value": %s + }, + { + "name": "xmin", + "value": %f + }, + { + "name": "xmax", + "value": %f + }, + { + "name": "func", + "value": %s + } + ], + "container": + { + "name": "masu.speag.com/comp.services/sidecar-solver", + "tag": "1.1" + } + }""" % (N, x_min, x_max, f) + + data = json.loads(data_str) + print(type(data)) + sys.stdout.flush() + hashstr = "" + [input_hash, input_exists] = parse_input_data(data) + + container_hash = parse_container_data(data) + + combined = hashlib.sha256() + combined.update(input_hash.encode('utf-8')) + combined.update(container_hash.encode('utf-8')) + output_hash = combined.hexdigest() + + output_ready = output_exists(output_hash) + task = celery.send_task('mytasks.run', args=[data], kwargs={}) + + return "check status of {id} ".format(id=task.id, + url=url_for('check_task',id=task.id,_external=True)) + + +if __name__ == "__main__": + app.run(port=8010, debug=True, host='0.0.0.0') diff --git a/demos/workflow/director/templates/layouts/index.html b/demos/workflow/director/templates/layouts/index.html new file mode 100644 index 00000000..890165bd --- /dev/null +++ b/demos/workflow/director/templates/layouts/index.html @@ -0,0 +1,39 @@ + + + + + + + + + {% for id in ids %} +

{{id}}

+
+ {% endfor %} + + + + +
+ + + + + + + + +
+ + diff --git a/demos/workflow/director/worker.py b/demos/workflow/director/worker.py new file mode 100644 index 00000000..2f334aa4 --- /dev/null +++ b/demos/workflow/director/worker.py @@ -0,0 +1,10 @@ +import os +from celery import Celery + +env=os.environ +CELERY_BROKER_URL=env.get('CELERY_BROKER_URL','amqp://z43:z43@rabbit:5672') +CELERY_RESULT_BACKEND=env.get('CELERY_RESULT_BACKEND','rpc://') + +celery= Celery('tasks', + broker=CELERY_BROKER_URL, + backend=CELERY_RESULT_BACKEND) diff --git a/demos/workflow/docker-compose-swarm.yml b/demos/workflow/docker-compose-swarm.yml new file mode 100644 index 00000000..1e96de46 --- /dev/null +++ b/demos/workflow/docker-compose-swarm.yml @@ -0,0 +1,98 @@ +version: '3' +services: + director: + image: masu.speag.com/comp.backend/simcore.director:1.0 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "8010:8010" + dns: + - 172.16.8.15 + deploy: + replicas: 1 + restart_policy: + condition: on-failure + placement: + constraints: [node.role == worker] + sidecar: + image: masu.speag.com/comp.backend/simcore.sidecar:1.0 + volumes: + - input:/input + - output:/output + - log:/log + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "8000:8000" + dns: + - 172.16.8.15 + deploy: + replicas: 5 + restart_policy: + condition: on-failure + placement: + constraints: [node.role == worker] + database: + image: mongo:3.4.0 + environment: + - MONGO_DATA_DIR=/data/db + - MONGO_LOG_DIR=/dev/null + volumes: + - db:/data/db + ports: + - "28017:28017" + command: mongod --httpinterface --rest --smallfiles --logpath=/dev/null # --quiet + deploy: + replicas: 1 + restart_policy: + condition: on-failure + placement: + constraints: [node.role == manager] + database_ui: + image: mongo-express + ports: + - "8081:8081" + environment: + - ME_CONFIG_MONGODB_SERVER=database + depends_on: + - database + deploy: + replicas: 1 + restart_policy: + condition: on-failure + placement: + constraints: [node.role == manager] + visualizer: + image: dockersamples/visualizer:stable + ports: + - "5000:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + deploy: + placement: + constraints: [node.role == manager] + rabbit: + image: rabbitmq:3-management + environment: + - RABBITMQ_DEFAULT_USER=z43 + - RABBITMQ_DEFAULT_PASS=z43 + ports: + - "15672:15672" + deploy: + placement: + constraints: [node.role == manager] + flower: + image: ondrejit/flower:latest + command: --broker=amqp://z43:z43@rabbit:5672 + ports: + - 5555:5555 + depends_on: + - director + deploy: + placement: + constraints: [node.role == manager] +volumes: + input: + output: + log: + db: + diff --git a/demos/workflow/docker-compose.yml b/demos/workflow/docker-compose.yml new file mode 100644 index 00000000..65da3203 --- /dev/null +++ b/demos/workflow/docker-compose.yml @@ -0,0 +1,67 @@ +version: '2' +services: + director: + build: + context: . + dockerfile: ./director/Dockerfile + volumes: + - ./director:/work + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "8010:8010" + depends_on: + - rabbit + sidecar: + build: + context: . + dockerfile: ./sidecar/Dockerfile + volumes: + - input:/input + - output:/output + - log:/log + - ./sidecar:/work + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "8000:8000" + dns: + - 172.16.8.15 + database: + image: mongo:3.4.0 + environment: + - MONGO_DATA_DIR=/data/db + - MONGO_LOG_DIR=/dev/null + volumes: + - db:/data/db + ports: + - "28017:28017" + command: mongod --httpinterface --rest --smallfiles --logpath=/dev/null # --quiet + database_ui: + image: mongo-express +# links: +# - database:mongo + ports: + - "8081:8081" + environment: + - ME_CONFIG_MONGODB_SERVER=database + depends_on: + - database + rabbit: + image: rabbitmq:3-management + environment: + - RABBITMQ_DEFAULT_USER=z43 + - RABBITMQ_DEFAULT_PASS=z43 + ports: + - "15672:15672" + flower: + image: ondrejit/flower:latest + command: --broker=amqp://z43:z43@rabbit:5672 + ports: + - 5555:5555 + depends_on: + - director +volumes: + input: + output: + log: + db: + diff --git a/demos/workflow/images/comp.backend.png b/demos/workflow/images/comp.backend.png new file mode 100755 index 0000000000000000000000000000000000000000..6a7e00f30ca73d7ff7e69940dfaa8d94e5ebb506 GIT binary patch literal 57513 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%0A`2#g0yZz+n-N=f(eIT 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 + echo "$usage" + exit 1 +fi + +if [ "$#" -ne 0 ]; then + while [ "$#" -gt 0 ] + do + case "$1" in + -h|--help) + echo "$usage" + exit 0 + ;; + -f|--foreground) + foreground="true" + ;; + -s|--stop) + stop="true" + ;; + -e|--environment) + environment="$2" + ;; + -q|--quiet) + quiet="true" + ;; + --) + break + ;; + -*) + echo "Invalid option '$1'. Use --help to see the valid options" >&2 + exit 1 + ;; + # an option argument, continue + *) ;; + esac + shift + done +fi + +pidport() { + lsof -n -i4TCP:$1 | grep --exclude-dir={.bzr,CVS,.git,.hg,.svn} LISTEN +} + +# Check if port contains ":", if so we should split +if [[ $port == *":"* ]]; then + # Split by : + ports=(${port//:/ }) + if [[ ${#ports[@]} != 2 ]]; then + if [[ $quiet == "false" ]]; then + echo "Port forwarding should be defined as hostport:targetport, for example: 8090:8080" + fi + exit 1 + fi + + + hostport=${ports[0]} + port=${ports[1]} +fi + + +if [[ ${stop} == "true" ]]; then + result=`pidport $hostport` + + if [ -z "${result}" ]; then + if [[ $quiet == "false" ]]; then + echo "Port $hostport is not forwarded, cannot stop" + fi + exit 1 + fi + + process=`echo "${result}" | awk '{ print $1 }'` + if [[ $process != "ssh" ]]; then + if [[ $quiet == "false" ]]; then + echo "Port $hostport is bound by process ${process} and not by docker-machine, won't stop" + fi + exit 1 + fi + + pid=`echo "${result}" | awk '{ print $2 }'` && + kill $pid && + echo "Stopped port forwarding for $hostport" +else + docker-machine ssh $environment `if [[ ${foreground} == "false" ]]; then echo "-f -N"; fi` -L $hostport:localhost:$port && + if [[ $quiet == "false" ]] && [[ $foreground == "false" ]]; then + printf "Forwarding port $port" + if [[ $hostport -ne $port ]]; then + printf " to host port $hostport" + fi + echo " in docker-machine environment $environment." + fi +fi diff --git a/demos/workflow/scripts/swarm-node-vbox-setup.sh b/demos/workflow/scripts/swarm-node-vbox-setup.sh new file mode 100755 index 00000000..b40bda76 --- /dev/null +++ b/demos/workflow/scripts/swarm-node-vbox-setup.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Swarm mode using Docker Machine + +managers=1 +workers=2 + +# create manager machines +echo "======> Creating $managers manager machines ..."; +for node in $(seq 1 $managers); +do + echo "======> Creating manager$node machine ..."; + docker-machine create -d virtualbox manager$node; +done + +# create worker machines +echo "======> Creating $workers worker machines ..."; +for node in $(seq 1 $workers); +do + echo "======> Creating worker$node machine ..."; + docker-machine create -d virtualbox worker$node; +done + +# list all machines +docker-machine ls + +# initialize swarm mode and create a manager +echo "======> Initializing first swarm manager ..." +docker-machine ssh manager1 "docker swarm init --listen-addr $(docker-machine ip manager1) --advertise-addr $(docker-machine ip manager1)" + +# get manager and worker tokens +export manager_token=`docker-machine ssh manager1 "docker swarm join-token manager -q"` +export worker_token=`docker-machine ssh manager1 "docker swarm join-token worker -q"` + +echo "manager_token: $manager_token" +echo "worker_token: $worker_token" + +# other masters join swarm +for node in $(seq 2 $managers); +do + echo "======> manager$node joining swarm as manager ..." + docker-machine ssh manager$node \ + "docker swarm join \ + --token $manager_token \ + --listen-addr $(docker-machine ip manager$node) \ + --advertise-addr $(docker-machine ip manager$node) \ + $(docker-machine ip manager1)" +done + +# show members of swarm +docker-machine ssh manager1 "docker node ls" + +# workers join swarm +for node in $(seq 1 $workers); +do + echo "======> worker$node joining swarm as worker ..." + docker-machine ssh worker$node \ + "docker swarm join \ + --token $worker_token \ + --listen-addr $(docker-machine ip worker$node) \ + --advertise-addr $(docker-machine ip worker$node) \ + $(docker-machine ip manager1)" +done + +# show members of swarm +docker-machine ssh manager1 "docker node ls" + diff --git a/demos/workflow/scripts/swarm-node-vbox-teardown.sh b/demos/workflow/scripts/swarm-node-vbox-teardown.sh new file mode 100755 index 00000000..1ae83ac8 --- /dev/null +++ b/demos/workflow/scripts/swarm-node-vbox-teardown.sh @@ -0,0 +1,7 @@ +#!/bin/bash +### Warning: This will remove all docker machines running ### +# Stop machines +docker-machine stop $(docker-machine ls -q) + +# remove machines +docker-machine rm $(docker-machine ls -q) diff --git a/demos/workflow/sidecar/Dockerfile b/demos/workflow/sidecar/Dockerfile new file mode 100644 index 00000000..6614ce91 --- /dev/null +++ b/demos/workflow/sidecar/Dockerfile @@ -0,0 +1,12 @@ +FROM continuumio/miniconda3 +MAINTAINER Manuel Guidon + +RUN conda install flask plotly pymongo +RUN conda install -c conda-forge celery + +RUN pip install docker + +EXPOSE 8000 + +WORKDIR /work +ENTRYPOINT celery -A sidecar worker -c 1 --loglevel=info diff --git a/demos/workflow/sidecar/Dockerfile-prod b/demos/workflow/sidecar/Dockerfile-prod new file mode 100644 index 00000000..ac71c7da --- /dev/null +++ b/demos/workflow/sidecar/Dockerfile-prod @@ -0,0 +1,17 @@ +FROM continuumio/miniconda3 +MAINTAINER Manuel Guidon + +RUN conda install flask plotly pymongo +RUN conda install -c conda-forge celery + +RUN pip install docker + +EXPOSE 8000 + +WORKDIR /work +ADD *.py /work/ + +ENTRYPOINT celery -A sidecar worker -c 1 --loglevel=info + + + diff --git a/demos/workflow/sidecar/README.md b/demos/workflow/sidecar/README.md new file mode 100644 index 00000000..20f72899 --- /dev/null +++ b/demos/workflow/sidecar/README.md @@ -0,0 +1,31 @@ +# Sidecar: Use sidecar container to control computaional service + + +Overview +======== + +Sidecar is an experimental project for implenting the sidecar pattern +for the control of computational services + +Building Services +================= +To build the sidecar use + +``` +$ cd sidecar +$ docker-compose up +``` + +The solver can be build using + +``` +$ cd solver +$ make build +``` + + +APIs and Documentation +====================== + +## Sidecar (port 8000) + diff --git a/demos/workflow/sidecar/sidecar.py b/demos/workflow/sidecar/sidecar.py new file mode 100644 index 00000000..7530aab8 --- /dev/null +++ b/demos/workflow/sidecar/sidecar.py @@ -0,0 +1,191 @@ +import json +import hashlib +import docker +import os +import sys +import time +import shutil +import uuid +from celery import Celery +from concurrent.futures import ThreadPoolExecutor +from pymongo import MongoClient +import gridfs + +env=os.environ +CELERY_BROKER_URL=env.get('CELERY_BROKER_URL','amqp://z43:z43@rabbit:5672'), +CELERY_RESULT_BACKEND=env.get('CELERY_RESULT_BACKEND','rpc://') + +celery= Celery('tasks', + broker=CELERY_BROKER_URL, + backend=CELERY_RESULT_BACKEND) + + +io_dirs = {} +buddy = None +buddy_image ="" +buddy_image = 'solver' +job_id = "" + +def delete_contents(folder): + for the_file in os.listdir(folder): + file_path = os.path.join(folder, the_file) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): shutil.rmtree(file_path) + except Exception as e: + print(e) + +def create_directories(): + global io_dirs + global job_id + job_id = str(uuid.uuid4()) + for d in ['input', 'output', 'log']: + dir = os.path.join("/", d, job_id) + io_dirs[d] = dir + if not os.path.exists(dir): + os.makedirs(dir) + else: + delete_contents(dir) + + +def parse_input_data(data): + global io_dirs + for d in data: + if "type" in d and d["type"] == "url": + r = requests.get(d["url"]) + filename = os.path.join(io_dirs['input'], d["name"]) + with open(filename, 'wb') as f: + f.write(r.content) + filename = os.path.join(io_dirs['input'], 'input.json') + with open(filename, 'w') as f: + f.write(json.dumps(data)) + +def fetch_container(data): + global buddy_image + buddy_name = data['name'] + buddy_tag = data['tag'] + client = docker.from_env(version='auto') + client.login(registry="masu.speag.com/v2", username="z43", password="z43") + img = client.images.pull(buddy_name, tag=buddy_tag) + buddy_image = buddy_name + ":" + buddy_tag + + +def prepare_input_and_container(data): + if 'input' in data: + parse_input_data(data['input']) + + if 'container' in data: + fetch_container(data['container']) + +def dump_log(): + global buddy_image + +def start_container(name, stage, io_env): + client = docker.from_env(version='auto') + buddy = client.containers.run(buddy_image, "run", + detach=False, remove=True, + volumes = {'workflow_input' : {'bind' : '/input'}, + 'workflow_output' : {'bind' : '/output'}, + 'workflow_log' : {'bind' : '/log'}}, + environment=io_env) + + # buddy.remove() + # hash output + output_hash = hash_job_output() + + store_job_output(output_hash) + + return output_hash + +def hash_job_output(): + output_hash = hashlib.sha256() + directory = io_dirs['output'] + + if not os.path.exists (directory): + return -1 + + try: + for root, dirs, files in os.walk(directory): + for names in files: + filepath = os.path.join(root,names) + try: + f1 = open(filepath, 'rb') + except: + # You can't open the file for some reason + f1.close() + continue + + while 1: + # Read file in as little chunks + buf = f1.read(4096) + if not buf : break + output_hash.update(buf) + f1.close() + except: + import traceback + # Print the stack traceback + traceback.print_exc() + return -2 + + return output_hash.hexdigest() + +def store_job_output(output_hash): + db_client = MongoClient("mongodb://database:27017/") + output_database = db_client.output_database + output_collections = output_database.output_collections + file_db = db_client.file_db + fs = gridfs.GridFS(file_db) + directory = io_dirs['output'] + data = {} + if not os.path.exists (directory): + return + try: + output_file_list = [] + ids = [] + for root, dirs, files in os.walk(directory): + for names in files: + filepath = os.path.join(root,names) + file_id = fs.put(open(filepath,'rb')) + ids.append(file_id) + with open(filepath, 'rb') as f: + file_data = f.read() + current = { 'filename' : names, 'contents' : file_data } + output_file_list.append(current) + + data["output"] = output_file_list + data["_hash"] = output_hash + data["ids"] = ids + + output_collections.insert_one(data) + + except: + import traceback + # Print the stack traceback + traceback.print_exc() + return -2 + +def do_run(data): + global buddy_image + global job_id + # add files if any and dump json + + prepare_input_and_container(data) + io_env = [] + io_env.append("INPUT_FOLDER=/input/"+job_id) + io_env.append("OUTPUT_FOLDER=/output/"+job_id) + io_env.append("LOG_FOLDER=/log/"+job_id) + + + return start_container(buddy_image, "run", io_env) + +@celery.task(name='mytasks.add') +def add(x, y): + time.sleep(5) # lets sleep for a while before doing the gigantic addition task! + return x + y + +@celery.task(name='mytasks.run') +def run(data): + create_directories() + return str(do_run(data)) + diff --git a/demos/workflow/solver/Dockerfile b/demos/workflow/solver/Dockerfile new file mode 100644 index 00000000..86acea14 --- /dev/null +++ b/demos/workflow/solver/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine + +MAINTAINER Manuel guidon + +RUN apk add --no-cache g++ bash jq + +WORKDIR /work + +ADD ./code /work +ADD ./simcore.io /simcore.io +RUN chmod +x /simcore.io/* + +ENV PATH="/simcore.io:${PATH}" + +RUN gcc -c -fPIC -lm tinyexpr.c -o libtiny.o +RUN g++ -std=c++11 -o test main.cpp libtiny.o +RUN rm *.cpp *.c *.h diff --git a/demos/workflow/solver/build-scripts/build.py b/demos/workflow/solver/build-scripts/build.py new file mode 100644 index 00000000..c007c8a3 --- /dev/null +++ b/demos/workflow/solver/build-scripts/build.py @@ -0,0 +1,66 @@ +import sys, os +from optparse import OptionParser +import docker +import json + +dir_path = os.path.dirname(os.path.realpath(__file__)) +parent_path = os.path.abspath(os.path.join(dir_path, os.pardir)) +path, folder_name = os.path.split(parent_path) + +def main(argv): + parser = OptionParser() + + parser.add_option( + "-i", "--imagename", dest="imagename", help="name for the image", default=folder_name) + + parser.add_option( + "-v", "--version", dest="version", help="version for the image", default='') + + parser.add_option( + "-r", "--registry", dest="registry", help="docker registry to use", default='') + + parser.add_option( + "-n", "--namespace", dest="namespace", help="which namespace to use", default="") + + parser.add_option( + "-p", "--publish", action="store_true", dest="publish", help="publish in registry") + + + (options, args) = parser.parse_args(sys.argv) + + registry = "" + namespace = "" + image_name = "" + version = "" + + _settings = {} + _input = {} + _output = {} + + labels = {} + for f in ["settings", "input", "output"]: + json_file = os.path.join(dir_path,''.join((f,".json"))) + with open(json_file) as json_data: + label_dict = json.load(json_data) + labels["io.simcore."+f] = str(label_dict[f]) + + tag = '' + if options.registry: + tag = options.registry + "/" + if options.namespace: + tag = tag + options.namespace + "/" + tag = tag + options.imagename + if options.version: + tag = tag + ":" + options.version + client = docker.from_env(version='auto') + + client.images.build(path=parent_path, tag=tag, labels=labels) + if options.publish: + client.login(registry=options.registry, username="z43", password="z43") + for line in client.api.push(tag, stream=True): + print line + + +if __name__ == "__main__": + main(sys.argv[1:]) + diff --git a/demos/workflow/solver/build-scripts/input.json b/demos/workflow/solver/build-scripts/input.json new file mode 100644 index 00000000..6092df5b --- /dev/null +++ b/demos/workflow/solver/build-scripts/input.json @@ -0,0 +1,14 @@ +{ + "input": [ + { + "name": "N", + "value": 10 , + "type" : "int" + }, + { + "name": "inputfile", + "url": "https://outbox.itis.ethz.ch/guidon/whassup.txt", + "type" : "url" + } + ] +} diff --git a/demos/workflow/solver/build-scripts/output.json b/demos/workflow/solver/build-scripts/output.json new file mode 100644 index 00000000..874df61c --- /dev/null +++ b/demos/workflow/solver/build-scripts/output.json @@ -0,0 +1,4 @@ +{ + "output": [ + ] +} diff --git a/demos/workflow/solver/build-scripts/settings.json b/demos/workflow/solver/build-scripts/settings.json new file mode 100644 index 00000000..5dbe2960 --- /dev/null +++ b/demos/workflow/solver/build-scripts/settings.json @@ -0,0 +1,16 @@ +{ + "settings": [ + { + "name": "N", + "text": "Number of iterations", + "type": "int", + "value":5 + }, + { + "name": "inputfile", + "text": "Inputfile for solver", + "type": "url", + "value": "" + } + ] +} diff --git a/demos/workflow/solver/code/libtiny.o b/demos/workflow/solver/code/libtiny.o new file mode 100644 index 0000000000000000000000000000000000000000..bb600ddc2d643c990b264ee7391cbc72fbfa6b6a GIT binary patch literal 23424 zcmeI3e|!|xwZJC{NWka@OKVz!tQFhvD@LRi6m=CB#!8e{Qm{fpNH!)Vznbggv;eV#ViL(a*-EHEOJDi;s8CopUC44tqgu zeSh`xnarKiyEFdjU(H&qY|4k|%0sEm5J5JZ`g52Hpr zIGC4=CW>Rhp?Gk}zRiy9OCz^q!7PH>q#X}tf9)Vg6kUJN3Qw}5>yuV^0+c5myW1`X z-J~6R%#J$Ncd_#@bVFHvGG~*k zi!cxnc_aJ;c0768ZN^gH3@s+`}J6vImuTRG!2Vj^$ zOIrJaLnp&&;C}c66MkxNFxI>G^@(}Or?atMh$Sx>3m%FG55RYa5OSX*7`s z12i)lvuViU9Si!#*L^XJZgszNgM^8`9$G(=idF4bZ66Pm*&7p)QA#b-;4-Kk^YK6h z*vkarCds$6cp(U2u(*Li@lrsbUyN!LLsuel9Ma4BdC-kZ zf`h}8Ww=`*x6~-P(kS`5kQ7&fA?G$z?_0b#S4~tsjx5zu&;0H_ z_U3ylS?;@O9n4fV0dCD%>)YI@A(#W$`dml%LK>07^llzEX`J8YUZHwmEZt{h$?@Y%CPaIxW$7NZ04a{xr73Lr#Q)@jx*? z4znlwHr);AgFOqe%O*OV3AYMr2x#cc8Q>BU)#P?;p&zf~3sucAb@J%<43z5)-EBkD zR>8db5)KcTz3M4l<=gaksOy7!=65`Ud+jIxM>W90l7Zh61IIP-^DMqEr}$YG-1?9|CQk;Mnr_jZCyckOt*l(E;J+L-FfE|}D z@pat?Uby!qceYWtrVCZi{uKuO2WUUF4BG=eVx+ zIGd-qludAKK#Qv>FjnXdSVwv`G&I}}G9sgAH2R%*(KEa&-HRhV=FDzze&7}w`N|3| zt#JXNdNnn?ydEOP4Hfzz?%*mB?uSl8#VcWv{kR?7n{UU)^|3S9TU=eX)Y~jL+34Y& zIZqbANNVst4UcH(hIkb$&!4fQy4c9{F;28Y)DQXjDsBJijkg6Atd zS-tf}$tS*rXCk>Oe^6Dl&Qh(csl(4 zfY153WGBL(j+JD~9J(Cfzz&iW%| zn)-Z4J^U+;RT8@=y8b|3@x+5?NS%FFkqRL^QoT93H;e!T*e3!68?f-(m(IeH4?<36zN|BP!{odeF{l3`o8yW23*J^a+8x zaQ4ISU#Xra>bc_#v+?1bN@Q&6D`2T=tEFmmecFn^y;!{$4!kysZj@@%09Q#*0QmsV zQm76lW1w(gJ6XUBJ8li!qFKb-{_&`ZdlG!tk7h^tHhlwbb04CX!%1B5CI-H!R~CDO zoG9j7>f!%()QWSe!Sr?>S@=GIa~cD1%fc$4GQ|d-I?ibp6_i_#KbHZIRqP;XwF+M) zEBo1WIH&2dT&6wsv1w0z>X9U(Scdgg^<$+;uJkbUHn~=4yYW=ys?_5NSL*SEEA@EN zmm~5Jda?{XsY;?dee6zKPIqSIa>wH%yW{bZ-SPOy?s$AmeGH+G%h1QFK6Es} zj_w@0-#45PH81y{}5ZpUPbv!Uz?D zGuyMcxS;81o}0ZNZf-h@J|6XlpK+#OH&x}GLGEm$STI0#^s1aCJWY_2O+eRvj5wv) z^5wMUX=AqKset=U`dNnUifvuluhnu7FR<~}=gulm_i`H{k5gQ~$0@Gg;}nlh>C`N6>WJB`dA6Ar|K8r81sZC3rr&@)?y_edciA(K zyX=|A-GpI$1i;fw0e>K-gzbAndaz5cb&<$Sy-5HUx5% zIX@gtrkI}anNgI%H3pQ12VeA2`zmKs3x5H@Hvp)uA0E-!JYUxzoK}F#jR{}Z@7$|s z%8%fe8sIw!7ImNQuno8`co1J8_#NwfWqtTXDn25KZMx1`9`z**|bIzQ+nrcZ}g1a$ij*J0WkK?_w(RwPIxeQl@$_(jX^C+Ql$>(lP!Zt2!JHY`-pyLyb)q3gBq4LRM%^n^j*^Yt=WkwzgYMEml+O)p(p)VYSq@TPX#)JqXvH)aki8$^~wM9W8y_a@^@U zTKc%_FFRW0Ap5xM|G4cRt9_3pKUVu3tNh0u-#=>qi0+?1ckVgcKKQ5 z71Ph05mA>jCOg1CqvFgjR?Mt88w!sj19@w|V&$zV&KqAes;~#v5%|*-9+(pN=fV?< zznwq#gyZsm2^x-U4suA3;vZMO$CWQd4ygkFuzaNwE8FXkLpl-vu>6u?`BvnRO!D21 z*m?}lTt0uVTmOENe+Ab?EH9|cLxoa6he&=Yo>`th&n;hyrh!|& zps@fo(B>SHFZ9SSb>&M*{xTY2`}zA-1z3I^$<-RzmH*6@ZzXx!Q~niKzMJGpkNkJ8 zd_T#5;E``pazC&`B>xnQXV<@Aw4k7<(6!;m*dV=&e_S8ma^(S%zvhv@?8=vuywxNB zhAZzR`6BEKwqJ0$8l!$d+enUiSALZ$j>MSI+z9Kn4fr>LQr!W z7^VUXw!KA~1lM^Q_fxrY;AsCW3R%s4VCNDqq0rQv2R5JhJ;c>q2Zr^doks+}ocN1^ zUqSp`!M{d)>~WBRq~eTPGm3te2e=eDmq> z>wFGSa|yET#J`LRkklN4^J+NI&dX#+&0Sy{h~G{ftL7}QDDiy!=1MlI_{2ENn4@jW@09o#KA3H(pM8IyVj z!ubg}kas~-W2$gYRuay+>^x`Se>Cuy4E%tB4;c6x2L6J94;lD-297%wx#BH=cFDy@ z8TbhXo-)`UYta9cfxl|7Gv1)@H}Epx*zXt7d{oa&uzi+6{~QCqz`z%h{e#qa>X`-h zmk@uC=GSY$Fx3)&iEc=(4z`*N_QM9gj_lk`Hy%IPvBHfl*TB9#d3M<|S^+O}(;Y;}EmC{*26(_YgY0!tl?!1}Rv zd|eo=gJ{--tHZ4;L$L1?ZV$KCS@q5Er@Fc$TvNNU zy0&3ub$t!&CV^L>RW<4ik+(HAg%CBifPZaPTRUXxt+q(GwxI^2uT})RVkz|pKCV6s z2Ye%<65gjVAwLZc=64~4gnSYl%yAxL!pABOVdlp{|6`II=LaUV^Eo(Jf2x6>Yv7j} z_*DkJ(!jrE;CE{Lbj{EEH2!&w^O~CDT}SJJ-;zG_F5>EIZz#|FM&f@H`nM1tfr^^` zR^syv{I`O$eO}vgy=*@t^qIdSIM*AY`q>WiD0VFQiEBOX-z~)PbdJVPV#Rzanzrq)jJZ`?vPl2yK`xsWZ-uSK7#sbr-27(X@UC$GvVNNSgLW{ zd}sa=arS2^W*{9j@Xyjx2jjvuCfoU{f$uc%J_8>#@OiY9lJ&l1;KeuyAW8je4ZPdH zdky?A#JSygoX@3ormXh`jh_#Gal3t%)}K;;74a#ctM}vgG=1I9A2qJ`%hza~jCS;X z{}yqqSGUuvaox_P^cK{=fiE`jCIi3Dz_%FqeFmN|@HdIec`=<{l$p<=aej`0 zuOlwo;U@BuNZhaZa_hj{XW;gml$}nfnRUn|7PGj4g6K&Jf8Wv`4p6|< z>*OH4Kcc?=JZjXqzW#q+^d8Orx03!T#Ic=qf94AP4$`kQ=+|m| zF4*FB#uzYRy?C^0?>{=neFIFi9Cq~=6Q^aG%lZ9{mM1R9eGE)=8+JMFhhU=N>GIDL zC-+@`2XVhDHS9lrNQY^Xb8_{g#LEO}EK2Gr4iI)ieW8$TPvwhs}#KeC7i1a53eh+cn2i5i2 z{xrcKApHu#A0j?e@Lv$0CHSw1&k=kt@qpmJA%3yoj}x~Ae}eb|!Jj6M`?7kx&k^U} zGq~NJCyx8Iy8esAmkRze@#TWQO1w_+_lP$L{%7J%g1N3VtT>`vw03@dpK;O?;Q&=Mmp6_=Ut1f(MB|D)=SD`vk8f{(Hf{Ogt%g z74c^Uzmj;r;LC{b7rcgeO7IZz1A;daPYd2m{Gj09Bt9s3g!qu)*AUMNzMl9Y!LK8( zZk#ZY_&%_axV{gJvKa9~ynw{|-NcIoznOTk;I|SVC;08eO9cNR@lwI>BJLObN5m%y zeh=|7!GBDAn&1x*uMqqp;xh&R1@T#e|BCn=!S@mm2>u)57YqJ4aa-^wh%XTQY2ph7 ze~$Pf!Jj9-MDQ1h^FA?(@wlG&a-m;MyiV}p{UwNq?8%mDHZQ1>ZzGA^2sqKl`ZQLE?Razg^<^ z^LxSHC7u-g@5G-K{4nu;!3${LVZY#`h^GYKKz<$&{CeVP!M{!Xpy1yjJ}CH2#D@g` zF7d43TZkVL{QJcDlPVPB!TXFO=slkK`LsV+DELLhiv*uXyjbur5g#Y`WyDJa{|a&5 z?`MC$N}RuoV*Z~L*Cb(Q5Aia=Unf3I@H1$iy+ZJth|d&!6zx;b68!tbc^{YkoIlPP zw*jF)m*Tou@J8ac;CB*VAov@^`THua_e$C)T_p5hB)&xOZ_)lE?^m&%vuM6JQQ)^~ z&hPSb2U)np(vnn)NdqxTjOY-*V)T8j{41{-=Og`iQ%bJ zSEy?;cuIr~Xj{2RXpVm0W%injZn81n2LA2g84|Etp~U(wNFl`Gob>JW8-S;Xb-tyVdHj~Hr!A}y`qQ2G4J zs-~+i_^4c{0X`~(&k1RCe)N+cisBH#xv8fBUG$df)X~)m{_HdYj(`p!KS&R?GWxCs zUnj7hLek^q#^;Drvyv3%{scGc$Ku!whmxDyI4q!f-Jw&>5e7 zx#BOuA0r{{qV*`oKN1cZ|6E{Lzo^Da`2j-m3xQwJ;XhbSuKJU-@aaS$B&>fF9J2m} zz_5OOjR2SK4_*3N1~KYl;`5bSUJ2+=`6T^nIM3ynv_J-#Z2t->FWaB}=kmCI$rb;? zMUDo)zheCOip}MD{N<_}OAg{knvmAe#K2*EDjc#rZs&?F0%BPW)NmU`z~a)v;gPF+ Q;bLXRTH~DDzq0)Q0b!tSqyPW_ literal 0 HcmV?d00001 diff --git a/demos/workflow/solver/code/main.cpp b/demos/workflow/solver/code/main.cpp new file mode 100644 index 00000000..3940bdcf --- /dev/null +++ b/demos/workflow/solver/code/main.cpp @@ -0,0 +1,71 @@ +extern "C" { + #include "tinyexpr.h" +} + +#include +#include +#include +#include +#include +#include +#include + + +int main(int argc, char* argv[]) +{ + int N = 100; + double xmin=0.f; + double xmax=1.f; + std::string filename = "output.dat"; + std::string func = "sin(x)"; + double x; + te_variable vars[] = {{"x", &x}}; + int err; + + if (argc>1) + { + xmin = std::atof(argv[1]); + } + if (argc>2) + { + xmax = std::atof(argv[2]); + } + if (argc>3) + { + N = std::atoi(argv[3]); + } + if (argc>4) + { + func = std::string(argv[4]); + } + if (argc>5) + { + filename = std::string(argv[5]); + } + + std::cout <(N); + + std::ofstream output; + output.open(filename); + for(int i=0; i<=N; i++) + { + x = xmin + static_cast(i)*dx; + double y = te_eval(expr); + double p = static_cast(i) / static_cast(N) * 100.f; + output << x << "\t" << y << std::endl; + std::cout << "Progress: " << p << " %" << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + output.close(); + + return 0; +} diff --git a/demos/workflow/solver/code/tinyexpr.c b/demos/workflow/solver/code/tinyexpr.c new file mode 100755 index 00000000..91a1848a --- /dev/null +++ b/demos/workflow/solver/code/tinyexpr.c @@ -0,0 +1,653 @@ +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* COMPILE TIME OPTIONS */ + +/* Exponentiation associativity: +For a^b^c = (a^b)^c and -a^b = (-a)^b do nothing. +For a^b^c = a^(b^c) and -a^b = -(a^b) uncomment the next line.*/ +/* #define TE_POW_FROM_RIGHT */ + +/* Logarithms +For log = base 10 log do nothing +For log = natural log uncomment the next line. */ +/* #define TE_NAT_LOG */ + +#include "tinyexpr.h" +#include +#include +#include +#include +#include + +#ifndef NAN +#define NAN (0.0/0.0) +#endif + +#ifndef INFINITY +#define INFINITY (1.0/0.0) +#endif + + +typedef double (*te_fun2)(double, double); + +enum { + TOK_NULL = TE_CLOSURE7+1, TOK_ERROR, TOK_END, TOK_SEP, + TOK_OPEN, TOK_CLOSE, TOK_NUMBER, TOK_VARIABLE, TOK_INFIX +}; + + +enum {TE_CONSTANT = 1}; + + +typedef struct state { + const char *start; + const char *next; + int type; + union {double value; const double *bound; const void *function;}; + void *context; + + const te_variable *lookup; + int lookup_len; +} state; + + +#define TYPE_MASK(TYPE) ((TYPE)&0x0000001F) + +#define IS_PURE(TYPE) (((TYPE) & TE_FLAG_PURE) != 0) +#define IS_FUNCTION(TYPE) (((TYPE) & TE_FUNCTION0) != 0) +#define IS_CLOSURE(TYPE) (((TYPE) & TE_CLOSURE0) != 0) +#define ARITY(TYPE) ( ((TYPE) & (TE_FUNCTION0 | TE_CLOSURE0)) ? ((TYPE) & 0x00000007) : 0 ) +#define NEW_EXPR(type, ...) new_expr((type), (const te_expr*[]){__VA_ARGS__}) + +static te_expr *new_expr(const int type, const te_expr *parameters[]) { + const int arity = ARITY(type); + const int psize = sizeof(void*) * arity; + const int size = (sizeof(te_expr) - sizeof(void*)) + psize + (IS_CLOSURE(type) ? sizeof(void*) : 0); + te_expr *ret = malloc(size); + memset(ret, 0, size); + if (arity && parameters) { + memcpy(ret->parameters, parameters, psize); + } + ret->type = type; + ret->bound = 0; + return ret; +} + + +void te_free_parameters(te_expr *n) { + if (!n) return; + switch (TYPE_MASK(n->type)) { + case TE_FUNCTION7: case TE_CLOSURE7: te_free(n->parameters[6]); + case TE_FUNCTION6: case TE_CLOSURE6: te_free(n->parameters[5]); + case TE_FUNCTION5: case TE_CLOSURE5: te_free(n->parameters[4]); + case TE_FUNCTION4: case TE_CLOSURE4: te_free(n->parameters[3]); + case TE_FUNCTION3: case TE_CLOSURE3: te_free(n->parameters[2]); + case TE_FUNCTION2: case TE_CLOSURE2: te_free(n->parameters[1]); + case TE_FUNCTION1: case TE_CLOSURE1: te_free(n->parameters[0]); + } +} + + +void te_free(te_expr *n) { + if (!n) return; + te_free_parameters(n); + free(n); +} + + +static double pi() {return 3.14159265358979323846;} +static double e() {return 2.71828182845904523536;} +static double fac(double a) {/* simplest version of fac */ + if (a < 0.0) + return NAN; + if (a > UINT_MAX) + return INFINITY; + unsigned int ua = (unsigned int)(a); + unsigned long int result = 1, i; + for (i = 1; i <= ua; i++) { + if (i > ULONG_MAX / result) + return INFINITY; + result *= i; + } + return (double)result; +} +static double ncr(double n, double r) { + if (n < 0.0 || r < 0.0 || n < r) return NAN; + if (n > UINT_MAX || r > UINT_MAX) return INFINITY; + unsigned long int un = (unsigned int)(n), ur = (unsigned int)(r), i; + unsigned long int result = 1; + if (ur > un / 2) ur = un - ur; + for (i = 1; i <= ur; i++) { + if (result > ULONG_MAX / (un - ur + i)) + return INFINITY; + result *= un - ur + i; + result /= i; + } + return result; +} +static double npr(double n, double r) {return ncr(n, r) * fac(r);} + +static const te_variable functions[] = { + /* must be in alphabetical order */ + {"abs", fabs, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"acos", acos, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"asin", asin, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"atan", atan, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"atan2", atan2, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"ceil", ceil, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"cos", cos, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"cosh", cosh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"e", e, TE_FUNCTION0 | TE_FLAG_PURE, 0}, + {"exp", exp, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"fac", fac, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"floor", floor, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"ln", log, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#ifdef TE_NAT_LOG + {"log", log, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#else + {"log", log10, TE_FUNCTION1 | TE_FLAG_PURE, 0}, +#endif + {"log10", log10, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"ncr", ncr, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"npr", npr, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"pi", pi, TE_FUNCTION0 | TE_FLAG_PURE, 0}, + {"pow", pow, TE_FUNCTION2 | TE_FLAG_PURE, 0}, + {"sin", sin, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"sinh", sinh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"sqrt", sqrt, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"tan", tan, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {"tanh", tanh, TE_FUNCTION1 | TE_FLAG_PURE, 0}, + {0, 0, 0, 0} +}; + +static const te_variable *find_builtin(const char *name, int len) { + int imin = 0; + int imax = sizeof(functions) / sizeof(te_variable) - 2; + + /*Binary search.*/ + while (imax >= imin) { + const int i = (imin + ((imax-imin)/2)); + int c = strncmp(name, functions[i].name, len); + if (!c) c = '\0' - functions[i].name[len]; + if (c == 0) { + return functions + i; + } else if (c > 0) { + imin = i + 1; + } else { + imax = i - 1; + } + } + + return 0; +} + +static const te_variable *find_lookup(const state *s, const char *name, int len) { + int iters; + const te_variable *var; + if (!s->lookup) return 0; + + for (var = s->lookup, iters = s->lookup_len; iters; ++var, --iters) { + if (strncmp(name, var->name, len) == 0 && var->name[len] == '\0') { + return var; + } + } + return 0; +} + + + +static double add(double a, double b) {return a + b;} +static double sub(double a, double b) {return a - b;} +static double mul(double a, double b) {return a * b;} +static double divide(double a, double b) {return a / b;} +static double negate(double a) {return -a;} +static double comma(double a, double b) {(void)a; return b;} + + +void next_token(state *s) { + s->type = TOK_NULL; + + do { + + if (!*s->next){ + s->type = TOK_END; + return; + } + + /* Try reading a number. */ + if ((s->next[0] >= '0' && s->next[0] <= '9') || s->next[0] == '.') { + s->value = strtod(s->next, (char**)&s->next); + s->type = TOK_NUMBER; + } else { + /* Look for a variable or builtin function call. */ + if (s->next[0] >= 'a' && s->next[0] <= 'z') { + const char *start; + start = s->next; + while ((s->next[0] >= 'a' && s->next[0] <= 'z') || (s->next[0] >= '0' && s->next[0] <= '9') || (s->next[0] == '_')) s->next++; + + const te_variable *var = find_lookup(s, start, s->next - start); + if (!var) var = find_builtin(start, s->next - start); + + if (!var) { + s->type = TOK_ERROR; + } else { + switch(TYPE_MASK(var->type)) + { + case TE_VARIABLE: + s->type = TOK_VARIABLE; + s->bound = var->address; + break; + + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + s->context = var->context; + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + s->type = var->type; + s->function = var->address; + break; + } + } + + } else { + /* Look for an operator or special character. */ + switch (s->next++[0]) { + case '+': s->type = TOK_INFIX; s->function = add; break; + case '-': s->type = TOK_INFIX; s->function = sub; break; + case '*': s->type = TOK_INFIX; s->function = mul; break; + case '/': s->type = TOK_INFIX; s->function = divide; break; + case '^': s->type = TOK_INFIX; s->function = pow; break; + case '%': s->type = TOK_INFIX; s->function = fmod; break; + case '(': s->type = TOK_OPEN; break; + case ')': s->type = TOK_CLOSE; break; + case ',': s->type = TOK_SEP; break; + case ' ': case '\t': case '\n': case '\r': break; + default: s->type = TOK_ERROR; break; + } + } + } + } while (s->type == TOK_NULL); +} + + +static te_expr *list(state *s); +static te_expr *expr(state *s); +static te_expr *power(state *s); + +static te_expr *base(state *s) { + /* = | | {"(" ")"} | | "(" {"," } ")" | "(" ")" */ + te_expr *ret; + int arity; + + switch (TYPE_MASK(s->type)) { + case TOK_NUMBER: + ret = new_expr(TE_CONSTANT, 0); + ret->value = s->value; + next_token(s); + break; + + case TOK_VARIABLE: + ret = new_expr(TE_VARIABLE, 0); + ret->bound = s->bound; + next_token(s); + break; + + case TE_FUNCTION0: + case TE_CLOSURE0: + ret = new_expr(s->type, 0); + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[0] = s->context; + next_token(s); + if (s->type == TOK_OPEN) { + next_token(s); + if (s->type != TOK_CLOSE) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + } + break; + + case TE_FUNCTION1: + case TE_CLOSURE1: + ret = new_expr(s->type, 0); + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[1] = s->context; + next_token(s); + ret->parameters[0] = power(s); + break; + + case TE_FUNCTION2: case TE_FUNCTION3: case TE_FUNCTION4: + case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + case TE_CLOSURE2: case TE_CLOSURE3: case TE_CLOSURE4: + case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + arity = ARITY(s->type); + + ret = new_expr(s->type, 0); + ret->function = s->function; + if (IS_CLOSURE(s->type)) ret->parameters[arity] = s->context; + next_token(s); + + if (s->type != TOK_OPEN) { + s->type = TOK_ERROR; + } else { + int i; + for(i = 0; i < arity; i++) { + next_token(s); + ret->parameters[i] = expr(s); + if(s->type != TOK_SEP) { + break; + } + } + if(s->type != TOK_CLOSE || i != arity - 1) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + } + + break; + + case TOK_OPEN: + next_token(s); + ret = list(s); + if (s->type != TOK_CLOSE) { + s->type = TOK_ERROR; + } else { + next_token(s); + } + break; + + default: + ret = new_expr(0, 0); + s->type = TOK_ERROR; + ret->value = NAN; + break; + } + + return ret; +} + + +static te_expr *power(state *s) { + /* = {("-" | "+")} */ + int sign = 1; + while (s->type == TOK_INFIX && (s->function == add || s->function == sub)) { + if (s->function == sub) sign = -sign; + next_token(s); + } + + te_expr *ret; + + if (sign == 1) { + ret = base(s); + } else { + ret = NEW_EXPR(TE_FUNCTION1 | TE_FLAG_PURE, base(s)); + ret->function = negate; + } + + return ret; +} + +#ifdef TE_POW_FROM_RIGHT +static te_expr *factor(state *s) { + /* = {"^" } */ + te_expr *ret = power(s); + + int neg = 0; + te_expr *insertion = 0; + + if (ret->type == (TE_FUNCTION1 | TE_FLAG_PURE) && ret->function == negate) { + te_expr *se = ret->parameters[0]; + free(ret); + ret = se; + neg = 1; + } + + while (s->type == TOK_INFIX && (s->function == pow)) { + te_fun2 t = s->function; + next_token(s); + + if (insertion) { + /* Make exponentiation go right-to-left. */ + te_expr *insert = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, insertion->parameters[1], power(s)); + insert->function = t; + insertion->parameters[1] = insert; + insertion = insert; + } else { + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, power(s)); + ret->function = t; + insertion = ret; + } + } + + if (neg) { + ret = NEW_EXPR(TE_FUNCTION1 | TE_FLAG_PURE, ret); + ret->function = negate; + } + + return ret; +} +#else +static te_expr *factor(state *s) { + /* = {"^" } */ + te_expr *ret = power(s); + + while (s->type == TOK_INFIX && (s->function == pow)) { + te_fun2 t = s->function; + next_token(s); + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, power(s)); + ret->function = t; + } + + return ret; +} +#endif + + + +static te_expr *term(state *s) { + /* = {("*" | "/" | "%") } */ + te_expr *ret = factor(s); + + while (s->type == TOK_INFIX && (s->function == mul || s->function == divide || s->function == fmod)) { + te_fun2 t = s->function; + next_token(s); + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, factor(s)); + ret->function = t; + } + + return ret; +} + + +static te_expr *expr(state *s) { + /* = {("+" | "-") } */ + te_expr *ret = term(s); + + while (s->type == TOK_INFIX && (s->function == add || s->function == sub)) { + te_fun2 t = s->function; + next_token(s); + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, term(s)); + ret->function = t; + } + + return ret; +} + + +static te_expr *list(state *s) { + /* = {"," } */ + te_expr *ret = expr(s); + + while (s->type == TOK_SEP) { + next_token(s); + ret = NEW_EXPR(TE_FUNCTION2 | TE_FLAG_PURE, ret, expr(s)); + ret->function = comma; + } + + return ret; +} + + +#define TE_FUN(...) ((double(*)(__VA_ARGS__))n->function) +#define M(e) te_eval(n->parameters[e]) + + +double te_eval(const te_expr *n) { + if (!n) return NAN; + + switch(TYPE_MASK(n->type)) { + case TE_CONSTANT: return n->value; + case TE_VARIABLE: return *n->bound; + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + switch(ARITY(n->type)) { + case 0: return TE_FUN(void)(); + case 1: return TE_FUN(double)(M(0)); + case 2: return TE_FUN(double, double)(M(0), M(1)); + case 3: return TE_FUN(double, double, double)(M(0), M(1), M(2)); + case 4: return TE_FUN(double, double, double, double)(M(0), M(1), M(2), M(3)); + case 5: return TE_FUN(double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4)); + case 6: return TE_FUN(double, double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4), M(5)); + case 7: return TE_FUN(double, double, double, double, double, double, double)(M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + default: return NAN; + } + + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + switch(ARITY(n->type)) { + case 0: return TE_FUN(void*)(n->parameters[0]); + case 1: return TE_FUN(void*, double)(n->parameters[1], M(0)); + case 2: return TE_FUN(void*, double, double)(n->parameters[2], M(0), M(1)); + case 3: return TE_FUN(void*, double, double, double)(n->parameters[3], M(0), M(1), M(2)); + case 4: return TE_FUN(void*, double, double, double, double)(n->parameters[4], M(0), M(1), M(2), M(3)); + case 5: return TE_FUN(void*, double, double, double, double, double)(n->parameters[5], M(0), M(1), M(2), M(3), M(4)); + case 6: return TE_FUN(void*, double, double, double, double, double, double)(n->parameters[6], M(0), M(1), M(2), M(3), M(4), M(5)); + case 7: return TE_FUN(void*, double, double, double, double, double, double, double)(n->parameters[7], M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + default: return NAN; + } + + default: return NAN; + } + +} + +#undef TE_FUN +#undef M + +static void optimize(te_expr *n) { + /* Evaluates as much as possible. */ + if (n->type == TE_CONSTANT) return; + if (n->type == TE_VARIABLE) return; + + /* Only optimize out functions flagged as pure. */ + if (IS_PURE(n->type)) { + const int arity = ARITY(n->type); + int known = 1; + int i; + for (i = 0; i < arity; ++i) { + optimize(n->parameters[i]); + if (((te_expr*)(n->parameters[i]))->type != TE_CONSTANT) { + known = 0; + } + } + if (known) { + const double value = te_eval(n); + te_free_parameters(n); + n->type = TE_CONSTANT; + n->value = value; + } + } +} + + +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error) { + state s; + s.start = s.next = expression; + s.lookup = variables; + s.lookup_len = var_count; + + next_token(&s); + te_expr *root = list(&s); + + if (s.type != TOK_END) { + te_free(root); + if (error) { + *error = (s.next - s.start); + if (*error == 0) *error = 1; + } + return 0; + } else { + optimize(root); + if (error) *error = 0; + return root; + } +} + + +double te_interp(const char *expression, int *error) { + te_expr *n = te_compile(expression, 0, 0, error); + double ret; + if (n) { + ret = te_eval(n); + te_free(n); + } else { + ret = NAN; + } + return ret; +} + +static void pn (const te_expr *n, int depth) { + int i, arity; + printf("%*s", depth, ""); + + switch(TYPE_MASK(n->type)) { + case TE_CONSTANT: printf("%f\n", n->value); break; + case TE_VARIABLE: printf("bound %p\n", n->bound); break; + + case TE_FUNCTION0: case TE_FUNCTION1: case TE_FUNCTION2: case TE_FUNCTION3: + case TE_FUNCTION4: case TE_FUNCTION5: case TE_FUNCTION6: case TE_FUNCTION7: + case TE_CLOSURE0: case TE_CLOSURE1: case TE_CLOSURE2: case TE_CLOSURE3: + case TE_CLOSURE4: case TE_CLOSURE5: case TE_CLOSURE6: case TE_CLOSURE7: + arity = ARITY(n->type); + printf("f%d", arity); + for(i = 0; i < arity; i++) { + printf(" %p", n->parameters[i]); + } + printf("\n"); + for(i = 0; i < arity; i++) { + pn(n->parameters[i], depth + 1); + } + break; + } +} + + +void te_print(const te_expr *n) { + pn(n, 0); +} diff --git a/demos/workflow/solver/code/tinyexpr.h b/demos/workflow/solver/code/tinyexpr.h new file mode 100644 index 00000000..5d0dc0c8 --- /dev/null +++ b/demos/workflow/solver/code/tinyexpr.h @@ -0,0 +1,86 @@ +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015, 2016 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef __TINYEXPR_H__ +#define __TINYEXPR_H__ + + +#ifdef __cplusplus +extern "C" { +#endif + + + +typedef struct te_expr { + int type; + union {double value; const double *bound; const void *function;}; + void *parameters[1]; +} te_expr; + + +enum { + TE_VARIABLE = 0, + + TE_FUNCTION0 = 8, TE_FUNCTION1, TE_FUNCTION2, TE_FUNCTION3, + TE_FUNCTION4, TE_FUNCTION5, TE_FUNCTION6, TE_FUNCTION7, + + TE_CLOSURE0 = 16, TE_CLOSURE1, TE_CLOSURE2, TE_CLOSURE3, + TE_CLOSURE4, TE_CLOSURE5, TE_CLOSURE6, TE_CLOSURE7, + + TE_FLAG_PURE = 32 +}; + +typedef struct te_variable { + const char *name; + const void *address; + int type; + void *context; +} te_variable; + + + +/* Parses the input expression, evaluates it, and frees it. */ +/* Returns NaN on error. */ +double te_interp(const char *expression, int *error); + +/* Parses the input expression and binds variables. */ +/* Returns NULL on error. */ +te_expr *te_compile(const char *expression, const te_variable *variables, int var_count, int *error); + +/* Evaluates the expression. */ +double te_eval(const te_expr *n); + +/* Prints debugging information on the syntax tree. */ +void te_print(const te_expr *n); + +/* Frees the expression. */ +/* This is safe to call on NULL pointers. */ +void te_free(te_expr *n); + + +#ifdef __cplusplus +} +#endif + +#endif /*__TINYEXPR_H__*/ diff --git a/demos/workflow/solver/simcore.io/do_postprocess b/demos/workflow/solver/simcore.io/do_postprocess new file mode 100644 index 00000000..b3917445 --- /dev/null +++ b/demos/workflow/solver/simcore.io/do_postprocess @@ -0,0 +1,2 @@ +#!/bin/bash +echo 'Running Postprocess' diff --git a/demos/workflow/solver/simcore.io/do_preprocess b/demos/workflow/solver/simcore.io/do_preprocess new file mode 100644 index 00000000..20eb2f44 --- /dev/null +++ b/demos/workflow/solver/simcore.io/do_preprocess @@ -0,0 +1,7 @@ +#!/bin/bash +arg1=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="xmin") .value') +arg2=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="xmax") .value') +arg3=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="N") .value') +echo $arg1 > $INPUT_FOLDER/input +echo $arg2 >> $INPUT_FOLDER/input +echo $arg3 >> $INPUT_FOLDER/input diff --git a/demos/workflow/solver/simcore.io/do_process b/demos/workflow/solver/simcore.io/do_process new file mode 100644 index 00000000..3f2f31cd --- /dev/null +++ b/demos/workflow/solver/simcore.io/do_process @@ -0,0 +1,10 @@ +#!/bin/bash +arg1=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="xmin") .value') +arg2=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="xmax") .value') +arg3=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="N") .value') +arg4=$(cat $INPUT_FOLDER/input.json | jq '.[] | select(.name =="func") .value') +temp="${arg4%\"}" +temp="${temp#\"}" +arg5=$OUTPUT_FOLDER/output + +./test $arg1 $arg2 $arg3 $temp $arg5 > $LOG_FOLDER/log.dat diff --git a/demos/workflow/solver/simcore.io/postprocess b/demos/workflow/solver/simcore.io/postprocess new file mode 100644 index 00000000..2033cb59 --- /dev/null +++ b/demos/workflow/solver/simcore.io/postprocess @@ -0,0 +1,2 @@ +#!/bin/bash +/bin/bash do_postprocess diff --git a/demos/workflow/solver/simcore.io/preprocess b/demos/workflow/solver/simcore.io/preprocess new file mode 100644 index 00000000..20a8fbe8 --- /dev/null +++ b/demos/workflow/solver/simcore.io/preprocess @@ -0,0 +1,2 @@ +#!/bin/bash +/bin/bash do_preprocess diff --git a/demos/workflow/solver/simcore.io/process b/demos/workflow/solver/simcore.io/process new file mode 100644 index 00000000..3af43012 --- /dev/null +++ b/demos/workflow/solver/simcore.io/process @@ -0,0 +1,2 @@ +#!/bin/bash +/bin/bash do_process diff --git a/demos/workflow/solver/simcore.io/run b/demos/workflow/solver/simcore.io/run new file mode 100644 index 00000000..64e9efc3 --- /dev/null +++ b/demos/workflow/solver/simcore.io/run @@ -0,0 +1,5 @@ +#!/bin/bash +/bin/bash do_preprocess +/bin/bash do_process +/bin/bash do_postprocess +