├── .travis.yml ├── Makefile ├── README.md ├── meta ├── make_page.py └── process.py ├── paper.tex ├── requirements.txt └── run.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | branches: 4 | only: 5 | - master 6 | python: 7 | - '3.5' 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install ghp-import 11 | script: 12 | - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && make publish' 13 | notifications: 14 | email: false 15 | env: 16 | global: 17 | secure: Lgoz3bRQA2V9lWWasGrqzZJLXJJZEpI10RIGE67oeYI7/3ubgG95fEh9q7+eHrap/Rdf34xuZ6fZaBBuMl8TxotQpil592ONtVU6FibOeJiCGH2UEyGccL6f7ScLmFFIeDFP/O55KeNKJPwe6j1JhiHw/uSBQUpMw4y+5faUiBI1Ztz6VeOeOVHdnRJMAHCMVN2fYI/UBVWn38HqHbboQnTxGhgpmK+dhjr3Lev2tMbvtXEVG/Yq7ilUI9bzVMTzK2LnOAk64C/bKix/wRDhN1dMP705XkzIiNzlFUwa/bAWOaAdhcGGUtFsY8PZZP75GbBL4j3ftrkZ5goK5CY23IjUN10cgWvvp+GFeY2be8+qfU589sOdPweisyUXqfbpF4ti64i2bNRZQEj5Bez3Kpmbq7sWV3qh2UkVaRV1rwEwIYCuTYj3RxVUWnbGy6Venb0TeYWv3OyrSGQ4/3zz5mgp7aI9PgTOf+4cX94rM9xSg7anmS59N269XLfY491ys4JUfdVwU0HIlXzb4Rw/gcepw0MZz/OJiGvtQdx19X16FuwGoK+an4m/M0zYHFcNj15APpKqFiZD68lK5c/NfShabkGLyoKRwtODZbHVwSz4kANcfYC4xJ8H16aZa1Gvy9IQqVpZvH4BbkV9Eg9v3+fTcvH+NryUjV4K4eLwwW4= 18 | addons: 19 | apt: 20 | packages: 21 | - texlive-latex-recommended 22 | - texlive-latex-extra 23 | - texlive-extra-utils 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | target=$(shell git rev-parse HEAD) 3 | branch=$(shell git rev-parse --abbrev-ref HEAD) 4 | message=$(shell git log -n 1 --pretty=format:%s) 5 | docker_run=docker run -ti --rm -v $${PWD}:/root 6 | 7 | paper.pdf: paper.tmp.tex 8 | ${docker_run} leodido/texlive bash -c "cd /root; lualatex --jobname=paper paper.tmp.tex" 9 | 10 | paper.tmp.tex: paper.tex 11 | ${docker_run} python bash -c "cd /root; pip install --user -r requirements.txt; python meta/process.py run.py paper.tex" 12 | 13 | clean: 14 | ${docker_run} busybox bash -c "cd /root; rm *.pdf *.aux *.log paper.tmp.tex" 15 | 16 | publish: paper.pdf 17 | @git clone -b gh-pages https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git _deploy 18 | cp paper.pdf _deploy/data/${target}.pdf 19 | cp .meta/make_page.py _deploy 20 | git config --global user.email "" 21 | git config --global user.name "Automatic travis commit" 22 | cd _deploy; echo ${target},${message},data/${target}.pdf >> entries.csv 23 | cd _deploy; python make_page.py 24 | cd _deploy; git add entries.csv index.html ./data/${target}.pdf 25 | cd _deploy; git commit -m "Add automatically to gh-pages" || true 26 | @cd _deploy; git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Fully reproducible research paper example 3 | 4 | This is an example of a fully reproducible research paper using Github, Python, jinja2 and travis. 5 | On each `git push`, travis checks out this repository and executes the `run.py` script. 6 | 7 | The variables defined in that script are then made available to the jinja2 template `paper.tex` (which contains the LaTeX code for a research paper, with template fields for values that should be filled in from the script). 8 | Travis fills in the template and compiles `paper.tex` into a PDF file, which it then uploads to the `gh-pages` branch of this repository. (See https://ibab.github.io/fully-reproducible for the rendered web page) 9 | This means that each stage of the analysis is represented as a git commit with a corresponding PDF file. 10 | 11 | Once all researchers agree on a commit that should represent the final version of their paper, they can use `git tag` to mark it as the official final version. 12 | When the paper is in review, corrections can in turn be marked with `git tag`. 13 | 14 | All of this makes it very easy for researchers to collaborate. 15 | For example, they can make use of Github's pull request feature to enable other researchers in their group to have a look at what has been changed and what the result in the paper is before they accept the change. 16 | This not only makes it more likely that mistakes are spotted early on, but also increases the effectiveness of the group ("Why don't you make use of X?"). 17 | 18 | It also makes it possible for outsiders (machine learning experts, software engineers, laymen, …) to easily discover and contribute to research projects. 19 | Ideally, a fully reproducible research paper should be public from day 1! 20 | 21 | The idea outlined here is a very basic approach that can already be realized if the computational effort needed for the analysis is not too large (can run on a single travis node). 22 | With some work, it should be possible to automatically launch a cluster of nodes on a cloud provides like AWS instead. 23 | This should allow for arbitrarily difficult computations and should also make it easier for outsiders to verify the analysis by launching their own nodes. 24 | 25 | -------------------------------------------------------------------------------- /meta/make_page.py: -------------------------------------------------------------------------------- 1 | 2 | import csv 3 | import os 4 | 5 | template = """ 6 | 7 | 8 | Paper overview 9 | 10 | 11 | 12 |
13 |

Paper overview

14 | 15 | 16 | 17 | 18 | 19 | {} 20 |
messagePDF
21 |
22 | 23 | 24 | """ 25 | 26 | contents = [] 27 | 28 | with open('./entries.csv') as f: 29 | for hsh, msg, link in list(csv.reader(f))[::-1]: 30 | item = '{}pdf'.format(msg, link) 31 | contents.append(item) 32 | 33 | page = template.format("\n".join(contents)) 34 | 35 | with open('index.html', 'w') as f: 36 | f.write(page) 37 | 38 | -------------------------------------------------------------------------------- /meta/process.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import shelve 4 | import re 5 | from jinja2 import Environment, FileSystemLoader, StrictUndefined 6 | from uncertainties import ufloat 7 | 8 | env = Environment( 9 | loader=FileSystemLoader('.'), 10 | undefined=StrictUndefined, 11 | ) 12 | 13 | LATEX_SUBS = ( 14 | (re.compile(r'\\'), r'\\textbackslash'), 15 | (re.compile(r'([{}_#%&$])'), r'\\\1'), 16 | (re.compile(r'~'), r'\~{}'), 17 | (re.compile(r'\^'), r'\^{}'), 18 | (re.compile(r'"'), r"''"), 19 | (re.compile(r'\.\.\.+'), r'\\ldots'), 20 | ) 21 | 22 | def escape_tex(value): 23 | newval = value 24 | for pattern, replacement in LATEX_SUBS: 25 | newval = pattern.sub(replacement, newval) 26 | return newval 27 | 28 | def si(value): 29 | if isinstance(value, tuple): 30 | value = ufloat(value[0], value[1]) 31 | return "{}".format(value).replace("+/-", "\\pm").replace('(', ' ').replace(')', ' ') 32 | 33 | def table_fmt(values, err=None, figures=None): 34 | before = 0 35 | after = 0 36 | if err is None: 37 | for v in values: 38 | pre, post = str(round(v), figures).split('.') 39 | if len(pre) > before: 40 | before = len(pre) 41 | if len(post) > after: 42 | after = len(post) 43 | return '{}.{}'.format(before, after) 44 | else: 45 | error = 0 46 | for v, e in zip(values, err): 47 | vv, ee = str(ufloat(v, e)).split('+/-') 48 | v_pre, v_post = vv.split('.') 49 | e_pre, e_post = ee.split('.') 50 | 51 | if len(v_pre) > before: 52 | before = len(v_pre) 53 | if len(v_post) > after: 54 | after = len(v_post) 55 | if len(e_post) > error: 56 | error = len(e_post) 57 | 58 | return "{}.{}({})".format(before, after, error) 59 | 60 | env.block_start_string = '#<' 61 | env.block_end_string = '>#' 62 | env.variable_start_string = '<<' 63 | env.variable_end_string = '>>' 64 | env.comment_start_string = '#=' 65 | env.comment_end_string = '=#' 66 | env.filters['escape_tex'] = escape_tex 67 | env.filters['si'] = si 68 | paper = sys.argv[-1] 69 | template = env.get_template(paper) 70 | 71 | db = dict() 72 | 73 | scripts = sys.argv[1:-1] 74 | for scr in scripts: 75 | with open(scr) as f: 76 | code = compile(f.read(), 'test.py', 'exec') 77 | exec(code, globals(), locals()) 78 | 79 | db = locals() 80 | db['table_fmt'] = table_fmt 81 | db['map'] = map 82 | db['hex'] = hex 83 | 84 | text = template.render(**db) 85 | with open('paper.tmp.tex', 'w') as f: 86 | f.write(text) 87 | 88 | -------------------------------------------------------------------------------- /paper.tex: -------------------------------------------------------------------------------- 1 | 2 | \documentclass{article} 3 | 4 | \usepackage{booktabs} 5 | 6 | \begin{document} 7 | 8 | \section{First section} 9 | 10 | Using the function $f(x) = x^5$, we have $f(<< answer >>) = << answer**5 >>$. 11 | 12 | \begin{table} 13 | \centering 14 | \caption{An example table} 15 | \begin{tabular}{l l} 16 | \toprule 17 | a & b \\ 18 | \midrule 19 | #< for a, b in data ># 20 | << a >> & << b >> \\ 21 | #< endfor ># 22 | \bottomrule 23 | \end{tabular} 24 | \end{table} 25 | 26 | \end{document} 27 | 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2 2 | uncertainties 3 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | answer = 42 4 | 5 | data = [(1, 2), (3, 4), (5, 6), (7, 8)] 6 | --------------------------------------------------------------------------------