From d800950bfaacab9c6c4bf4d44206d60016b2f04d Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Wed, 21 Oct 2020 03:02:35 +0000 Subject: CTAN sync 202010210302 --- macros/latex/contrib/jupynotex/LICENSE | 201 +++++++ macros/latex/contrib/jupynotex/README.md | 73 +++ macros/latex/contrib/jupynotex/example/build | 5 + macros/latex/contrib/jupynotex/example/example.tex | 27 + .../latex/contrib/jupynotex/example/notebook.ipynb | 593 +++++++++++++++++++++ macros/latex/contrib/jupynotex/jupynotex.py | 166 ++++++ macros/latex/contrib/jupynotex/jupynotex.sty | 7 + macros/latex/contrib/jupynotex/tests/run | 3 + .../contrib/jupynotex/tests/test_cellparser.py | 79 +++ macros/latex/contrib/jupynotex/tests/test_main.py | 111 ++++ .../latex/contrib/jupynotex/tests/test_notebook.py | 237 ++++++++ 11 files changed, 1502 insertions(+) create mode 100644 macros/latex/contrib/jupynotex/LICENSE create mode 100644 macros/latex/contrib/jupynotex/README.md create mode 100755 macros/latex/contrib/jupynotex/example/build create mode 100644 macros/latex/contrib/jupynotex/example/example.tex create mode 100644 macros/latex/contrib/jupynotex/example/notebook.ipynb create mode 100644 macros/latex/contrib/jupynotex/jupynotex.py create mode 100644 macros/latex/contrib/jupynotex/jupynotex.sty create mode 100755 macros/latex/contrib/jupynotex/tests/run create mode 100644 macros/latex/contrib/jupynotex/tests/test_cellparser.py create mode 100644 macros/latex/contrib/jupynotex/tests/test_main.py create mode 100644 macros/latex/contrib/jupynotex/tests/test_notebook.py (limited to 'macros/latex/contrib/jupynotex') diff --git a/macros/latex/contrib/jupynotex/LICENSE b/macros/latex/contrib/jupynotex/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/macros/latex/contrib/jupynotex/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/macros/latex/contrib/jupynotex/README.md b/macros/latex/contrib/jupynotex/README.md new file mode 100644 index 0000000000..d19b75542c --- /dev/null +++ b/macros/latex/contrib/jupynotex/README.md @@ -0,0 +1,73 @@ +# What is Jupynotex? + +A Jupyter Notebook to LaTeX translator to include whole or partial notebooks in your papers. + +## Wait, what? + +A TeX package that you can use in your project to include Jupyter Notebooks (all of them, or some specific cells) as part of your text. + +It will convert the Jupyter Notebook format to proper LaTeX so it gets included seamless, supporting text, latex, images, etc. + + +# How To Use? + +All you need to do is include the `jupynotex.py` and `jupynotex.sty` files in your LaTeX project, and use the package from your any of your `.tex` files: + + \usepackage{jupynotex} + +After that, you can include a whole Jupyter Notebook in your file just specifying it's file name: + + \jupynotex{file_name_for_your_notebook.ipynb} + +If you do not want to include it completely, you can optionally specify which cells: + + \jupynotex[]{sample.ipynb} + +The cells specification can be numbers separated by comma, or ranges using dashes (defaulting to first and last if any side is not included). + +Examples: + +- include the whole *foobar* notebook: + + `\jupynotex{foobar.ipynb}` + +- include just the cell #7: + + `\jupynotex[7]{sample.ipynb}` + +- include cells 1, 3, and 6, 7, and 8 from the range: + + `\jupynotex[1,3,6-8]{sample.ipynb}` + +- include everything up to the fourth cell, and the eigth: + + `\jupynotex[-4,8]{whatever.ipynb}` + +- include the cell number 3, and from 12 to the notebook's end + + `\jupynotex[3,12-]{somenote.ipynb}` + + +## Full Example + +Check the `example` directory in this project. + +There you will find an example `notebook.ipynb`, an `example.tex` file that includes cells from that notebook in different ways, and a `build` script. + +Play with it. Enjoy. + + +# Dependencies + +You need Python 3 in your system, and the [tcolorbox](https://ctan.org/pkg/tcolorbox) module in your LaTeX toolbox. + + +# Feedback & Development + +Please open any issue or ask any question [here](https://github.com/facundobatista/jupynotex/issues/new). + +To run the tests (need to have [fades](https://github.com/pyar/fades) installed): + + ./tests/run + +This material is subject to the Apache 2.0 license. diff --git a/macros/latex/contrib/jupynotex/example/build b/macros/latex/contrib/jupynotex/example/build new file mode 100755 index 0000000000..51598c0450 --- /dev/null +++ b/macros/latex/contrib/jupynotex/example/build @@ -0,0 +1,5 @@ +#!/bin/sh + +ln -s ../jupynotex.py . +ln -s ../jupynotex.sty . +xelatex -shell-escape example.tex diff --git a/macros/latex/contrib/jupynotex/example/example.tex b/macros/latex/contrib/jupynotex/example/example.tex new file mode 100644 index 0000000000..5251586b63 --- /dev/null +++ b/macros/latex/contrib/jupynotex/example/example.tex @@ -0,0 +1,27 @@ +\documentclass{article} + +\usepackage{jupynotex} + +\begin{document} + +One cell: + +\jupynotex[1]{notebook.ipynb} + + +A range of cells: + +\jupynotex[4-6]{notebook.ipynb} + + +Some specific cells: + +\jupynotex[12,17]{notebook.ipynb} + + +The whole notebook: + +\jupynotex{notebook.ipynb} + + +\end{document} diff --git a/macros/latex/contrib/jupynotex/example/notebook.ipynb b/macros/latex/contrib/jupynotex/example/notebook.ipynb new file mode 100644 index 0000000000..a423e9a11f --- /dev/null +++ b/macros/latex/contrib/jupynotex/example/notebook.ipynb @@ -0,0 +1,593 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Non alphanums {'>', '\\t', '[', '\\r', '\\x13', '\\x1d', '<', '^', '\\x04', '\\x08', '\\x17', '\\x1b', '\\\\', '\\x19', '`', '\\x1f', '$', '\\x0b', '\\x14', '\\x12', '\\x18', ')', '=', '?', ' ', \"'\", '\\x1c', '|', ';', '/', '\\x1e', '\\n', '\\x07', '\\x10', '\\x03', '\\x02', '#', '\\x0c', '@', '\\x16', '_', '}', '.', '-', '(', '!', '+', '\\x06', ']', '{', ':', '\\x01', '\\x11', '\\x0f', '\\x05', ',', '~', '\\x15', '\\x00', '\"', '%', '\\x0e', '*', '&', '\\x1a'}\n", + "Separators b'([\\\\>\\\\\\t\\\\[\\\\\\r\\\\\\x13\\\\\\x1d\\\\<\\\\^\\\\\\x04\\\\\\x08\\\\\\x17\\\\\\x1b\\\\\\\\\\\\\\x19\\\\`\\\\\\x1f\\\\$\\\\\\x0b\\\\\\x14\\\\\\x12\\\\\\x18\\\\)\\\\=\\\\?\\\\ \\\\\\'\\\\\\x1c\\\\|\\\\;\\\\/\\\\\\x1e\\\\\\n\\\\\\x07\\\\\\x10\\\\\\x03\\\\\\x02\\\\#\\\\\\x0c\\\\@\\\\\\x16\\\\_\\\\}\\\\.\\\\-\\\\(\\\\!\\\\+\\\\\\x06\\\\]\\\\{\\\\:\\\\\\x01\\\\\\x11\\\\\\x0f\\\\\\x05\\\\,\\\\~\\\\\\x15\\\\\\x00\\\\\"\\\\%\\\\\\x0e\\\\*\\\\&\\\\\\x1a])'\n" + ] + } + ], + "source": [ + "import string\n", + "\n", + "non_alphanums = set(chr(x) for x in range(127)) - set(string.ascii_letters) - set(string.digits)\n", + "print(\"Non alphanums\", non_alphanums)\n", + "separators = '([' + ''.join('\\\\' + x for x in non_alphanums) + '])'\n", + "separators = separators.encode('ascii')\n", + "print(\"Separators\", separators)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[b'dlg',\n", + " b'=',\n", + " b'Resource',\n", + " b'.',\n", + " b'loadfromresfile',\n", + " b'(',\n", + " b'filename',\n", + " b',',\n", + " b'win',\n", + " b',',\n", + " b'QuoteDialog',\n", + " b'.',\n", + " b'MyQuoteDialog',\n", + " b',',\n", + " b\"'\",\n", + " b'QuoteDialog',\n", + " b\"'\",\n", + " b',',\n", + " b'win',\n", + " b')']" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import re\n", + "program_line = b\"\"\"dlg = Resource.loadfromresfile(filename, win, QuoteDialog.MyQuoteDialog, 'QuoteDialog', win)\"\"\"\n", + "tokens = [t for x in re.split(separators, program_line) if (t := x.strip())]\n", + "tokens" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Checking base dir: HTML document\n", + "Checking base dir: C source\n", + "Checking base dir: C++ source\n", + "Checking base dir: PHP script\n", + "Checking base dir: ReStructuredText file\n", + "Checking base dir: Python script\n", + "Checking base dir: Ruby script\n", + "Checking base dir: Java source\n", + "Checking base dir: Objective-C source\n", + "Checking base dir: Perl5 module source\n", + "Checking base dir: XML 1.0 document\n" + ] + } + ], + "source": [ + "import os\n", + "from collections import Counter\n", + "\n", + "DUMP_BASE = '/home/facundo/devel/ml/dump'\n", + "\n", + "# directories with 1000 files of each code type, excluding \"just text\" (ascii, utf8, etc)\n", + "CODE_TYPES = [\n", + " 'HTML document',\n", + " 'C source',\n", + " 'C++ source',\n", + " 'PHP script',\n", + " 'ReStructuredText file',\n", + " 'Python script',\n", + " 'Ruby script',\n", + " 'Java source',\n", + " 'Objective-C source',\n", + " 'Perl5 module source',\n", + " 'XML 1.0 document',\n", + "]\n", + "\n", + "# let's collect ALL tokens present in all the program files\n", + "tokens = Counter()\n", + "for basedir in CODE_TYPES:\n", + " print(\"Checking base dir:\", basedir)\n", + " for dirpath, dirnames, filenames in os.walk(os.path.join(DUMP_BASE, basedir)):\n", + " for fname in filenames:\n", + " fpath = os.path.join(dirpath, fname)\n", + " with open(fpath, 'rb') as fh:\n", + " tokens.update(t for x in re.split(separators, fh.read()) if (t := x.strip()))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total different tokens 213400\n", + " 870525 b'.'\n", + " 756849 b'_'\n", + " 730609 b'('\n", + " 717433 b'='\n", + " 699725 b')'\n", + " 688556 b'/'\n", + " 661461 b'\"'\n", + " 640989 b','\n", + " 625121 b'-'\n", + " 594091 b'>'\n" + ] + } + ], + "source": [ + "different_tokens = len(tokens)\n", + "print(\"Total different tokens\", different_tokens)\n", + "for name, quant in tokens.most_common(10):\n", + " print(\"{:8d} {}\".format(quant, name))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total tokens found: 20101050\n", + "Total representative: 3324\n", + "Last ten...\n", + " 289 b'enables'\n", + " 289 b'smaller'\n", + " 289 b'Creates'\n", + " 289 b'cross'\n", + " 289 b'GLFW'\n", + " 289 b'Os'\n", + " 289 b'usb'\n", + " 288 b'stylesheets'\n", + " 288 b'ad'\n", + " 288 b'WIDTH'\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "total_tokens = sum(tokens.values())\n", + "print(\"Total tokens found:\", total_tokens)\n", + "\n", + "most = total_tokens * 0.9\n", + "tot = 0\n", + "representative_data = []\n", + "for name, quant in tokens.most_common():\n", + " representative_data.append((name, quant))\n", + " tot += quant\n", + " if tot > most:\n", + " break\n", + "\n", + "print(\"Total representative:\", len(representative_data))\n", + "print(\"Last ten...\")\n", + "for name, quant in representative_data[-10:]:\n", + " print(\"{:8d} {}\".format(quant, name))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting data from base dir HTML document\n", + "Collecting data from base dir C source\n", + "Collecting data from base dir C++ source\n", + "Collecting data from base dir PHP script\n", + "Collecting data from base dir ReStructuredText file\n", + "Collecting data from base dir Python script\n", + "Collecting data from base dir Ruby script\n", + "Collecting data from base dir Java source\n", + "Collecting data from base dir Objective-C source\n", + "Collecting data from base dir Perl5 module source\n", + "Collecting data from base dir XML 1.0 document\n", + "Src data samples: 11000\n" + ] + } + ], + "source": [ + "import random\n", + "\n", + "# real \"ML data\": a list of (code_type, features) (one pair for each file)\n", + "# code_type: the *position* of the code type corresponding to the file (needs to be an int)\n", + "# features: a list of values, each value corresponds to how many of the tokens of that position the file has\n", + "\n", + "representative_tokens = [name for name, _ in representative_data]\n", + "\n", + "all_src_data = []\n", + "for idx, basedir in enumerate(CODE_TYPES):\n", + " print(\"Collecting data from base dir\", basedir)\n", + " for dirpath, dirnames, filenames in os.walk(os.path.join(DUMP_BASE, basedir)):\n", + " for fname in filenames:\n", + " fpath = os.path.join(dirpath, fname)\n", + " with open(fpath, 'rb') as fh:\n", + " fcontent = fh.read()\n", + " \n", + " file_tokens = Counter(t for x in re.split(separators, fcontent) if (t := x.strip()))\n", + " token_quantities = [file_tokens.get(t, 0) for t in representative_tokens]\n", + "\n", + " all_src_data.append((idx, token_quantities))\n", + "\n", + "print(\"Src data samples:\", len(all_src_data))\n", + "\n", + "# shuffle, as currently is too much \"per directory\"\n", + "random.shuffle(all_src_data)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "from tensorflow.keras import Model, layers\n", + "import numpy as np\n", + "\n", + "# representation of our model\n", + "num_classes = len(CODE_TYPES)\n", + "num_features = len(representative_tokens)\n", + "\n", + "# 1st and 2nd layer number of neurons (these numbers are just chamuyo)\n", + "n_hidden_1 = 128 \n", + "n_hidden_2 = 256\n", + "\n", + "# training parameters (more chamuyo)\n", + "learning_rate = 0.1\n", + "\n", + "batch_size = 256\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Example code types: (2, 0, 4, 4, 5)\n", + "Example token quants: [246, 786, 246, 61, 246] [32, 32, 5, 22, 5]\n", + "Token quants shape: (11000, 3324)\n", + "Example normalized quants: [0.3129771 1. 0.3129771 0.07760815 0.3129771 ] [0.9411765 0.9411765 0.14705883 0.64705884 0.14705883]\n" + ] + } + ], + "source": [ + "# separate the source data and into two pairing lists\n", + "code_types, token_quantities = zip(*all_src_data)\n", + "print(\"Example code types:\", code_types[:5])\n", + "print(\"Example token quants:\", token_quantities[0][:5], token_quantities[117][:5])\n", + "\n", + "# convert features to float\n", + "float_quantities = np.array(token_quantities, np.float32)\n", + "print(\"Token quants shape:\", float_quantities.shape)\n", + "\n", + "# normalize EACH ONE to [0, 1]\n", + "for quants in float_quantities:\n", + " quants /= max(quants)\n", + "print(\"Example normalized quants:\", float_quantities[0][:5], float_quantities[117][:5])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Real training set: 89.70%\n" + ] + } + ], + "source": [ + "# let's prepare the teching sets, input and output for training first (90% of cases) and\n", + "# then testing what's learned (the remaining 10%); \n", + "input_training = []\n", + "output_training = []\n", + "input_testing = []\n", + "output_testing = []\n", + "for token_distribution, code_type in zip(float_quantities, code_types):\n", + " if random.random() < .1:\n", + " input_testing.append(token_distribution)\n", + " output_testing.append(code_type)\n", + " else:\n", + " input_training.append(token_distribution)\n", + " output_training.append(code_type)\n", + "print(\"Real training set: {:.2f}%\".format(100 * len(input_training) / len(float_quantities))) " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import tensorflow as tf\n", + "from tensorflow.keras import Model\n", + "\n", + "class NeuralNet(Model):\n", + " \"\"\"Chamuyo al cuadrado.\"\"\"\n", + " \n", + " def __init__(self):\n", + " super(NeuralNet, self).__init__()\n", + " self.fc1 = layers.Dense(n_hidden_1, activation=tf.nn.sigmoid) # se puede cambiar a relu\n", + " self.fc2 = layers.Dense(n_hidden_2, activation=tf.nn.sigmoid) # se puede cambiar a relu\n", + " self.out = layers.Dense(num_classes)\n", + "\n", + " def call(self, x, is_training=False):\n", + " x = self.fc1(x)\n", + " x = self.fc2(x)\n", + " x = self.out(x)\n", + " if not is_training:\n", + " # tf cross entropy expect logits without softmax, so only\n", + " # apply softmax when not training.\n", + " x = tf.nn.softmax(x)\n", + " return x\n", + "\n", + "neural_net = NeuralNet()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# Note that this will apply 'softmax' to the logits.\n", + "def cross_entropy_loss(x, y):\n", + " # Convert labels to int 64 for tf cross-entropy function.\n", + " y = tf.cast(y, tf.int64)\n", + " # Apply softmax to logits and compute cross-entropy.\n", + " loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=x)\n", + " # Average loss across the batch.\n", + " return tf.reduce_mean(loss)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# Stochastic gradient descent optimizer.\n", + "optimizer = tf.optimizers.SGD(learning_rate)\n", + "\n", + "# Optimización. \n", + "def run_optimization(x, y):\n", + " # Funciones para calcular el gradiente\n", + " with tf.GradientTape() as g:\n", + " # Algoritmo de forward\n", + " pred = neural_net(x, is_training=True)\n", + " # Computa la función de costo o pérdida utilizando entropía cruzada\n", + " loss = cross_entropy_loss(pred, y)\n", + " \n", + " # Actualiza las variables de entrenamiento.\n", + " trainable_variables = neural_net.trainable_variables\n", + "\n", + " # Computa los gradientes\n", + " gradients = g.gradient(loss, trainable_variables)\n", + " \n", + " # Actualiza los nuevos parámetros W (pesos) y b (bias).\n", + " optimizer.apply_gradients(zip(gradients, trainable_variables))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Accuracy metric.\n", + "def accuracy(y_pred, y_true):\n", + " # Predicted class is the index of highest score in prediction vector (i.e. argmax).\n", + " correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.cast(y_true, tf.int64))\n", + " return tf.reduce_mean(tf.cast(correct_prediction, tf.float32), axis=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Step 100: loss=2.379924, accuracy=0.125000\n", + "Step 200: loss=2.372284, accuracy=0.128906\n", + "Step 300: loss=2.366430, accuracy=0.140625\n", + "Step 400: loss=2.360768, accuracy=0.121094\n", + "Step 500: loss=2.347394, accuracy=0.109375\n", + "Step 600: loss=2.349795, accuracy=0.199219\n", + "Step 700: loss=2.312950, accuracy=0.218750\n", + "Step 800: loss=2.284508, accuracy=0.289062\n", + "Step 900: loss=2.173395, accuracy=0.335938\n", + "Step 1000: loss=2.115966, accuracy=0.300781\n", + "Step 1100: loss=1.977837, accuracy=0.441406\n", + "Step 1200: loss=1.860783, accuracy=0.425781\n", + "Step 1300: loss=1.866206, accuracy=0.425781\n", + "Step 1400: loss=1.773057, accuracy=0.402344\n", + "Step 1500: loss=1.736271, accuracy=0.546875\n", + "Step 1600: loss=1.626320, accuracy=0.578125\n", + "Step 1700: loss=1.537970, accuracy=0.539062\n", + "Step 1800: loss=1.369012, accuracy=0.609375\n", + "Step 1900: loss=1.286771, accuracy=0.625000\n", + "Step 2000: loss=1.270916, accuracy=0.640625\n" + ] + } + ], + "source": [ + "train_data = tf.data.Dataset.from_tensor_slices((input_training, output_training))\n", + "\n", + "# NOTE: this doesn't only selectes, it completely transform the structures\n", + "# from \n", + "# to \n", + "train_data = train_data.repeat().shuffle(5000).batch(batch_size).prefetch(1)\n", + "\n", + "display_step = 100\n", + "training_steps = 2000\n", + "\n", + "# Run training for the given number of steps.\n", + "for step, (input_batch, output_batch) in enumerate(train_data.take(training_steps), 1):\n", + " # Run the optimization to update W and b values.\n", + " run_optimization(input_batch, output_batch)\n", + " \n", + " if step % display_step == 0:\n", + " pred = neural_net(input_batch, is_training=True)\n", + " loss = cross_entropy_loss(pred, output_batch)\n", + " acc = accuracy(pred, output_batch)\n", + " print(\"Step {}: loss={:f}, accuracy={:f}\".format(step, loss, acc))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test Accuracy: 0.578994\n" + ] + } + ], + "source": [ + "# Test model on validation set\n", + "input_testing = np.array(input_testing)\n", + "pred = neural_net(input_testing, is_training=False)\n", + "print(\"Test Accuracy: {:f}\".format(accuracy(pred, output_testing)))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# If we consider the \"more probable\" prediction on each item, how well it goes?\n", + "all_guesses = neural_net(input_testing, is_training=False)\n", + "guesses_ok = [0] * len(CODE_TYPES)\n", + "guesses_bad = [0] * len(CODE_TYPES)\n", + "\n", + "for guess, real in zip(all_guesses, output_testing):\n", + " # guess is an array of len(CODE_TYPES) with a float in each position showing\n", + " # which one is the most probable to be real, so we need to get position \n", + " # for the max one and check if it matches with the real real :)\n", + " position_for_max = np.where(guess == np.amax(guess))[0][0]\n", + " if position_for_max == real:\n", + " guesses_ok[real] += 1\n", + " else:\n", + " guesses_bad[real] += 1" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAFmCAYAAACC84ZkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAABBB0lEQVR4nO3debyc4/3/8dc7mwSJWPJNEZHUEiUbkqC2iLWqltb6pYKiliLftrZuWt+26LdVxY9GLaGUoLbqYkkT+5KECEFsDUIQscUSJD6/P65r5txnMnPOnGSue87yeT4e8zhz3/fM/bnuOTNzzbXLzHDOOecAOtU7Ac4551oPzxScc84VeabgnHOuyDMF55xzRZ4pOOecK/JMwTnnXJFnCq4RSQdLurPe6XCBpG0lzc4p1hxJOyU6dw9JD0jarYnHjJY0N7M9S9LoFOlxlXmm0M5JmiLpXUkrVPN4M7vGzHZZjngTJP1yWZ/f0UkySesXts3sPjMbVM801ch44Hdm9q9qn2Bmm5jZlHRJcuV4ptCOSRoAbAsYsGcO8TqnjtFeSOpS7zTkycwONbOb6p0O1zzPFNq3Q4GHgQnA2OwBSetIuknSfEkLJF0Y9x8m6f7M4zaSdJekdyTNlrR/5tgESRdL+oekj4DvAAcDp0j6UNLf4uO+Ekss78UqgT0z59hd0tOSFkp6TdIPy12IpE6SfiLpZUlvSbpK0ioVHjta0lxJP4iPnSfp8MzxVeLz58fz/URSVZ+FeB1nSXpU0geSbpW0Wub4nvEa34uP/Urm2BxJp0qaCXxUmjFIujfefSK+fgeUqVKZI+n0+Jq9K+kKSd0zx4+S9EL8f90maa0mruXb8foXSPpxybFOkk6T9GI8fn32Osucay9JM+Jr8mKhmkjSWjEd78R0HZV5To/4HnpX0tPAyJJzFquz4mOvjI99RtIpJa9LxfeYayEz81s7vQEvAMcBmwOfA33j/s7AE8DvgZWA7sA28dhhwP3x/krAq8DhQBdgU+BtYON4fALwPrA14QdG97jvl5k0dI3p+BHQDRgDLAQGxePzgG3j/VWBzSpcyxHxPF8GVgZuAv5c4bGjgcXAmTH+7sDHwKrx+FXArUBPYADwHPCdKl/TKcBrwOD4+vwVuDoe2xD4CNg5xj0lprlbPD4HmAGsA/SocH4D1i+5lrmZ7TnAU/EcqwEPFF7v+Nq+DWwGrABcANxbIc7GwIfAdvGx58bXbKd4/CTCD4p+8fh44NoK5xoV3wc7x/fB2sBG8di9wEXxvTEcmA+MicfOBu6L17FOvK7Sa90p89h74nukHzCz8FiaeY/5rYXfG/VOgN8S/WNhG0JGsEbcfhb4n3h/q/jh7FLmeYfRkCkcANxXcnw8cEa8PwG4quT4BBpnCtsCbwCdMvuuBX4e778CfBfo1cz1TAKOy2wPitdX7hpGA59kjwFvAVsSMsTPiBlbPPZdYEqVr+sU4OzM9sbxfJ2BnwLXZ451ImQgo+P2HOCIZs5fTaZwTGZ7d+DFeP8y4DeZYyvH12hAmTg/A67LbK8Ur6PwJfwMsGPm+JpNvN7jgd+X2b8OsATomdl3FjAh3n8J2C1z7Ogy17pT5rG7Zo4dSUOm0OR7zG8tu3n1Ufs1FrjTzN6O23+hoQppHeBlM1vczDnWBbaIRfL3JL1HqB76UuYxrzZzjrWAV83si8y+lwm/JgG+Rfhie1nSPZK2auI8L5ecowvQt8LjF5Rc38eEL8k1CL8sS8+1NtXLXvPL8XxrlKYxXvOrJedu7vValviFKqLS+B8CCyh/bWtlz2NmH8XHFqwL3Jz5vz9D+IIv93qvA7xYIcY7ZrawJL1rZ46XXkslpY99tfRYE+8x1wIdqrGro5DUA9gf6Czpjbh7BaC3pGGED1R/SV2ayRheBe4xs52beEzpNLul268D60jqlPnQ9idU2WBmU4G9JHUFvgdcT/iSKfU64YuqoD+huuPNJtJWztuEX7zrAk9nzvVaC86RTV//eL63YxqHFA5IUnxs9ty1mJa4NP7r8X6j10jSSsDqlL+2eUC2vWPF+NiCVwmlmgeqSM+rwHpl9r8OrCapZyZjyL7W8+K1zMocq2Qeodqo8D/LvgZNvsdcy3hJoX3am/CrbmNCPe5wwhfAfYTG50cJH7KzJa0kqbukrcuc53Zgw9gg2TXeRmYbT8t4k1DvX/AI4Vf6KfH5o4FvANdJ6qYwLmIVM/sc+AD4ovSE0bXA/0gaKGll4NfAxCpKO42Y2RJCxvMrST0lrQt8H7gaQo8thW6hA5o4zSGSNo5fpGcCN2bO+3VJO8ZM7gfAp8CDLUhi6etXzvGS+sWG3x8DE+P+a4HDJQ1X6IL8a+ARM5tT5hw3AntI2kZSt3gd2e+DPxJeo3UBJPWRtFeF9FwW4+4YG6jXlrSRmb1KuPaz4ntsKKEzwtXxedcDp0taVVI/4IQmrjn72LUJPyAKKr7Hmjifq6Te9Vd+q/0N+BehT3jp/v0Jda9dCL+kbiFUGbwNnB8fcxixTSFuDwL+TmiDWAD8Gxgej00g034Q921AaEx9D7gl7tuE0Ej4PuGX3j5xf7eY1ncJGcJUYoN3mbR3ItSDvxrTcjWx4bjMY0eTqZuO++bQUD+9anz+/Hi+nxHrown103OArhXOPYVQL/5oTPPfiO028fg+8Rrfj9e8Sbk0NPG/O4aQYb8X/1+NriWe4/QY4z3gSmDFkue/CLxDyNT7NRFrLKFNZwEhc8m+Rp0ImeVsQqPti8CvmzjXPoTG34WERt9d4/5+MR3vxHNk20NWJDT6vxev5+Qy11pIz0rAn+NjnwF+QmxLaeo95reW3xRfUOccIOknwHwzG1/h+BRCb6NLc01YQ/w5wJFmdnc94rcWko4FDjSz7eudlvbG2xScyzAzH43dCklak1Ct9hChNPoD4MK6Jqqd8kzBOdcWdCN0fR1IqEK6jjD+wdWYVx8555wr8t5Hzjnnitp89dEaa6xhAwYMqHcynHOuTZk+ffrbZtandH+bzxQGDBjAtGnT6p0M55xrUySVHUHu1UfOOeeKPFNwzjlX5JmCc865ojbfpuA6ls8//5y5c+eyaNGieiel3ejevTv9+vWja9eu9U6KawU8U3Btyty5c+nZsycDBgwgTELqloeZsWDBAubOncvAgQPrnRzXCnj1kWtTFi1axOqrr+4ZQo1IYvXVV/eSlytKmilIulxhjdynMvv+T9KzkmZKullS78yx0+M6rrMl7Zoyba7t8gyhtvz1dFmpSwoTgN1K9t0FDDazoYRFME4HkLQxcCBhCtzdgIskdU6cPueccxlJ2xTM7N7SxUrM7M7M5sPAvvH+XoQ1Yz8F/iPpBcKC4A+lTKNr2wac9veanm/O2V+v6nFvvPEG48aNY+rUqfTu3Zu+ffty3nnnseGGG9Y0PeVMmDCBXXbZhbXWWqv5BzvXQvVuaD6ChlWj1iZkEgVzqbDGqqSjCYt8079/Uyv4OVd7ZsY+++zD2LFjue66sLjXE088wZtvvtlsprB48WK6dOlScbsaEyZMYPDgwe0jU/j5KgnP/X66c7djdWtolvRjwhq717T0uWZ2iZmNMLMRffosNXWHc0lNnjyZrl27cswxxxT3DRs2jG222YaTTz6ZwYMHM2TIECZODL93pkyZwrbbbsuee+7JxhtvvNT2kiVLOPnkkxk5ciRDhw5l/PiG9X3OOecchgwZwrBhwzjttNO48cYbmTZtGgcffDDDhw/nk08+YdKkSWy66aYMGTKEI444gk8//TT318S1H3UpKUg6DNgD2NEa5u5+jcaLcfejZYupO5eLp556is0333yp/TfddBMzZszgiSee4O2332bkyJFst912ADz22GM89dRTDBw4kClTpjTavuSSS1hllVWYOnUqn376KVtvvTW77LILzz77LLfeeiuPPPIIK664Iu+88w6rrbYaF154Ib/97W8ZMWIEixYt4rDDDmPSpElsuOGGHHrooVx88cWMGzcu51fFtRe5lxQk7QacAuxpZh9nDt0GHChpBUkDCasrPZp3+pxbVvfffz8HHXQQnTt3pm/fvmy//fZMnToVgFGjRjUaB5DdvvPOO7nqqqsYPnw4W2yxBQsWLOD555/n7rvv5vDDD2fFFVcEYLXVVlsq5uzZsxk4cGCx2mrs2LHce++9qS/VtWNJSwqSriUsPL6GpLnAGYTeRisAd8WucA+b2TFmNkvS9YRFtxcDx5vZkpTpc25ZbLLJJtx4440tes5KK61UcdvMuOCCC9h118a9sO+4445lT6RzyyhpScHMDjKzNc2sq5n1M7PLzGx9M1vHzIbH2zGZx//KzNYzs0Fm9s+UaXNuWY0ZM4ZPP/2USy65pLhv5syZ9O7dm4kTJ7JkyRLmz5/Pvffey6hRo5o936677srFF1/M559/DsBzzz3HRx99xM4778wVV1zBxx+HAvU777wDQM+ePVm4cCEAgwYNYs6cObzwwgsA/PnPf2b77X0te7fs6t37yLnlUm0X0lqSxM0338y4ceM455xz6N69OwMGDOC8887jww8/ZNiwYUjiN7/5DV/60pd49tlnmzzfkUceyZw5c9hss80wM/r06cMtt9zCbrvtxowZMxgxYgTdunVj991359e//jWHHXYYxxxzDD169OChhx7iiiuuYL/99mPx4sWMHDmyUQO4cy3V5tdoHjFihPkiOx3HM888w1e+8pV6J6Pdqdvr6l1S60bSdDMbUbq/Y5cUUr0h/c3onGujfEI855xzRZ4pOOecK/JMwTnnXJFnCs4554o8U3DOOVfUsXsfubav1j3Iqug5tvLKK/Phhx/WNm4NnXfeeRx99NHF6TGcawkvKTjXiixevLjJ7Wqcd955xVHQzrWUZwrOLaMpU6YwevRo9t13XzbaaCMOPvhgCoNBp06dyle/+lWGDRvGqFGjWLhwIYsWLeLwww9nyJAhbLrppkyePBkI6yPsueeejBkzhh133HGp7Y8++ogjjjiCUaNGsemmm3LrrbcCsGTJEn74wx8yePBghg4dygUXXMD555/P66+/zg477MAOO+wAwLXXXsuQIUMYPHgwp556an1eLNdmePWRc8vh8ccfZ9asWay11lpsvfXWPPDAA4waNYoDDjiAiRMnMnLkSD744AN69OjBH/7wByTx5JNP8uyzz7LLLrvw3HPPAWFq7ZkzZ7LaaqsxYcKERts/+tGPGDNmDJdffjnvvfceo0aNYqedduKqq65izpw5zJgxgy5duhSn1j733HOZPHkya6yxBq+//jqnnnoq06dPZ9VVV2WXXXbhlltuYe+9967vC+daLS8pOLccRo0aRb9+/ejUqRPDhw9nzpw5zJ49mzXXXJORI0cC0KtXL7p06cL999/PIYccAsBGG23EuuuuW8wUdt5550ZTY2e377zzTs4++2yGDx/O6NGjWbRoEa+88gp333033/3ud4srt5WbWnvq1KmMHj2aPn360KVLFw4++GCfWts1yUsKzi2HFVZYoXi/c+fOy9QGAM1Prf3Xv/6VQYMGLVsinWsBLyk4V2ODBg1i3rx5xQV2Fi5cyOLFi9l222255pqw+uxzzz3HK6+8UtUX/a677soFF1xQbK94/PHHgVCaGD9+fDEjKje19qhRo7jnnnt4++23WbJkCddee61Pre2a5CUF17a1wskHu3XrxsSJEznhhBP45JNP6NGjB3fffTfHHXccxx57LEOGDKFLly5MmDChUUmjkp/+9KeMGzeOoUOH8sUXXzBw4EBuv/12jjzySJ577jmGDh1K165dOeqoo/je977H0UcfzW677cZaa63F5MmTOfvss9lhhx0wM77+9a+z11575fAquLaqY0+d7bOktjk+dXYaPnV2x1Np6myvPnLOOVfk1UeudvxXn3NtnpcUXJvT1qs8Wxt/PV2WZwquTenevTsLFizwL7IaMTMWLFhA9+7d650U10p49ZFrU/r168fcuXOZP39+vZPSbnTv3p1+/frV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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "values_ok = [guesses_ok[i] for i in range(len(CODE_TYPES))]\n", + "values_bad = [guesses_bad[i] for i in range(len(CODE_TYPES))]\n", + "x_positions = np.arange(len(CODE_TYPES))\n", + "\n", + "p1 = plt.bar(x_positions, guesses_ok)\n", + "p2 = plt.bar(x_positions, guesses_bad, bottom=guesses_ok)\n", + "\n", + "plt.ylabel('Tipo de código')\n", + "plt.title('Aciertos o no, por tipo de código')\n", + "plt.xticks(x_positions, CODE_TYPES, rotation=90)\n", + "#plt.yticks(np.arange(0, 81, 10))\n", + "plt.legend((p1[0], p2[0]), ('Correcto', 'Incorrecto'))\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/macros/latex/contrib/jupynotex/jupynotex.py b/macros/latex/contrib/jupynotex/jupynotex.py new file mode 100644 index 0000000000..036be088ce --- /dev/null +++ b/macros/latex/contrib/jupynotex/jupynotex.py @@ -0,0 +1,166 @@ +# Copyright 2020 Facundo Batista +# All Rights Reserved +# Licensed under Apache 2.0 + +"""USAGE: jupynote.py notebook.ipynb cells + + cells is a string with which cells to include, separate groups + with comma, ranges with dash (with defaults to start and end. +""" + +import base64 +import json +import sys +import tempfile +import traceback + + +def _verbatimize(lines): + """Wrap a series of lines around a verbatim indication.""" + result = [r"\begin{verbatim}"] + for line in lines: + result.append(line.rstrip()) + result.append(r"\end{verbatim}") + return result + + +def _save_content(data): + """Save the received b64encoded data to a temp file.""" + _, fname = tempfile.mkstemp(suffix='.png') + with open(fname, 'wb') as fh: + fh.write(base64.b64decode(data)) + return fname + + +class Notebook: + """The notebook converter to latex.""" + + def __init__(self, path): + with open(path, 'rt', encoding='utf8') as fh: + nb_data = json.load(fh) + + self._cells = nb_data['cells'] + + def __len__(self): + return len(self._cells) + + def _proc_src(self, content): + """Process the source of a cell.""" + source = content['source'] + result = [] + if content['cell_type'] == 'code': + result.extend(_verbatimize(source)) + elif content['cell_type'] == 'markdown': + # XXX: maybe we could parse this? + result.extend(_verbatimize(source)) + else: + raise ValueError( + "Cell type not supported when processing source: {!r}".format( + content['cell_type'])) + + return '\n'.join(result) + '\n' + + def _proc_out(self, content): + """Process the output of a cell.""" + outputs = content.get('outputs') + if not outputs: + return + + result = [] + for item in outputs: + output_type = item['output_type'] + if output_type == 'execute_result': + data = item['data'] + if 'image/png' in data: + fname = _save_content(data['image/png']) + result.append(r"\includegraphics{{{}}}".format(fname)) + elif 'text/latex' in data: + result.extend(data["text/latex"]) + else: + result.extend(_verbatimize(data["text/plain"])) + elif output_type == 'stream': + result.extend(_verbatimize(x.rstrip() for x in item["text"])) + elif output_type == 'display_data': + data = item['data'] + fname = _save_content(data['image/png']) + result.append(r"\includegraphics{{{}}}".format(fname)) + else: + raise ValueError("Output type not supported in item {!r}".format(item)) + + return '\n'.join(result) + '\n' + + def get(self, cell_idx): + """Return the content from a specific cell in the notebook. + + The content is already splitted in source and output, and converted to latex. + """ + content = self._cells[cell_idx - 1] + source = self._proc_src(content) + output = self._proc_out(content) + return source, output + + +def _parse_cells(spec, maxlen): + """Convert the cells spec to a range of ints.""" + if not spec: + raise ValueError("Empty cells spec not allowed") + if set(spec) - set('0123456789-,'): + raise ValueError( + "Found forbidden characters in cells definition (allowed digits, '-' and ',')") + + cells = set() + groups = spec.split(',') + for group in groups: + if '-' in group: + cfrom, cto = group.split('-') + cfrom = 1 if cfrom == '' else int(cfrom) + cto = maxlen if cto == '' else int(cto) + if cfrom >= cto: + raise ValueError( + "Range 'from' need to be smaller than 'to' (got {!r})".format(group)) + cells.update(range(cfrom, cto + 1)) + else: + cells.add(int(group)) + cells = sorted(cells) + + if any(x < 1 for x in cells): + raise ValueError("Cells need to be >=1") + if maxlen < cells[-1]: + raise ValueError( + "Notebook loaded of len {}, smaller than requested cells: {}".format(maxlen, cells)) + + return cells + + +def main(notebook_path, cells_spec): + """Main entry point.""" + nb = Notebook(notebook_path) + cells = _parse_cells(cells_spec, len(nb)) + + for cell in cells: + try: + src, out = nb.get(cell) + except Exception: + title = "ERROR when parsing cell {}".format(cell) + print( + r"\begin{{tcolorbox}}" + r"[colback=red!5!white,colframe=red!75!,title={{{}}}]".format(title)) + tb = traceback.format_exc() + print('\n'.join(_verbatimize(tb.split('\n')))) + print(r"\end{tcolorbox}") + continue + + print(r"\begin{{tcolorbox}}[title=Cell {{{:02d}}}]".format(cell)) + print(src) + if out: + print(r"\tcblower") + print(out) + print(r"\end{tcolorbox}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print(__doc__) + exit() + + main(*sys.argv[1:3]) diff --git a/macros/latex/contrib/jupynotex/jupynotex.sty b/macros/latex/contrib/jupynotex/jupynotex.sty new file mode 100644 index 0000000000..205343d097 --- /dev/null +++ b/macros/latex/contrib/jupynotex/jupynotex.sty @@ -0,0 +1,7 @@ +\ProvidesPackage{jupynotex}[0.1] + +\usepackage{tcolorbox} + +\newcommand{\jupynotex}[2][-]{ + \input|"python3 jupynotex.py #2 #1" +} diff --git a/macros/latex/contrib/jupynotex/tests/run b/macros/latex/contrib/jupynotex/tests/run new file mode 100755 index 0000000000..a5fa69f44d --- /dev/null +++ b/macros/latex/contrib/jupynotex/tests/run @@ -0,0 +1,3 @@ +#!/bin/sh + +PYTHONPATH=. fades -d pytest -x pytest -sv "$@" diff --git a/macros/latex/contrib/jupynotex/tests/test_cellparser.py b/macros/latex/contrib/jupynotex/tests/test_cellparser.py new file mode 100644 index 0000000000..f9108f4bb8 --- /dev/null +++ b/macros/latex/contrib/jupynotex/tests/test_cellparser.py @@ -0,0 +1,79 @@ +# Copyright 2020 Facundo Batista +# All Rights Reserved +# Licensed under Apache 2.0 + +import pytest +import re + +from jupynotex import _parse_cells + + +def test_empty(): + msg = "Empty cells spec not allowed" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('', 100) + + +def test_simple(): + r = _parse_cells('1', 100) + assert r == [1] + + +def test_several_comma(): + r = _parse_cells('1,3,5,9,7', 100) + assert r == [1, 3, 5, 7, 9] + + +def test_several_range(): + r = _parse_cells('1-9', 100) + assert r == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + +def test_several_limited(): + msg = "Notebook loaded of len 3, smaller than requested cells: [1, 2, 3, 4]" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('1-4', 3) + + +def test_range_default_start(): + r = _parse_cells('-3', 8) + assert r == [1, 2, 3] + + +def test_range_default_end(): + r = _parse_cells('5-', 8) + assert r == [5, 6, 7, 8] + + +def test_not_int(): + msg = "Found forbidden characters in cells definition (allowed digits, '-' and ',')" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('1,a', 3) + + +def test_not_positive(): + msg = "Cells need to be >=1" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('3,0', 3) + + +def test_several_mixed(): + r = _parse_cells('1,3,5-7,2,9,11-13', 80) + assert r == [1, 2, 3, 5, 6, 7, 9, 11, 12, 13] + + +def test_overlapped(): + r = _parse_cells('3,5-7,6-9,8', 80) + assert r == [3, 5, 6, 7, 8, 9] + + +def test_bad_range_equal(): + msg = "Range 'from' need to be smaller than 'to' (got '12-12')" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('12-12', 80) + + +def test_bad_range_smaller(): + msg = "Range 'from' need to be smaller than 'to' (got '3-2')" + with pytest.raises(ValueError, match=re.escape(msg)): + _parse_cells('3-2', 80) diff --git a/macros/latex/contrib/jupynotex/tests/test_main.py b/macros/latex/contrib/jupynotex/tests/test_main.py new file mode 100644 index 0000000000..fabfa4e01e --- /dev/null +++ b/macros/latex/contrib/jupynotex/tests/test_main.py @@ -0,0 +1,111 @@ +# Copyright 2020 Facundo Batista +# All Rights Reserved +# Licensed under Apache 2.0 + +import textwrap + +import jupynotex +from jupynotex import main + + +class FakeNotebook: + """Fake notebook. + + The instance supports calling (as it if were instantiated). The .get will return the + value in a dict for received key; raise it if exception. + """ + + def __init__(self, side_effects): + self.side_effects = side_effects + + def __call__(self, path): + return self + + def __len__(self): + return len(self.side_effects) + + def get(self, key): + """Return or raise the stored side effect.""" + value = self.side_effects[key] + if isinstance(value, Exception): + raise value + else: + return value + + +def test_simple_ok(monkeypatch, capsys): + fake_notebook = FakeNotebook({ + 1: ("test cell content up", "test cell content down"), + }) + monkeypatch.setattr(jupynotex, 'Notebook', fake_notebook) + + main('boguspath', '1') + expected = textwrap.dedent("""\ + \\begin{tcolorbox}[title=Cell {01}] + test cell content up + \\tcblower + test cell content down + \\end{tcolorbox} + """) + assert expected == capsys.readouterr().out + + +def test_simple_only_first(monkeypatch, capsys): + fake_notebook = FakeNotebook({ + 1: ("test cell content up", ""), + }) + monkeypatch.setattr(jupynotex, 'Notebook', fake_notebook) + + main('boguspath', '1') + expected = textwrap.dedent("""\ + \\begin{tcolorbox}[title=Cell {01}] + test cell content up + \\end{tcolorbox} + """) + assert expected == capsys.readouterr().out + + +def test_simple_error(monkeypatch, capsys): + fake_notebook = FakeNotebook({ + 1: ValueError("test problem"), + }) + monkeypatch.setattr(jupynotex, 'Notebook', fake_notebook) + + main('boguspath', '1') + + # verify the beginning and the end, as the middle part is specific to the environment + # where the test runs + expected_ini = [ + r"\begin{tcolorbox}[colback=red!5!white,colframe=red!75!,title={ERROR when parsing cell 1}]", # NOQA + r"\begin{verbatim}", + r"Traceback (most recent call last):", + ] + expected_end = [ + r"ValueError: test problem", + r"\end{verbatim}", + r"\end{tcolorbox}", + ] + out = [line for line in capsys.readouterr().out.split('\n') if line] + assert expected_ini == out[:3] + assert expected_end == out[-3:] + + +def test_multiple(monkeypatch, capsys): + fake_notebook = FakeNotebook({ + 1: ("test cell content up", "test cell content down"), + 2: ("test cell content ONLY up", ""), + }) + monkeypatch.setattr(jupynotex, 'Notebook', fake_notebook) + + main('boguspath', '1-2') + expected = textwrap.dedent("""\ + \\begin{tcolorbox}[title=Cell {01}] + test cell content up + \\tcblower + test cell content down + \\end{tcolorbox} + \\begin{tcolorbox}[title=Cell {02}] + test cell content ONLY up + \\end{tcolorbox} + """) + assert expected == capsys.readouterr().out diff --git a/macros/latex/contrib/jupynotex/tests/test_notebook.py b/macros/latex/contrib/jupynotex/tests/test_notebook.py new file mode 100644 index 0000000000..bf6511cbd3 --- /dev/null +++ b/macros/latex/contrib/jupynotex/tests/test_notebook.py @@ -0,0 +1,237 @@ +# Copyright 2020 Facundo Batista +# All Rights Reserved +# Licensed under Apache 2.0 + +import base64 +import json +import os +import pathlib +import re +import tempfile +import textwrap + +import pytest + +from jupynotex import Notebook + + +@pytest.fixture +def notebook(): + _, name = tempfile.mkstemp() + + def _f(cells): + with open(name, 'wt', encoding='utf8') as fh: + json.dump({'cells': cells}, fh) + + return Notebook(name) + + yield _f + os.unlink(name) + + +def test_empty(notebook): + nb = notebook([]) + assert len(nb) == 0 + + +def test_source_code(notebook): + rawcell = { + 'cell_type': 'code', + 'source': ['line1\n', ' line2\n'], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + src, _ = nb.get(1) + expected = textwrap.dedent("""\ + \\begin{verbatim} + line1 + line2 + \\end{verbatim} + """) + assert src == expected + + +def test_source_markdown(notebook): + rawcell = { + 'cell_type': 'markdown', + 'source': ['line1\n', ' line2\n'], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + src, _ = nb.get(1) + expected = textwrap.dedent("""\ + \\begin{verbatim} + line1 + line2 + \\end{verbatim} + """) + assert src == expected + + +def test_output_missing(notebook): + rawcell = { + 'cell_type': 'code', + 'source': [], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + assert out is None + + +def test_output_simple_executeresult_plain(notebook): + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'execute_result', + 'data': { + 'text/plain': ['default always present', 'line2'], + }, + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + expected = textwrap.dedent("""\ + \\begin{verbatim} + default always present + line2 + \\end{verbatim} + """) + assert out == expected + + +def test_output_simple_executeresult_latex(notebook): + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'execute_result', + 'data': { + 'text/latex': ['some latex line', 'latex 2'], + 'text/plain': ['default always present'], + }, + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + expected = textwrap.dedent("""\ + some latex line + latex 2 + """) + assert out == expected + + +def test_output_simple_executeresult_image(notebook): + raw_content = b"\x01\x02 asdlklda3wudghlaskgdlask" + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'execute_result', + 'data': { + 'image/png': base64.b64encode(raw_content).decode('ascii'), + 'text/plain': ['default always present'], + }, + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + m = re.match(r'\\includegraphics\{(.+)\}\n', out) + assert m + (fpath,) = m.groups() + assert pathlib.Path(fpath).read_bytes() == raw_content + + +def test_output_simple_stream(notebook): + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'stream', + 'text': ['some text line', 'text 2'], + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + expected = textwrap.dedent("""\ + \\begin{verbatim} + some text line + text 2 + \\end{verbatim} + """) + assert out == expected + + +def test_output_simple_display_data(notebook): + raw_content = b"\x01\x02 asdlklda3wudghlaskgdlask" + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'display_data', + 'data': { + 'image/png': base64.b64encode(raw_content).decode('ascii'), + }, + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + m = re.match(r'\\includegraphics\{(.+)\}\n', out) + assert m + (fpath,) = m.groups() + assert pathlib.Path(fpath).read_bytes() == raw_content + + +def test_output_multiple(notebook): + rawcell = { + 'cell_type': 'code', + 'source': [], + 'outputs': [ + { + 'output_type': 'execute_result', + 'data': { + 'text/latex': ['some latex line', 'latex 2'], + }, + }, { + 'output_type': 'stream', + 'text': ['some text line', 'text 2'], + }, + ], + } + nb = notebook([rawcell]) + assert len(nb) == 1 + + _, out = nb.get(1) + expected = textwrap.dedent("""\ + some latex line + latex 2 + \\begin{verbatim} + some text line + text 2 + \\end{verbatim} + """) + assert out == expected -- cgit v1.2.3