diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..211a8b2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,42 @@ + + +Book +---- + +This book is licensed under a Creative Commons Attribution +Non‐Commercial Share‐Alike 4.0 International License, which permits +use, sharing, adaptation, distribution and reproduction in any medium +or format, as long as you give appropriate credit to the original +author(s) and the source, provide a link to the Creative Commons +license, and indicate if changes were made. You may not use the +material for commercial purposes. If you remix, transform, or build +upon the material, you must distribute your contributions under the +same license as the original. To learn more, visit creativecommons.org + + +Code +---- + +Copyright 2021 Nicolas P. Rougier + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bfdf05e --- /dev/null +++ b/Makefile @@ -0,0 +1,182 @@ +# +# The build requires docutils 0.17 since 0.18 breaks the build process. +# +TEX_DIR = tex +PDF_DIR = pdf +RST_DIR = rst +RST_FILES := $(wildcard $(RST_DIR)/*.rst) + +DEPS := $(TEX_DIR)/book.tex \ + $(TEX_DIR)/main.tex \ + $(TEX_DIR)/00-dedication.tex \ + $(TEX_DIR)/00-preface.tex \ + $(TEX_DIR)/00-introduction.tex \ + $(TEX_DIR)/00-acknowledgments.tex \ + $(RST_FILES) + +all: directories pdf + +.PHONY: help +help: + @echo "Targets" + @echo "-------" + @echo "" + @echo " pdf - Build PDF (body + cover)" + @echo "" + @echo " hardcover - Build PDF (body + cover) for a hardcover printed book" + @echo " hardcover-cmyk - Convert hardcover PDF to CMYK colors" + @echo " hardcover-count - Enumerate color pages in the hardcover PDF (body)" + @echo "" + @echo " softcover - Build PDF (body + cover) for a softcover printed book" + @echo " softcover-cmyk - Convert softcover PDF to CMYK colors" + @echo " softcover-count - Enumerate color pages in the softcover PDF (body)" + @echo "" + + +.PHONY: directories +directories: + mkdir -p pdf + +hardcover: hardcover-body hardcover-cover + +softcover: softcover-body softcover-cover + +hardcover-cmyk: hardcover-body-cmyk hardcover-cover-cmyk + +softcover-cmyk: softcover-body-cmyk softcover-cover-cmyk + +pdf: $(DEPS) $(TEX_DIR)/front-cover.pdf $(TEX_DIR)/back-cover.pdf + @latexmk -pdfxe -cd -bibtex -shell-escape -silent -jobname="book" -pretex="" -use-make -usepretex $(TEX_DIR)/book.tex + @cp $(TEX_DIR)/book.pdf $(PDF_DIR) + +# Build front cover from the hardcover +front-cover: softcover-cover + @latexmk -cd -silent -pdf -use-make $(TEX_DIR)/front-cover.tex + +# Build back cover from the hardcover +back-cover: softcover-cover + @latexmk -cd -silent -pdf -use-make $(TEX_DIR)/back-cover.tex + + + +# Build softcover book (body) +softcover-body: $(DEPS) + @latexmk -pdfxe -cd -silent -jobname="softcover-body" -pretex="\def\softcover{1}" -use-make -usepretex $(TEX_DIR)/book.tex + @cp $(TEX_DIR)/softcover-body.pdf $(PDF_DIR) + +# Build softcover book (cover) +softcover-cover: $(TEX_DIR)/full-cover.tex + @latexmk -pdfxe -cd -silent -jobname="softcover-cover" -pretex="\def\softcover{1}" -use-make -usepretex $(TEX_DIR)/full-cover.tex + @cp $(TEX_DIR)/softcover-cover.pdf $(PDF_DIR) + +# Convert softcover book (body) to CMYK +softcover-body-cmyk: $(TEX_DIR)/softcover-body.pdf + @gs -dSAFER -dNOPAUSE -dBATCH \ + -dAutoRotatePages=/None \ + -sColorConversionStrategy=CMYK -sProcessColorModel=DeviceCMYK \ + -dOverrideICC=true -dRenderIntent=3 -dDeviceGrayToK=true \ + -sDEVICE=pdfwrite -sOutputFile=$(PDF_DIR)/softcover-body-cmyk.pdf \ + $(TEX_DIR)/softcover-body.pdf + +# Convert softcover book (body) to CMYK +softcover-cover-cmyk: $(TEX_DIR)/softcover-cover.pdf + @gs -dSAFER -dNOPAUSE -dBATCH \ + -dAutoRotatePages=/None \ + -sColorConversionStrategy=CMYK -sProcessColorModel=DeviceCMYK \ + -dOverrideICC=true -dRenderIntent=3 -dDeviceGrayToK=true \ + -sDEVICE=pdfwrite -sOutputFile=$(PDF_DIR)/softcover-cover-cmyk.pdf \ + $(TEX_DIR)/softcover-cover.pdf + +softcover-count: $(TEX_DIR)/softcover-body.pdf + @gs -o - -sDEVICE=inkcov $(TEX_DIR)/softcover-body.pdf \ + | tail -n +6 \ + | sed '/^Page*/N;s/\n//' \ + | sed -E '/Page [0-9]+ 0.00000 0.00000 0.00000 / d' \ + | cut -f 2 -d ' ' \ + | xargs -I {} echo -n {},; echo + + + +# Build hardcover book (body) +hardcover-body: $(DEPS) + @latexmk -pdfxe -cd -silent -jobname="hardcover-body" -pretex="\def\hardcover{1}" -use-make -usepretex $(TEX_DIR)/book.tex + @cp $(TEX_DIR)/hardcover-body.pdf $(PDF_DIR) + +# Build hardcover book (cover) +hardcover-cover: $(TEX_DIR)/full-cover.tex + @latexmk -pdfxe -cd -silent -jobname="hardcover-cover" -pretex="\def\hardcover{1}" -use-make -usepretex $(TEX_DIR)/full-cover.tex + @cp $(TEX_DIR)/hardcover-cover.pdf $(PDF_DIR) + +# Convert hardcover book (body) to CMYK +hardcover-body-cmyk: $(TEX_DIR)/hardcover-body.pdf + @gs -dSAFER -dNOPAUSE -dBATCH \ + -dAutoRotatePages=/None \ + -sColorConversionStrategy=CMYK -sProcessColorModel=DeviceCMYK \ + -sDEVICE=pdfwrite -sOutputFile=$(PDF_DIR)/hardcover-body-cmyk.pdf \ + $(TEX_DIR)/hardcover-body.pdf + +# Convert hardcover book (body) to CMYK +hardcover-cover-cmyk: $(TEX_DIR)/hardcover-cover.pdf + @gs -dSAFER -dNOPAUSE -dBATCH \ + -dAutoRotatePages=/None \ + -sColorConversionStrategy=CMYK -sProcessColorModel=DeviceCMYK \ + -sDEVICE=pdfwrite -sOutputFile=$(PDF_DIR)/hardcover-cover-cmyk.pdf \ + $(TEX_DIR)/hardcover-cover.pdf + +hardcover-count: $(TEX_DIR)/hardcover-body.pdf + @gs -o - -sDEVICE=inkcov $(TEX_DIR)/hardcover-body.pdf \ + | tail -n +6 \ + | sed '/^Page*/N;s/\n//' \ + | sed -E '/Page [0-9]+ 0.00000 0.00000 0.00000 / d' \ + | cut -f 2 -d ' ' \ + | xargs -I {} echo -n {},; echo + + + +$(TEX_DIR)/00-%.tex: $(RST_DIR)/00-%.rst + @echo "Building $@" + @./rst2latex.py --documentclass=book \ + --no-doc-title \ + --table-style=booktabs \ + --trim-footnote-reference-space \ + --use-latex-citations \ + --figure-citations \ + --reference-label=ref* \ + --strip-comments \ + --template=$(RST_DIR)/chapter.tex \ + $< > $@ + +$(TEX_DIR)/%.tex: $(RST_DIR)/%.rst $(RST_FILES) + @echo "Building $@" + @./rst2latex.py --documentclass=book \ + --use-part-section \ + --no-doc-title \ + --table-style=booktabs \ + --use-latex-citations \ + --figure-citations \ + --reference-label=ref* \ + --strip-comments \ + --template=$(RST_DIR)/chapter.tex \ + $< > $@ + +.PHONY: clean +clean: + @rm -f $(TEX_DIR)/*.aux + @rm -f $(TEX_DIR)/*.log + @rm -f $(TEX_DIR)/*.blg + @rm -f $(TEX_DIR)/*.fls + @rm -f $(TEX_DIR)/*.run.xml + @rm -f $(TEX_DIR)/*.bcf + @rm -f $(TEX_DIR)/*.xdv + @rm -f $(TEX_DIR)/*.toc + @rm -f $(TEX_DIR)/*.out + @rm -f $(TEX_DIR)/*.fdb_latexmk + @rm -f $(TEX_DIR)/book.pdf + @rm -f $(TEX_DIR)/hardcover-body.pdf + @rm -f $(TEX_DIR)/softcover-body.pdf + @rm -f $(TEX_DIR)/main.tex + @rm -f $(TEX_DIR)/00-preface.tex + @rm -f $(TEX_DIR)/00-introduction.tex + @rm -f $(TEX_DIR)/00-dedication.tex + @rm -f $(TEX_DIR)/00-acknowledgments.tex + @echo "Cleanup complete!" diff --git a/README.md b/README.md index a8090e4..4889da8 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,53 @@ - Scientific Python — Volume II -**Scientific Visualization – Python & Matplotlib**, Nicolas P. Rougier +## Scientific Visualization: Python + Matplotlib +**Nicolas P. Rougier, Bordeaux, November 2021.** -Front cover +Front cover -An open access book on scientific visualization using python and matplotlib to -be released during Spring 2021 (hopefully). Sources will be available in this -repository, the PDF book will be open-access and the printed book will cost 50$. +The Python scientific visualisation landscape is huge. It is composed of a myriad of tools, ranging from the most versatile and widely used down to the more specialised and confidential. Some of these tools are community based while others are developed by companies. Some are made specifically for the web, others are for the desktop only, some deal with 3D and large data, while others target flawless 2D rendering. In this landscape, Matplotlib has a very special place. It is a versatile and powerful library that allows you to design very high quality figures, suitable for scientific publishing. It also offers a simple and intuitive interface as well as an object oriented architecture that allows you to tweak anything within a figure. Finally, it can be used as a regular graphic library in order to design non‐scientific figures. This book is organized into four parts. The first part considers the fundamental principles of the Matplotlib library. This includes reviewing the different parts that constitute a figure, the different coordinate systems, the available scales and projections, and we’ll also introduce a few concepts related to typography and colors. The second part is dedicated to the actual design of a figure. After introducing some simple rules for generating better figures, we’ll then go on to explain the Matplotlib defaults and styling system before diving on into figure layout organization. We’ll then explore the different types of plot available and see how a figure can be ornamented with different elements. The third part is dedicated to more advanced concepts, namely 3D figures, optimization & animation. The fourth and final part is a collection of showcases. -If you want to support the book, you can: +### Read the book - * Star the project - * [Tip a few euros (10 €)](https://www.paypal.me/NicolasPRougier/10) - * [Access the private repository (25 €)](https://www.paypal.me/NicolasPRougier/25) during the writing - (and let me know about your github handle) - * [Sponsor me](https://github.com/sponsors/rougier) through GitHub sponsorship program - * **[Nominate me](https://stars.github.com/nominate/)** for the GitHub stars program (it's free) - -Note that in any case, the repository will be made public at the end of the writing and the PDF will be -available for free. +You can read the book **[PDF](https://hal.inria.fr/hal-03427242/document)** (95Mo) that is open access and hosted on +[HAL](https://hal.archives-ouvertes.fr/) which is a French open +archive for academics. +Sources for the book (including code examples) +are available at +[github.com/rougier/scientific-visualization-book](https://github.com/rougier/scientific-visualization-book). -In the meantime and if you're impatient, you can read: +### Buy the book -* [Python & OpenGL for Scientific Visualization](https://www.labri.fr/perso/nrougier/python-opengl/) -* [From Python to Numpy](https://www.labri.fr/perso/nrougier/from-python-to-numpy/) -* [100 Numpy exercices](https://github.com/rougier/numpy-100) -* [Matplotlib cheat sheet](https://github.com/rougier/matplotlib-cheatsheet) - - -### Latest news - - -You can read them on them [news issue](https://github.com/rougier/scientific-visualization-book/issues/1) and subscribe to this specific issue to get notified about new posts. - -

---- -
- -### Image of the week - -The images below come from the book and have been made using matplotlib, of course. - - **Fancy axes (locatable axes + Latex)** -![](images/fancy-axes.png) - - **Spiral Pi (text path)** -![](images/spiral-pi.png) - - **Dropshadowed contours (contourf + imshow)** -![](images/contour-dropshadow.png) - - **Domain coloring (imshow and contours)** -![](images/domain-coloring.png) - - **Waterfall 3d (poly collection)** -![](images/waterfall-3d.png) - - **Flower Polar (poly collection)** -![](images/flower-polar.png) - - **Textual contours (text path)** -![](images/typography-text-path.png) +If you want to buy the book, you can order a **printed edition** at +[amazon.com](https://www.amazon.com/dp/2957990105) for 49$. If you want to support or sponsor my +future work on Python (and +[Emacs](https://github.com/rougier/nano-emacs)), you can use +[paypal](https://www.paypal.com/paypalme/NicolasPRougier/10), +[github](https://github.com/sponsors/rougier) or +[liberapay](https://en.liberapay.com/rougier/). - **Typography (text, ticks)** -![](images/typography-matters.png) + + + + + + - **Complex axes layout (axes, text, imshow)** -![](images/poster-layout.png) - **Signal multisampling (plot, collection, imshow)** -![](images/multisample.png) +### See also - **Polygons & testing** -![](images/radial-maze.png) - - **Polygons & clipping** -![](images/polygon-clipping.png) - - **Post-processing** -![](images/metropolis.png) - - **3D projection** -![](images/projection-3d-gaussian.png) - - **Polar projection** -![](images/polar-projection.png) - - **Scales** (plot + scales) -![](images/scales.png) - - **Matplotlib map** (Contour + lines collection) -![](images/matplotlib-map.png) - - **Escher style** (Polygons) -![](images/escher.png) - - **Many plots** (plot & fill_between) -![](images/zorder-plots.png) - - **Oriented histogram** (axisartist toolkit) -![](images/histogram-pca.png) - - **Seasonal plot** (polar plot & patches) -![](images/text-polar.png) - - **Hatched bars** (bar) -![](images/hatched-bars.png) - - **Platonic solids** (PolyCollection) -![](images/platonic-solids.png) - - [**Calendar heatmap (github activity)**](https://github.com/rougier/calendar-heatmap) (Imshow, PolyCollection) -![](https://github.com/rougier/calendar-heatmap/raw/master/github-activity.png) - - [**Recursive Voronoi**](https://github.com/rougier/recursive-voronoi) (PolyCollection) -![](https://raw.githubusercontent.com/rougier/recursive-voronoi/master/recursive-voronoi.png) +* [Python & OpenGL for Scientific Visualization](https://www.labri.fr/perso/nrougier/python-opengl/) +* [From Python to Numpy](https://www.labri.fr/perso/nrougier/from-python-to-numpy/) (Scientific Python Volume I) +* [100 Numpy exercices](https://github.com/rougier/numpy-100) +* [Matplotlib cheat sheets](https://github.com/matplotlib/cheatsheets) - **Scatter 3D** (PolyCollection, scatter(s), Ellipses and custom 3D projection) -![](images/scatter-3d.png) - **Text shadow** (TextPath, PolyCollection and imshow) -![](images/text-shadow.png) +### Screenshots - **Boots** (scatter plot) -![](images/boots.png) + + + + + + + + diff --git a/code/.DS_Store b/code/.DS_Store new file mode 100644 index 0000000..e30addc Binary files /dev/null and b/code/.DS_Store differ diff --git a/code/anatomy/anatomy.py b/code/anatomy/anatomy.py new file mode 100644 index 0000000..2b9f8e8 --- /dev/null +++ b/code/anatomy/anatomy.py @@ -0,0 +1,161 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import AutoMinorLocator, MultipleLocator, FuncFormatter + +np.random.seed(123) + +X = np.linspace(0.5, 3.5, 100) +Y1 = 3 + np.cos(X) +Y2 = 1 + np.cos(1 + X / 0.75) / 2 +Y3 = np.random.uniform(Y1, Y2, len(X)) + +fig = plt.figure(figsize=(8, 8)) +ax = fig.add_subplot(1, 1, 1, aspect=1) + + +def minor_tick(x, pos): + if not x % 1.0: + return "" + return "%.2f" % x + + +ax.xaxis.set_major_locator(MultipleLocator(1.000)) +ax.xaxis.set_minor_locator(AutoMinorLocator(4)) +ax.yaxis.set_major_locator(MultipleLocator(1.000)) +ax.yaxis.set_minor_locator(AutoMinorLocator(4)) +ax.xaxis.set_minor_formatter(FuncFormatter(minor_tick)) + +ax.set_xlim(0, 4) +ax.set_ylim(0, 4) + +ax.tick_params(which="major", width=1.0) +ax.tick_params(which="major", length=10) +ax.tick_params(which="minor", width=1.0, labelsize=10) +ax.tick_params(which="minor", length=5, labelsize=10, labelcolor="0.25") + +ax.grid(linestyle="--", linewidth=0.5, color=".25", zorder=-10) + +ax.plot(X, Y1, c=(0.25, 0.25, 1.00), lw=2, label="Blue signal", zorder=10) +ax.plot(X, Y2, c=(1.00, 0.25, 0.25), lw=2, label="Red signal") +ax.plot(X, Y3, linewidth=0, marker="o", markerfacecolor="w", markeredgecolor="k") + +ax.set_title("Anatomy of a figure", fontsize=20, verticalalignment="bottom") +ax.set_xlabel("X axis label") +ax.set_ylabel("Y axis label") + +ax.legend() + + +def circle(x, y, radius=0.15): + from matplotlib.patches import Circle + from matplotlib.patheffects import withStroke + + circle = Circle( + (x, y), + radius, + clip_on=False, + zorder=10, + linewidth=1, + edgecolor="black", + facecolor=(0, 0, 0, 0.0125), + path_effects=[withStroke(linewidth=5, foreground="w")], + ) + ax.add_artist(circle) + + +def text(x, y, text): + ax.text( + x, + y, + text, + backgroundcolor="white", + # fontname="Yanone Kaffeesatz", fontsize="large", + ha="center", + va="top", + weight="regular", + color="#000099", + ) + + +# Minor tick +circle(0.50, -0.10) +text(0.50, -0.32, "Minor tick label") + +# Major tick +circle(-0.03, 4.00) +text(0.03, 3.80, "Major tick") + +# Minor tick +circle(0.00, 3.50) +text(0.00, 3.30, "Minor tick") + +# Major tick label +circle(-0.15, 3.00) +text(-0.15, 2.80, "Major tick label") + +# X Label +circle(1.80, -0.27) +text(1.80, -0.45, "X axis label") + +# Y Label +circle(-0.27, 1.80) +text(-0.27, 1.6, "Y axis label") + +# Title +circle(1.60, 4.13) +text(1.60, 3.93, "Title") + +# Blue plot +circle(1.75, 2.80) +text(1.75, 2.60, "Line\n(line plot)") + +# Red plot +circle(1.20, 0.60) +text(1.20, 0.40, "Line\n(line plot)") + +# Scatter plot +circle(3.20, 1.75) +text(3.20, 1.55, "Markers\n(scatter plot)") + +# Grid +circle(3.00, 3.00) +text(3.00, 2.80, "Grid") + +# Legend +circle(3.70, 3.80) +text(3.70, 3.60, "Legend") + +# Axes +circle(0.5, 0.5) +text(0.5, 0.3, "Axes") + +# Figure +circle(-0.3, 0.65) +text(-0.3, 0.45, "Figure") + +color = "#000099" +ax.annotate( + "Spines", + xy=(4.0, 0.35), + xytext=(3.3, 0.5), + color=color, + weight="regular", # fontsize="large", fontname="Yanone Kaffeesatz", + arrowprops=dict(arrowstyle="->", connectionstyle="arc3", color=color), +) + +ax.annotate( + "", + xy=(3.15, 0.0), + xytext=(3.45, 0.45), + color=color, + weight="regular", # fontsize="large", fontname="Yanone Kaffeesatz", + arrowprops=dict(arrowstyle="->", connectionstyle="arc3", color=color), +) + +plt.savefig("../../figures/anatomy/anatomy.pdf") +plt.show() diff --git a/code/anatomy/bold-ticklabel.py b/code/anatomy/bold-ticklabel.py new file mode 100644 index 0000000..09917da --- /dev/null +++ b/code/anatomy/bold-ticklabel.py @@ -0,0 +1,15 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(5, 2)) +for label in ax.get_xaxis().get_ticklabels(): + label.set_fontweight("bold") + +plt.tight_layout() +plt.savefig("../../figures/anatomy/bold-ticklabel.pdf") +plt.show() diff --git a/code/anatomy/figure-dpi.py b/code/anatomy/figure-dpi.py new file mode 100644 index 0000000..8c9760a --- /dev/null +++ b/code/anatomy/figure-dpi.py @@ -0,0 +1,33 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import matplotlib.pyplot as plt + + +def figure(dpi): + fig = plt.figure(figsize=(4.25, 0.2)) + ax = plt.subplot(1, 1, 1, frameon=False) + plt.xticks([]), plt.yticks([]) + text = "A text rendered at 10pt size using {0} dpi".format(dpi) + ax.text( + 0.5, + 0.5, + text, + fontname="Source Serif Pro", + ha="center", + va="center", + fontsize=10, + fontweight="light", + ) + plt.savefig("../../figures/anatomy/figure-dpi-{0:03d}.png".format(dpi), dpi=dpi) + + +figure(50) +figure(100) +figure(300) +figure(600) + +# Using ImageMagick +# convert -resize 2550x -append figure-dpi-*.png figure-dpi.png diff --git a/code/anatomy/inch-cm.py b/code/anatomy/inch-cm.py new file mode 100644 index 0000000..b12fcaa --- /dev/null +++ b/code/anatomy/inch-cm.py @@ -0,0 +1,70 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +# The goal is to display a metric axis whose physical size (once printed) is +# correct. The figure will be printed on A5 papersize (210x148mm) with 20mm +# margin on each side +# +# Figure width (mm) is thus 148 - 2x 20 = 108mm = 10.8cm +# Figure width (inch) (10.8/2.54) ~ 4.25 inches +# +# However, we need to have margins in our figure (or tick labels will be cut) +# so we'll have 0.25 inches margin on each side of the axis such that the axis +# will be 4 inches. +# ---------------------------------------------------------------------------- +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.ticker import MultipleLocator + +mpl.rcParams["font.serif"] = "Source Serif Pro" +mpl.rcParams["font.size"] = 10 +mpl.rcParams["font.weight"] = 400 +mpl.rcParams["font.family"] = "serif" +mpl.rcParams["axes.labelweight"] = 400 + +inch = 2.54 +fig_width = 10.8 / inch # ~4.252 inches +fig_height = 1.25 # 1.250 inches +margin = 0.125 # 0.125 inches + +fig = plt.figure(figsize=(fig_width, fig_height)) +plt.subplots_adjust( + left=0.5 * margin / fig_width, + right=1 - 0.5 * margin / fig_width, + bottom=0.5 * margin / fig_height, + top=1 - 0.5 * margin / fig_height, +) + +ax1 = plt.subplot(1, 1, 1) +xmin, xmax = 0, 4 +ymin, ymax = 0, 1 + +# Inches graduation +ax1 = plt.subplot(1, 1, 1, yticks=[]) +ax1.spines["right"].set_visible(False) +ax1.spines["left"].set_visible(False) +ax1.spines["top"].set_visible(False) +ax1.set_xlim(xmin, xmax) +ax1.set_ylim(ymin, ymax) +ax1.spines["bottom"].set_position(("axes", 0.45)) +ax1.xaxis.set_major_locator(MultipleLocator(1.00)) +ax1.xaxis.set_minor_locator(MultipleLocator(0.25)) +ax1.tick_params(axis="both", which="major") +ax1.set_xlabel("inch") + +# Centimeter graduation +ax2 = ax1.twiny() +ax2.spines["right"].set_visible(False) +ax2.spines["left"].set_visible(False) +ax2.spines["bottom"].set_visible(False) +ax2.set_xlim(xmin * inch, xmax * inch) +ax2.spines["top"].set_position(("axes", 0.55)) +ax2.xaxis.set_major_locator(MultipleLocator(1.00)) +ax2.xaxis.set_minor_locator(MultipleLocator(0.10)) +ax2.tick_params(axis="both", which="major", labelsize=10) +ax2.set_xlabel("cm") + +plt.savefig("../../figures/anatomy/inch-cm.pdf", dpi=600) +plt.show() diff --git a/code/anatomy/pixel-font.py b/code/anatomy/pixel-font.py new file mode 100644 index 0000000..04893c6 --- /dev/null +++ b/code/anatomy/pixel-font.py @@ -0,0 +1,131 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +# Translated from +# http://www.piclist.com/tecHREF/datafile/charset/extractor/charset_extractor.htm +font = np.array( + [ + (0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0x10, 0xE3, 0x84, 0x10, 0x01, 0x00), + (0x6D, 0xB4, 0x80, 0x00, 0x00, 0x00), + (0x00, 0xA7, 0xCA, 0x29, 0xF2, 0x80), + (0x20, 0xE4, 0x0C, 0x09, 0xC1, 0x00), + (0x65, 0x90, 0x84, 0x21, 0x34, 0xC0), + (0x21, 0x45, 0x08, 0x55, 0x23, 0x40), + (0x30, 0xC2, 0x00, 0x00, 0x00, 0x00), + (0x10, 0x82, 0x08, 0x20, 0x81, 0x00), + (0x20, 0x41, 0x04, 0x10, 0x42, 0x00), + (0x00, 0xA3, 0x9F, 0x38, 0xA0, 0x00), + (0x00, 0x41, 0x1F, 0x10, 0x40, 0x00), + (0x00, 0x00, 0x00, 0x00, 0xC3, 0x08), + (0x00, 0x00, 0x1F, 0x00, 0x00, 0x00), + (0x00, 0x00, 0x00, 0x00, 0xC3, 0x00), + (0x00, 0x10, 0x84, 0x21, 0x00, 0x00), + (0x39, 0x14, 0xD5, 0x65, 0x13, 0x80), + (0x10, 0xC1, 0x04, 0x10, 0x43, 0x80), + (0x39, 0x10, 0x46, 0x21, 0x07, 0xC0), + (0x39, 0x10, 0x4E, 0x05, 0x13, 0x80), + (0x08, 0x62, 0x92, 0x7C, 0x20, 0x80), + (0x7D, 0x04, 0x1E, 0x05, 0x13, 0x80), + (0x18, 0x84, 0x1E, 0x45, 0x13, 0x80), + (0x7C, 0x10, 0x84, 0x20, 0x82, 0x00), + (0x39, 0x14, 0x4E, 0x45, 0x13, 0x80), + (0x39, 0x14, 0x4F, 0x04, 0x23, 0x00), + (0x00, 0x03, 0x0C, 0x00, 0xC3, 0x00), + (0x00, 0x03, 0x0C, 0x00, 0xC3, 0x08), + (0x08, 0x42, 0x10, 0x20, 0x40, 0x80), + (0x00, 0x07, 0xC0, 0x01, 0xF0, 0x00), + (0x20, 0x40, 0x81, 0x08, 0x42, 0x00), + (0x39, 0x10, 0x46, 0x10, 0x01, 0x00), + (0x39, 0x15, 0xD5, 0x5D, 0x03, 0x80), + (0x39, 0x14, 0x51, 0x7D, 0x14, 0x40), + (0x79, 0x14, 0x5E, 0x45, 0x17, 0x80), + (0x39, 0x14, 0x10, 0x41, 0x13, 0x80), + (0x79, 0x14, 0x51, 0x45, 0x17, 0x80), + (0x7D, 0x04, 0x1E, 0x41, 0x07, 0xC0), + (0x7D, 0x04, 0x1E, 0x41, 0x04, 0x00), + (0x39, 0x14, 0x17, 0x45, 0x13, 0xC0), + (0x45, 0x14, 0x5F, 0x45, 0x14, 0x40), + (0x38, 0x41, 0x04, 0x10, 0x43, 0x80), + (0x04, 0x10, 0x41, 0x45, 0x13, 0x80), + (0x45, 0x25, 0x18, 0x51, 0x24, 0x40), + (0x41, 0x04, 0x10, 0x41, 0x07, 0xC0), + (0x45, 0xB5, 0x51, 0x45, 0x14, 0x40), + (0x45, 0x95, 0x53, 0x45, 0x14, 0x40), + (0x39, 0x14, 0x51, 0x45, 0x13, 0x80), + (0x79, 0x14, 0x5E, 0x41, 0x04, 0x00), + (0x39, 0x14, 0x51, 0x55, 0x23, 0x40), + (0x79, 0x14, 0x5E, 0x49, 0x14, 0x40), + (0x39, 0x14, 0x0E, 0x05, 0x13, 0x80), + (0x7C, 0x41, 0x04, 0x10, 0x41, 0x00), + (0x45, 0x14, 0x51, 0x45, 0x13, 0x80), + (0x45, 0x14, 0x51, 0x44, 0xA1, 0x00), + (0x45, 0x15, 0x55, 0x55, 0x52, 0x80), + (0x45, 0x12, 0x84, 0x29, 0x14, 0x40), + (0x45, 0x14, 0x4A, 0x10, 0x41, 0x00), + (0x78, 0x21, 0x08, 0x41, 0x07, 0x80), + (0x38, 0x82, 0x08, 0x20, 0x83, 0x80), + (0x01, 0x02, 0x04, 0x08, 0x10, 0x00), + (0x38, 0x20, 0x82, 0x08, 0x23, 0x80), + (0x10, 0xA4, 0x40, 0x00, 0x00, 0x00), + (0x00, 0x00, 0x00, 0x00, 0x00, 0x3F), + (0x30, 0xC1, 0x00, 0x00, 0x00, 0x00), + (0x00, 0x03, 0x81, 0x3D, 0x13, 0xC0), + (0x41, 0x07, 0x91, 0x45, 0x17, 0x80), + (0x00, 0x03, 0x91, 0x41, 0x13, 0x80), + (0x04, 0x13, 0xD1, 0x45, 0x13, 0xC0), + (0x00, 0x03, 0x91, 0x79, 0x03, 0x80), + (0x18, 0x82, 0x1E, 0x20, 0x82, 0x00), + (0x00, 0x03, 0xD1, 0x44, 0xF0, 0x4E), + (0x41, 0x07, 0x12, 0x49, 0x24, 0x80), + (0x10, 0x01, 0x04, 0x10, 0x41, 0x80), + (0x08, 0x01, 0x82, 0x08, 0x24, 0x8C), + (0x41, 0x04, 0x94, 0x61, 0x44, 0x80), + (0x10, 0x41, 0x04, 0x10, 0x41, 0x80), + (0x00, 0x06, 0x95, 0x55, 0x14, 0x40), + (0x00, 0x07, 0x12, 0x49, 0x24, 0x80), + (0x00, 0x03, 0x91, 0x45, 0x13, 0x80), + (0x00, 0x07, 0x91, 0x45, 0x17, 0x90), + (0x00, 0x03, 0xD1, 0x45, 0x13, 0xC1), + (0x00, 0x05, 0x89, 0x20, 0x87, 0x00), + (0x00, 0x03, 0x90, 0x38, 0x13, 0x80), + (0x00, 0x87, 0x88, 0x20, 0xA1, 0x00), + (0x00, 0x04, 0x92, 0x49, 0x62, 0x80), + (0x00, 0x04, 0x51, 0x44, 0xA1, 0x00), + (0x00, 0x04, 0x51, 0x55, 0xF2, 0x80), + (0x00, 0x04, 0x92, 0x31, 0x24, 0x80), + (0x00, 0x04, 0x92, 0x48, 0xE1, 0x18), + (0x00, 0x07, 0x82, 0x31, 0x07, 0x80), + (0x18, 0x82, 0x18, 0x20, 0x81, 0x80), + (0x10, 0x41, 0x00, 0x10, 0x41, 0x00), + (0x30, 0x20, 0x83, 0x08, 0x23, 0x00), + (0x29, 0x40, 0x00, 0x00, 0x00, 0x00), + (0x10, 0xE6, 0xD1, 0x45, 0xF0, 0x00), + ], + dtype=np.uint8, +) + +text = "The quick brown fox jumps over the lazy dog!" +Z = np.zeros((8, len(text), 6, 4)) +for i in range(len(text)): + Z[:, i, :, -1] = np.unpackbits(font[ord(text[i]) - ord(" ")]).reshape(8, 6) +Z = Z.reshape(8, len(text) * 6, 4) + +height, width, _ = Z.shape +dpi = 100 +fig_width = width / dpi +fig_height = height / dpi + +fig = plt.figure(figsize=(fig_width, fig_height), dpi=dpi) +ax = fig.add_axes([0, 0, 1, 1], frameon=False, xticks=[], yticks=[]) +ax.imshow(Z, interpolation="nearest") + +plt.savefig( + "../../figures/anatomy/pixel-size.png", dpi=4 * 1200, transparent=True +) # dpi/10) +# plt.show() diff --git a/code/anatomy/raster-vector.py b/code/anatomy/raster-vector.py new file mode 100644 index 0000000..9397588 --- /dev/null +++ b/code/anatomy/raster-vector.py @@ -0,0 +1,101 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.patches import Rectangle + + +def pixelated_text(dpi=100): + fig = Figure(figsize=(1, 1), dpi=dpi) + canvas, ax = FigureCanvasAgg(fig), fig.gca() + ax.text(0.5, 0.5, "a", fontsize=75, ha="center", va="center") + ax.axis("off") + canvas.draw() + image = np.frombuffer(canvas.tostring_argb(), dtype="uint8") + image = image.reshape(dpi, dpi, 4) + image = np.roll(image, 3, axis=2) + return image + + +def square(position, size, edgecolor, facecolor, zorder): + rect = Rectangle( + position, + size, + size, + transform=ax.transAxes, + clip_on=False, + zorder=zorder, + linewidth=0.5, + edgecolor=edgecolor, + facecolor=facecolor, + ) + ax.add_artist(rect) + + +image = pixelated_text(75) +fig = plt.figure(figsize=(4.25, 2), dpi=100) + +# Left (raster) +ax = plt.subplot( + 1, 2, 1, frameon=False, aspect=1, xticks=[], yticks=[], xlim=[0, 1], ylim=[0, 1] +) + +ax.imshow(image, extent=[0.1, 1.0, 0.1, 1.0], zorder=10, interpolation="nearest") +square((0.1, 0.1), 0.9, "black", "None", 20) + +square((0.0, 0.0), 0.2, "black", "white", 20) +ax.imshow(image, extent=[0.0, 0.2, 0.0, 0.2], zorder=30, interpolation="nearest") +square((0.0, 0.0), 0.2, "black", "None", 40) + +ax.text(0.55, 1.025, "Raster rendering", fontsize="small", ha="center", va="bottom") +ax.text( + 0.6, 0.1 - 0.025, ".PNG / .JPG / .TIFF", fontsize="x-small", ha="center", va="top" +) + +# Right (vector) +ax = plt.subplot( + 1, 2, 2, frameon=False, aspect=1, xticks=[], yticks=[], xlim=[0, 1], ylim=[0, 1] +) +ax.text(0.55, 0.55, "a", fontsize=100, ha="center", va="center", color="#000099") +square((0.1, 0.1), 0.9, "#000099", "None", 20) +square((0.0, 0.0), 0.2, "#000099", "white", 20) +ax.text( + 0.1, + 0.1, + "a", + fontsize=22, + ha="center", + va="center", + clip_on=False, + zorder=30, + color="#000099", +) +square((0.0, 0.0), 0.2, "#000099", "None", 40) + +ax.text( + 0.55, + 1.025, + "Vector rendering", + fontsize="small", + ha="center", + va="bottom", + color="#000099", +) +ax.text( + 0.6, + 0.1 - 0.025, + ".PDF / .SVG / .PS", + fontsize="x-small", + ha="center", + va="top", + color="#000099", +) + + +plt.savefig("../../figures/anatomy/raster-vector.pdf", dpi=600) +plt.show() diff --git a/code/anatomy/ruler.py b/code/anatomy/ruler.py new file mode 100644 index 0000000..e6d3b71 --- /dev/null +++ b/code/anatomy/ruler.py @@ -0,0 +1,87 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + + +class Ruler: + """ Ruler add a whole figure axis whose ticks indicate figure + dimensions and adapt itself to figure resize event. + """ + + def __init__(self, fig=None): + self.fig = fig or plt.gcf() + self.ax = None + self.show() + + def show(self): + + if self.ax is None: + ax = fig.add_axes([0, 0, 1, 1], zorder=-10, facecolor="None") + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + + ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.1)) + ax.tick_params( + axis="x", which="both", labelsize="x-small", direction="in", pad=-15 + ) + ax.xaxis.tick_top() + + ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.1)) + ax.yaxis.tick_left() + ax.tick_params( + axis="y", which="both", labelsize="x-small", direction="in", pad=-8 + ) + ax.yaxis.tick_left() + for label in ax.yaxis.get_ticklabels(): + label.set_horizontalalignment("left") + + self.text = ax.text( + 0.5, 0.4, "cm", ha="center", va="center", size="x-small" + ) + ax.grid(linestyle="--", linewidth=0.5) + + self.ax = ax + + self.update() + plt.connect("resize_event", self.update) + + def update(self, *args): + + inch = 2.54 + width_cm = self.fig.get_figwidth() * inch + height_cm = self.fig.get_figheight() * inch + + n = int(width_cm) + 1 + self.ax.set_xlim(0, width_cm) + self.ax.set_xticks(np.arange(n)) + self.ax.set_xticklabels([""] + ["%d" % x for x in np.arange(1, n)]) + + markersize = self.ax.xaxis.get_ticklines(True)[0].get_markersize() + for line in self.ax.xaxis.get_ticklines(True)[2::9]: + line.set_markersize(1.5 * markersize) + + n = int(height_cm) + 1 + self.ax.set_ylim(height_cm, 0) + self.ax.set_yticks(np.arange(n)) + self.ax.set_yticklabels([""] + ["%d" % y for y in np.arange(1, n)]) + + markersize = self.ax.yaxis.get_ticklines(True)[0].get_markersize() + for line in self.ax.yaxis.get_ticklines(True)[1::9]: + line.set_markersize(1.5 * markersize) + + +# width = page width - left margin - right margin +width = (14.8 - 1.5 - 2.0) / 2.54 +height = width / 2 + +fig = plt.figure(figsize=(width, height), dpi=100) +ax = plt.subplot() +ruler = Ruler() + +plt.savefig("../../figures/anatomy/ruler.pdf") +plt.show() diff --git a/code/anatomy/zorder-plots.py b/code/anatomy/zorder-plots.py new file mode 100644 index 0000000..1af280e --- /dev/null +++ b/code/anatomy/zorder-plots.py @@ -0,0 +1,87 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +# Some nice but random curves +def curve(): + n = np.random.randint(1, 5) + centers = np.random.normal(0.0, 1.0, n) + widths = np.random.uniform(5.0, 50.0, n) + widths = 10 * widths / widths.sum() + scales = np.random.uniform(0.1, 1.0, n) + scales /= scales.sum() + X = np.zeros(500) + x = np.linspace(-3, 3, len(X)) + for center, width, scale in zip(centers, widths, scales): + X = X + scale * np.exp(-(x - center) * (x - center) * width) + return X + + +np.random.seed(123) +cmap = mpl.cm.get_cmap("Spectral") +fig = plt.figure(figsize=(8, 8)) + + +ax = None +for n in range(3): + ax = plt.subplot(1, 3, n + 1, frameon=False, sharex=ax) + for i in range(50): + Y = curve() + X = np.linspace(-3, 3, len(Y)) + ax.plot(X, 3 * Y + i, color="k", linewidth=0.75, zorder=100 - i) + color = cmap(i / 50) + ax.fill_between(X, 3 * Y + i, i, color=color, zorder=100 - i) + + # Some random text on the right of the curve + v = np.random.uniform(0, 1) + if v < 0.4: + text = "*" + if v < 0.05: + text = "***" + elif v < 0.2: + text = "**" + ax.text( + 3.0, + i, + text, + ha="right", + va="baseline", + size=8, + transform=ax.transData, + zorder=300, + ) + + ax.yaxis.set_tick_params(tick1On=False) + ax.set_xlim(-3, 3) + ax.set_ylim(-1, 53) + ax.axvline(0.0, ls="--", lw=0.75, color="black", zorder=250) + ax.text( + 0.0, + 1.0, + "Value %d" % (n + 1), + ha="left", + va="top", + weight="bold", + transform=ax.transAxes, + ) + + if n == 0: + ax.yaxis.set_tick_params(labelleft=True) + ax.set_yticks(np.arange(50)) + ax.set_yticklabels(["Serie %d" % i for i in range(1, 51)]) + for tick in ax.yaxis.get_major_ticks(): + tick.label.set_fontsize(6) + tick.label.set_verticalalignment("bottom") + else: + ax.yaxis.set_tick_params(labelleft=False) + + +plt.tight_layout() +plt.savefig("../../figures/anatomy/zorder-plots.png", dpi=600) +plt.savefig("../../figures/anatomy/zorder-plots.pdf") +plt.show() diff --git a/code/anatomy/zorder.py b/code/anatomy/zorder.py new file mode 100644 index 0000000..2cf98cf --- /dev/null +++ b/code/anatomy/zorder.py @@ -0,0 +1,64 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.text import TextPath +from matplotlib.transforms import Affine2D +import mpl_toolkits.mplot3d.art3d as art3d +from matplotlib.patches import Rectangle, PathPatch + + +def text3d(ax, xyz, s, zdir="z", size=None, angle=0, **kwargs): + x, y, z = xyz + if zdir == "y": + x, y, z = x, z, y + elif zdir == "x": + x, y, z = y, z, x + else: + x, y, z = x, y, z + text_path = TextPath((0, 0), s, size=size) + trans = Affine2D().rotate(angle).translate(x, y) + p = PathPatch(trans.transform_path(text_path), **kwargs) + ax.add_patch(p) + art3d.pathpatch_2d_to_3d(p, z=z, zdir=zdir) + + +fig = plt.figure(figsize=(6, 8)) +ax = fig.add_subplot(111, projection="3d", xticks=[], yticks=[], zticks=[]) +ax.set_axis_off() +ax.set_xlim(0, 10), ax.set_ylim(0, 10), ax.set_zlim(0, 10) +for i, text in enumerate( + [ + "Figure (background)", + "Axes (spines, ticks & labels)", + "Patches (zorder=1)", + "Lines (zorder=2)", + "Text (zorder=3)", + "Inset axes & legend (zorder=5)", + ] +): + + p = Rectangle((0, 0), 10, 10, edgecolor="None", facecolor="white", alpha=0.5) + ax.add_patch(p) + art3d.pathpatch_2d_to_3d(p, z=i, zdir="z") + + p = Rectangle((0, 0), 10, 10, edgecolor="black", facecolor="None") + ax.add_patch(p) + art3d.pathpatch_2d_to_3d(p, z=i, zdir="z") + + text3d( + ax, + (-0.25, 0.25, i), + text, + zdir="z", + size=0.5, + angle=np.pi / 2, + ec="none", + fc="k", + ) + +plt.savefig("../../figures/anatomy/zorder.pdf") +plt.show() diff --git a/code/animation/__pycache__/fluid.cpython-38.pyc b/code/animation/__pycache__/fluid.cpython-38.pyc new file mode 100644 index 0000000..8fdc894 Binary files /dev/null and b/code/animation/__pycache__/fluid.cpython-38.pyc differ diff --git a/code/animation/earthquakes.py b/code/animation/earthquakes.py new file mode 100644 index 0000000..b7d95fa --- /dev/null +++ b/code/animation/earthquakes.py @@ -0,0 +1,82 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import urllib +import numpy as np +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import matplotlib.animation as animation + + +def rain_update(frame): + global E, R, scatter + + current = frame % len(E) + i = frame % len(R) + + R["color"][:, 3] = np.maximum(0, R["color"][:, 3] - 1 / len(R)) + R["size"] += R["growth"] + + i = frame % len(R) + R["position"][i] = E["position"][current] + R["size"][i] = 5 + R["growth"][i] = 0.1 * np.exp(E["magnitude"][current]) + R["color"][i, 3] = 1 + + scatter.set_edgecolors(R["color"]) + scatter.set_sizes(R["size"].ravel()) + scatter.set_offsets(R["position"]) + + if frame == 50: + plt.savefig("../../figures/chapter-13/earthquakes-frame-50.pdf") + + return (scatter,) + + +# -> http://earthquake.usgs.gov/earthquakes/feed/v1.0/csv.php +feed = "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/" + +# Magnitude > 4.5 +url = urllib.request.urlopen(feed + "4.5_month.csv") + +# Reading and storage of data +data = url.read().split(b"\n")[+1:-1] +E = np.zeros(len(data), dtype=[("position", float, (2,)), ("magnitude", float, (1,))]) + +for i in range(len(data)): + row = data[i].split(b",") + E["position"][i] = float(row[2]), float(row[1]) + E["magnitude"][i] = float(row[4]) + +fig = plt.figure(figsize=(10, 5), dpi=75) +ax = plt.axes(projection=ccrs.PlateCarree()) +ax.coastlines() +scatter = ax.scatter( + [], + [], + s=[], + transform=ccrs.PlateCarree(), + linewidth=0.5, + edgecolors=[], + facecolors="None", +) + +n = 50 +R = np.zeros( + n, + dtype=[ + ("position", float, (2,)), + ("size", float, (1,)), + ("growth", float, (1,)), + ("color", float, (4,)), + ], +) +R["position"] = np.random.uniform(0, 1, (n, 2)) +R["size"] = np.linspace(0, 1, n).reshape(n, 1) +R["color"][:, 3] = np.linspace(0, 1, n) + +animation = animation.FuncAnimation(fig, rain_update, interval=10, frames=200) +plt.tight_layout() +plt.show() diff --git a/code/animation/fluid-animation.py b/code/animation/fluid-animation.py new file mode 100644 index 0000000..9a6cf3a --- /dev/null +++ b/code/animation/fluid-animation.py @@ -0,0 +1,63 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +from fluid import Fluid, inflow +from scipy.special import erf +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +shape = 256, 256 +duration = 500 +fluid = Fluid(shape, "dye") +inflows = [inflow(fluid, x) for x in np.linspace(-np.pi, np.pi, 8, endpoint=False)] + +# Animation setup +fig = plt.figure(figsize=(5, 5), dpi=100) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) +ax.set_xlim(0, 1) +ax.set_xticks([]) +ax.set_ylim(0, 1) +ax.set_yticks([]) +im = ax.imshow( + np.zeros(shape), + extent=[0, 1, 0, 1], + vmin=0, + vmax=1, + origin="lower", + interpolation="bicubic", + cmap=plt.cm.RdYlBu, +) + +# Animation scenario +scenario = [] +for i in range(8): + scenario.extend([[i]] * 20) +scenario.extend([[0, 2, 4, 6]] * 30) +scenario.extend([[1, 3, 5, 7]] * 30) + +# Animation update +def update(frame): + for i in scenario[frame % len(scenario)]: + inflow_velocity, inflow_dye = inflows[i] + fluid.velocity += inflow_velocity + fluid.dye += inflow_dye + divergence, curl, pressure = fluid.step() + Z = curl + Z = (erf(Z * 2) + 1) / 4 + + im.set_data(Z) + im.set_clim(vmin=Z.min(), vmax=Z.max()) + + text.set_text("Frame %d" % frame) + if frame in [30, 60, 90, 120, 150, 180, 210, 240]: + plt.savefig("../../figures/animation/fluid-animation-%03d.png" % frame, dpi=300) + + return im, text + + +text = ax.text(0.01, 0.99, "Test", ha="left", va="top", transform=ax.transAxes) +anim = animation.FuncAnimation(fig, update, interval=10, frames=duration) +plt.show() diff --git a/code/animation/fluid.py b/code/animation/fluid.py new file mode 100644 index 0000000..df22cf2 --- /dev/null +++ b/code/animation/fluid.py @@ -0,0 +1,114 @@ +# Code by Gregory Johnson at https://github.com/GregTJ/stable-fluids +# This is free and unencumbered software released into the public domain. + +import numpy as np +import scipy.sparse as sp +from math import factorial +from itertools import cycle +from functools import reduce +from scipy.sparse.linalg import factorized +from scipy.ndimage import map_coordinates, spline_filter + + +def difference(derivative, accuracy=1): + # Central differences implemented based on the article here: + # http://web.media.mit.edu/~crtaylor/calculator.html + derivative += 1 + radius = accuracy + derivative // 2 - 1 + points = range(-radius, radius + 1) + coefficients = np.linalg.inv(np.vander(points)) + return coefficients[-derivative] * factorial(derivative - 1), points + + +def operator(shape, *differences): + # Credit to Philip Zucker for figuring out + # that kronsum's argument order is reversed. + # Without that bit of wisdom I'd have lost it. + differences = zip(shape, cycle(differences)) + factors = (sp.diags(*diff, shape=(dim,) * 2) for dim, diff in differences) + return reduce(lambda a, f: sp.kronsum(f, a, format="csc"), factors) + + +class Fluid: + def __init__(self, shape, *quantities, pressure_order=1, advect_order=3): + self.shape = shape + self.dimensions = len(shape) + + # Prototyping is simplified by dynamically + # creating advected quantities as needed. + self.quantities = quantities + for q in quantities: + setattr(self, q, np.zeros(shape)) + + self.indices = np.indices(shape) + self.velocity = np.zeros((self.dimensions, *shape)) + + laplacian = operator(shape, difference(2, pressure_order)) + self.pressure_solver = factorized(laplacian) + + self.advect_order = advect_order + + def step(self): + # Advection is computed backwards in time as described in Stable Fluids. + advection_map = self.indices - self.velocity + + # SciPy's spline filter introduces checkerboard divergence. + # A linear blend of the filtered and unfiltered fields based + # on some value epsilon eliminates this error. + def advect(field, filter_epsilon=10e-2, mode="constant"): + filtered = spline_filter(field, order=self.advect_order, mode=mode) + field = filtered * (1 - filter_epsilon) + field * filter_epsilon + return map_coordinates( + field, + advection_map, + prefilter=False, + order=self.advect_order, + mode=mode, + ) + + # Apply advection to each axis of the + # velocity field and each user-defined quantity. + for d in range(self.dimensions): + self.velocity[d] = advect(self.velocity[d]) + + for q in self.quantities: + setattr(self, q, advect(getattr(self, q))) + + # Compute the jacobian at each point in the + # velocity field to extract curl and divergence. + jacobian_shape = (self.dimensions,) * 2 + partials = tuple(np.gradient(d) for d in self.velocity) + jacobian = np.stack(partials).reshape(*jacobian_shape, *self.shape) + + divergence = jacobian.trace() + + # If this curl calculation is extended to 3D, the y-axis value must be negated. + # This corresponds to the coefficients of the levi-civita symbol in that dimension. + # Higher dimensions do not have a vector -> scalar, or vector -> vector, + # correspondence between velocity and curl due to differing isomorphisms + # between exterior powers in dimensions != 2 or 3 respectively. + curl_mask = np.triu(np.ones(jacobian_shape, dtype=bool), k=1) + curl = (jacobian[curl_mask] - jacobian[curl_mask.T]).squeeze() + + # Apply the pressure correction to the fluid's velocity field. + pressure = self.pressure_solver(divergence.flatten()).reshape(self.shape) + self.velocity -= np.gradient(pressure) + return divergence, curl, pressure + + +def inflow(fluid, angle=0, padding=25, radius=7, velocity=1.5): + """ Source defnition """ + + center = np.floor_divide(fluid.shape, 2) + points = np.array([angle]) + points = tuple(np.array((np.cos(p), np.sin(p))) for p in points) + normals = tuple(-p for p in points) + r = np.min(center) - padding + points = tuple(r * p + center for p in points) + inflow_velocity = np.zeros_like(fluid.velocity) + inflow_dye = np.zeros(fluid.shape) + for p, n in zip(points, normals): + mask = np.linalg.norm(fluid.indices - p[:, None, None], axis=0) <= radius + inflow_velocity[:, mask] += n[:, None] * velocity + inflow_dye[mask] = 1 + return inflow_velocity, inflow_dye diff --git a/code/animation/imgcat.py b/code/animation/imgcat.py new file mode 100644 index 0000000..adcd376 --- /dev/null +++ b/code/animation/imgcat.py @@ -0,0 +1,24 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib + +# Needs OSX and iterm2 +matplotlib.use("module://imgcat") +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(8, 4), frameon=False) + +ax = plt.subplot(2, 1, 1) +X = np.linspace(0, 4 * 2 * np.pi, 500) +(line,) = ax.plot(X, np.cos(X)) + +ax = plt.subplot(2, 1, 2) +X = np.linspace(0, 4 * 2 * np.pi, 500) +(line,) = ax.plot(X, np.sin(X)) + +plt.tight_layout() +plt.show() diff --git a/code/animation/less-is-more.py b/code/animation/less-is-more.py new file mode 100644 index 0000000..91eb884 --- /dev/null +++ b/code/animation/less-is-more.py @@ -0,0 +1,370 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Usage: +# $ for i in `seq 1 35`; do echo $i; python less-is-more.py $i; done +# $ cp frame-35.png frame-36.png +# $ cp frame-35.png frame-37.png +# $ cp frame-35.png frame-38.png +# $ cp frame-35.png frame-39.png +# $ cp frame-35.png frame-40.png +# $ convert -delay 100 *.png less-is-more.gif + +import re +import sys +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.image as mpimg +from matplotlib.artist import Artist + + +def smooth1d(x, window_len): + s = np.r_[2 * x[0] - x[window_len:1:-1], x, 2 * x[-1] - x[-1:-window_len:-1]] + w = np.hanning(window_len) + y = np.convolve(w / w.sum(), s, mode="same") + return y[window_len - 1 : -window_len + 1] + + +def smooth2d(A, sigma=3): + window_len = max(int(sigma), 3) * 2 + 1 + A1 = np.array([smooth1d(x, window_len) for x in np.asarray(A)]) + A2 = np.transpose(A1) + A3 = np.array([smooth1d(x, window_len) for x in A2]) + A4 = np.transpose(A3) + return A4 + + +class BaseFilter(object): + def prepare_image(self, src_image, dpi, pad): + ny, nx, depth = src_image.shape + padded_src = np.zeros([pad * 2 + ny, pad * 2 + nx, depth], dtype="d") + padded_src[pad:-pad, pad:-pad, :] = src_image[:, :, :] + return padded_src + + def get_pad(self, dpi): + return 0 + + def __call__(self, im, dpi): + pad = self.get_pad(dpi) + padded_src = self.prepare_image(im, dpi, pad) + tgt_image = self.process_image(padded_src, dpi) + return tgt_image, -pad, -pad + + +class OffsetFilter(BaseFilter): + def __init__(self, offsets=None): + if offsets is None: + self.offsets = (0, 0) + else: + self.offsets = offsets + + def get_pad(self, dpi): + return int(max(*self.offsets) / 72.0 * dpi) + + def process_image(self, padded_src, dpi): + ox, oy = self.offsets + a1 = np.roll(padded_src, int(ox / 72.0 * dpi), axis=1) + a2 = np.roll(a1, -int(oy / 72.0 * dpi), axis=0) + return a2 + + +class GaussianFilter(BaseFilter): + def __init__(self, sigma, alpha=0.5, color=None): + self.sigma = sigma + self.alpha = alpha + if color is None: + self.color = (0, 0, 0) + else: + self.color = color + + def get_pad(self, dpi): + return int(self.sigma * 3 / 72.0 * dpi) + + def process_image(self, padded_src, dpi): + tgt_image = np.zeros_like(padded_src) + aa = smooth2d(padded_src[:, :, -1] * self.alpha, self.sigma / 72.0 * dpi) + tgt_image[:, :, -1] = aa + tgt_image[:, :, :-1] = self.color + return tgt_image + + +class DropShadowFilter(BaseFilter): + def __init__(self, sigma, alpha=0.3, color=None, offsets=None): + self.gauss_filter = GaussianFilter(sigma, alpha, color) + self.offset_filter = OffsetFilter(offsets) + + def get_pad(self, dpi): + return max(self.gauss_filter.get_pad(dpi), self.offset_filter.get_pad(dpi)) + + def process_image(self, padded_src, dpi): + t1 = self.gauss_filter.process_image(padded_src, dpi) + t2 = self.offset_filter.process_image(t1, dpi) + return t2 + + +class FilteredArtistList(Artist): + def __init__(self, artist_list, filter): + self._artist_list = artist_list + self._filter = filter + Artist.__init__(self) + + def draw(self, renderer): + renderer.start_rasterizing() + renderer.start_filter() + for a in self._artist_list: + a.draw(renderer) + renderer.stop_filter(self._filter) + renderer.stop_rasterizing() + + +# ----------------------------------------------------------------------------- +class RangeDict(dict): + def __getitem__(self, item): + if type(item) != range: + for key in self: + if item in key: + return self[key] + else: + return super().__getitem__(item) + + +_ax1_title = RangeDict( + { + range(1, 2): "Less is More", + range(2, 5): "Remove Backgrounds", + range(5, 10): "Remove redundant labels", + range(10, 14): "Remove borders", + range(14, 16): "Reduce colors", + range(16, 19): "Remove special effects", + range(19, 22): "Remove bolding", + range(22, 25): "Lighten labels", + range(25, 29): "Ligthen lines", + range(29, 33): "Or remove lines", + range(33, 35): "Direct label", + range(35, 36): "Less is More", + } +) +_bar_facecolors = RangeDict( + { + range(1, 15): ["#22befc", "#1dB95d", "#ca5a56", "#8b6eaa", "#fcfc38"], + range(15, 36): ["#b2b2b2", "#b2b2b2", "#cb5958", "#b2b2b2", "#b2b2b2"], + } +) +_bar_edgecolor = RangeDict({range(1, 13): "#000000", range(13, 36): "None"}) +_bar_shadows = RangeDict({range(1, 18): True, range(18, 36): False}) +_xticklabels = RangeDict( + { + range(1, 36): [ + "French\nFries", + "Potato\nChips", + "Bacon\n", + "Pizza\n", + "Chili Dog\n", + ] + } +) +_xticklines_color = RangeDict( + {range(1, 27): "#000000", range(27, 31): "#b2b2b2", range(34, 36): "None"} +) +_yticklabels = RangeDict( + { + range(1, 34): ["0", "100", "200", "300", "400", "500", "600", "700"], + range(34, 36): ["", "", "", "", "", "", "", ""], + } +) +_yticklabels_weight = RangeDict({range(1, 21): "bold", range(21, 36): "normal"}) +_yticklabels_color = RangeDict( + {range(1, 24): "#000000", range(24, 34): "#b2b2b2", range(34, 36): "None"} +) +_yticklines_color = RangeDict( + {range(1, 28): "#000000", range(28, 34): "#b2b2b2", range(34, 36): "None"} +) +_grid_color = RangeDict( + {range(1, 26): "#000000", range(26, 30): "#b2b2b2", range(30, 36): "None"} +) +_ax3_title = RangeDict( + { + range(1, 7): "Calories per 100g for different foods", + range(7, 36): "Calories per 100g", + } +) +_ax3_title_weight = RangeDict({range(1, 20): "bold", range(20, 36): "normal"}) +_ax3_title_color = RangeDict({range(1, 23): "#000000", range(23, 36): "#b2b2b2"}) +_xlabel = RangeDict({range(1, 9): "Type of Food", range(9, 36): ""}) +_ylabel = RangeDict({range(1, 8): "Number of Calories", range(8, 36): ""}) +_ax2_background = RangeDict({range(1, 3): True, range(3, 36): False}) +_ax3_background = RangeDict({range(1, 4): True, range(4, 36): False}) +_legend = RangeDict({range(1, 6): True, range(6, 36): False}) +_ax2_spines_linewidth = RangeDict( + {range(1, 11): (2, 2, 2, 2), range(11, 36): (0, 0, 0, 0)} +) +_ax3_spines_linewidth = RangeDict( + { + range(1, 12): (2, 2, 2, 2), + range(12, 31): (0, 0.75, 0.75, 0), + range(31, 32): (0, 0, 0.75, 0), + range(32, 36): (0, 0, 0, 0), + } +) +_ax3_spines_color = RangeDict( + { + range(1, 27): ("k", "k", "k", "k"), + range(27, 28): ("k", "#b2b2b2", "k", "k"), + range(28, 36): ("k", "#b2b2b2", "#b2b2b2", "k"), + } +) +_direct_label = RangeDict({range(1, 35): False, range(35, 36): True}) + + +if len(sys.argv) > 1: + frame = int(sys.argv[1]) +else: + frame = 1 +frame = max(min(frame, 36), 1) + + +dpi = 100 +width, height = 640, 460 +values = [607, 542, 533, 296, 260] + +facecolors = _bar_facecolors[frame] +edgecolor = _bar_edgecolor[frame] +labels = _xticklabels[frame] +shadow = _bar_shadows[frame] +legend = _legend[frame] +ax3_background = _ax3_background[frame] +ax2_background = _ax2_background[frame] +ax2_spines_linewidth = _ax2_spines_linewidth[frame] +ax3_spines_linewidth = _ax3_spines_linewidth[frame] +ax3_spines_color = _ax3_spines_color[frame] +ax1_title = _ax1_title[frame] +ax3_title = _ax3_title[frame] +ax3_title_weight = _ax3_title_weight[frame] +ax3_title_color = _ax3_title_color[frame] +xlabel = _xlabel[frame] +ylabel = _ylabel[frame] +grid_color = _grid_color[frame] +yticklabels = _yticklabels[frame] +yticklabels_weight = _yticklabels_weight[frame] +yticklabels_color = _yticklabels_color[frame] +yticklines_color = _yticklines_color[frame] +xticklines_color = _xticklines_color[frame] +direct_label = _direct_label[frame] + + +fig = plt.figure(figsize=(width / dpi, height / dpi), dpi=dpi) + +ax1 = fig.add_axes([0.0, 0.0, 1.0, 1.0], frameon=False) +ax1.text( + 0.05, + 0.05, + "A remake of www.darkhorseanalytics.com", + ha="left", + va="top", + family="Source Sans Pro", + weight="light", +) +ax1.text( + 0.95, 0.05, "Made with matplotlib", ha="right", va="top", family="Source Sans Pro" +) + +x, y = 0.050, 0.075 +w, h = 1 - 2 * x, 1 - 2 * y - 0.1 +ax2 = fig.add_axes([x, y, w, h]) +ax2.set_xticks([]) +ax2.set_yticks([]) + +x, y = 0.165, 0.25 +w, h = 0.585, 0.45 +ax3 = fig.add_axes([x, y, w, h]) +ax3.set_ylim(0, 701) +ax3.set_yticks(range(0, 701, 100)) +ax3.set_xlim(0, 10) +ax3.tick_params("x", length=0, which="major", pad=12.5) + + +# ----------------------------------------------------------------------------- +ax1.text( + 0.05, + 0.95, + ax1_title, + ha="left", + va="top", + family="Source Sans Pro", + fontsize=32, + weight="light", +) + + +# ----------------------------------------------------------------------------- +if ax2_background: + ax2.imshow(mpimg.imread("texture.jpg")) +for linewidth, axis in zip(ax2_spines_linewidth, ["top", "bottom", "left", "right"]): + ax2.spines[axis].set_linewidth(linewidth) + +# ----------------------------------------------------------------------------- +if ax3_background: + ax3.set_facecolor(".75") + +for color, linewidth, axis in zip( + ax3_spines_color, ax3_spines_linewidth, ["top", "bottom", "left", "right"] +): + ax3.spines[axis].set_linewidth(linewidth) + ax3.spines[axis].set_color(color) + +ax3.set_xticks(range(1, 10, 2)) +ax3.set_xticklabels(labels) +if ax3_spines_linewidth[1] > 0: + locator = matplotlib.ticker.FixedLocator(range(0, 10, 2)) + ax3.xaxis.set_minor_locator(locator) + ax3.tick_params("x", length=3.5, width=0.8, which="minor", color=xticklines_color) + + +for label in ax3.get_yticklabels(): + label.set_fontweight(yticklabels_weight) + label.set_color(yticklabels_color) +ax3.tick_params(axis="y", color=yticklines_color) + + +ax3.set_xlabel(xlabel, weight="bold") +ax3.set_ylabel(ylabel, weight="bold") + +ax3.set_title( + ax3_title, weight=ax3_title_weight, size=12, loc="left", color=ax3_title_color +) + +plt.grid(axis="y", color=grid_color) + +X, Y = range(1, 10, 2), values +bars = ax3.bar(X, Y, 0.8, facecolor="w", edgecolor="k", linewidth=1.5, zorder=10) +for bar, color in zip(bars, facecolors): + bar.set_facecolor(color) + bar.set_edgecolor(edgecolor) +if direct_label: + for x, y in zip(X, Y): + ax3.text( + x, + y - 25, + "%d" % y, + ha="center", + va="top", + color="white", + zorder=20, + transform=ax3.transData, + fontsize=9, + ) + +if shadow: + gauss = DropShadowFilter(5, alpha=0.65, offsets=(3, 1)) + shadow = FilteredArtistList(bars, gauss) + ax3.add_artist(shadow) + shadow.set_zorder(bars[0].get_zorder() - 0.1) + +if legend: + ax3.legend(bars, labels, edgecolor="k", loc="center left", bbox_to_anchor=(1, 0.65)) + +plt.savefig("frame-%02d.png" % frame, dpi=dpi) +# plt.show() diff --git a/code/animation/lissajous.py b/code/animation/lissajous.py new file mode 100644 index 0000000..2c7e3c1 --- /dev/null +++ b/code/animation/lissajous.py @@ -0,0 +1,58 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +fig = plt.figure(figsize=(8, 4), dpi=100) + +frames = 500 +n = 8 + +X, Y, L = [], [], [] +for row in range(n // 2): + T2 = np.linspace(0, row * 2 * np.pi, frames) if row > 0 else np.zeros(frames) + + for col in range(n): + T1 = np.linspace(0, col * 2 * np.pi, frames) if col > 0 else np.ones(frames) + + index = n * row + col + + ax = plt.subplot(n // 2, n, 1 + index, aspect=1, frameon=False) + ax.set_xlim([-1, +1]) + ax.set_xticks([]) + ax.set_ylim([-1, +1]) + ax.set_yticks([]) + + X.append(np.cos(T1)) + Y.append(np.sin(T2)) + + ax.plot(X[-1], Y[-1], color="0.95", clip_on=False, linewidth=0.75) + (l,) = ax.plot( + [], + [], + "-o", + clip_on=False, + markevery=[-1], + markeredgewidth=2, + markerfacecolor="C0", + markeredgecolor="white", + ) + L.append(l) + + +plt.subplots_adjust(left=0.0, bottom=None, right=0.95, top=None) + + +def animate(frame): + for i in range(len(L)): + L[i].set_data(X[i][:frame], Y[i][:frame]) + if frame == 150: + plt.savefig("../../figures/animation/lissajous.pdf") + + +ani = animation.FuncAnimation(fig, animate, interval=5, frames=frames) +plt.show() diff --git a/code/animation/platecarree.py b/code/animation/platecarree.py new file mode 100644 index 0000000..7a8fa98 --- /dev/null +++ b/code/animation/platecarree.py @@ -0,0 +1,16 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import cartopy.crs as ccrs +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(10, 5)) +ax = plt.axes(projection=ccrs.PlateCarree()) +ax.coastlines() + +plt.tight_layout() +plt.savefig("../../figures/animation/platecarree.pdf") +plt.show() diff --git a/code/animation/rain.py b/code/animation/rain.py new file mode 100644 index 0000000..de444b8 --- /dev/null +++ b/code/animation/rain.py @@ -0,0 +1,47 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + + +def rain_update(frame): + global R, scatter + + R["color"][:, 3] = np.maximum(0, R["color"][:, 3] - 1 / len(R)) + R["size"] += 1 / len(R) + + i = frame % len(R) + R["position"][i] = np.random.uniform(0, 1, 2) + R["size"][i] = 0 + R["color"][i, 3] = 1 + + scatter.set_edgecolors(R["color"]) + scatter.set_sizes(1000 * R["size"].ravel()) + scatter.set_offsets(R["position"]) + + if frame == 50: + plt.savefig("../../figures/chapter-13/rain.pdf") + return (scatter,) + + +fig = plt.figure(figsize=(6, 2), facecolor="white", dpi=100) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) # , aspect=1) +scatter = ax.scatter([], [], s=[], linewidth=0.5, edgecolors=[], facecolors="None") + +n = 100 +R = np.zeros( + n, dtype=[("position", float, (2,)), ("size", float, (1,)), ("color", float, (4,))] +) +R["position"] = np.random.uniform(0, 1, (n, 2)) +R["size"] = np.linspace(0, 1, n).reshape(n, 1) +R["color"][:, 3] = np.linspace(0, 1, n) + +ax.set_xlim(0, 1), ax.set_xticks([]) +ax.set_ylim(0, 1), ax.set_yticks([]) + +animation = animation.FuncAnimation(fig, rain_update, interval=10, frames=200) +plt.show() diff --git a/code/animation/sine-cosine-mp4.py b/code/animation/sine-cosine-mp4.py new file mode 100644 index 0000000..122f7ea --- /dev/null +++ b/code/animation/sine-cosine-mp4.py @@ -0,0 +1,43 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +fig = plt.figure(figsize=(7, 2)) +ax = plt.subplot() + +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) +(line1,) = ax.plot(X, C, marker="o", markevery=[-1], markeredgecolor="white") +(line2,) = ax.plot(X, S, marker="o", markevery=[-1], markeredgecolor="white") +text = ax.text(0.01, 0.95, "Test", ha="left", va="top", transform=ax.transAxes) +ax.set_xticks([]) +ax.set_yticks([]) + + +def update(frame): + line1.set_data(X[:frame], C[:frame]) + line2.set_data(X[:frame], S[:frame]) + text.set_text("Frame %d" % frame) + if frame in [1, 32, 128, 255]: + plt.savefig("../../figures/animation/sine-cosine-frame-%03d.pdf" % frame) + return line1, line2, text + + +plt.tight_layout() +writer = animation.FFMpegWriter(fps=30) +anim = animation.FuncAnimation(fig, update, interval=10, frames=len(X)) +from tqdm.autonotebook import tqdm + +bar = tqdm(total=len(X)) +anim.save( + "../../figures/animation/sine-cosine.mp4", + writer=writer, + dpi=100, + progress_callback=lambda i, n: bar.update(1), +) +bar.close() diff --git a/code/animation/sine-cosine.py b/code/animation/sine-cosine.py new file mode 100644 index 0000000..88a9377 --- /dev/null +++ b/code/animation/sine-cosine.py @@ -0,0 +1,27 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +fig = plt.figure(figsize=(7, 2)) +ax = plt.subplot() + +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) +(line1,) = ax.plot(X, C, marker="o", markevery=[-1], markeredgecolor="white") +(line2,) = ax.plot(X, S, marker="o", markevery=[-1], markeredgecolor="white") + + +def update(frame): + line1.set_data(X[:frame], C[:frame]) + line2.set_data(X[:frame], S[:frame]) + + +plt.tight_layout() +ani = animation.FuncAnimation(fig, update, interval=10) +plt.savefig("../../figures/animation/sine-cosine.pdf") +plt.show() diff --git a/code/beyond/__pycache__/bluenoise.cpython-38.pyc b/code/beyond/__pycache__/bluenoise.cpython-38.pyc new file mode 100644 index 0000000..ddea5bd Binary files /dev/null and b/code/beyond/__pycache__/bluenoise.cpython-38.pyc differ diff --git a/code/beyond/bluenoise.py b/code/beyond/bluenoise.py new file mode 100644 index 0000000..10135cd --- /dev/null +++ b/code/beyond/bluenoise.py @@ -0,0 +1,122 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +from math import cos, sin, floor, sqrt, pi, ceil + + +def generate(shape, radius, k=32, seed=None): + """ + Generate blue noise over a two-dimensional rectangle of size (width,height) + + Parameters + ---------- + + shape : tuple + Two-dimensional domain (width x height) + radius : float + Minimum distance between samples + k : int, optional + Limit of samples to choose before rejection (typically k = 30) + seed : int, optional + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + + References + ---------- + + .. [1] Fast Poisson Disk Sampling in Arbitrary Dimensions, Robert Bridson, + Siggraph, 2007. :DOI:`10.1145/1278780.1278807` + """ + + def sqdist(a, b): + """ Squared Euclidean distance """ + dx, dy = a[0] - b[0], a[1] - b[1] + return dx * dx + dy * dy + + def grid_coords(p): + """ Return index of cell grid corresponding to p """ + return int(floor(p[0] / cellsize)), int(floor(p[1] / cellsize)) + + def fits(p, radius): + """ Check whether p can be added to the queue """ + + radius2 = radius * radius + gx, gy = grid_coords(p) + for x in range(max(gx - 2, 0), min(gx + 3, grid_width)): + for y in range(max(gy - 2, 0), min(gy + 3, grid_height)): + g = grid[x + y * grid_width] + if g is None: + continue + if sqdist(p, g) <= radius2: + return False + return True + + # When given a seed, we use a private random generator in order to not + # disturb the default global random generator + if seed is not None: + from numpy.random.mtrand import RandomState + + rng = RandomState(seed=seed) + else: + rng = np.random + + width, height = shape + cellsize = radius / sqrt(2) + grid_width = int(ceil(width / cellsize)) + grid_height = int(ceil(height / cellsize)) + grid = [None] * (grid_width * grid_height) + + p = rng.uniform(0, shape, 2) + queue = [p] + grid_x, grid_y = grid_coords(p) + grid[grid_x + grid_y * grid_width] = p + + while queue: + qi = rng.randint(len(queue)) + qx, qy = queue[qi] + queue[qi] = queue[-1] + queue.pop() + for _ in range(k): + theta = rng.uniform(0, 2 * pi) + r = radius * np.sqrt(rng.uniform(1, 4)) + p = qx + r * cos(theta), qy + r * sin(theta) + if not (0 <= p[0] < width and 0 <= p[1] < height) or not fits(p, radius): + continue + queue.append(p) + gx, gy = grid_coords(p) + grid[gx + gy * grid_width] = p + + return np.array([p for p in grid if p is not None]) + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + import matplotlib.pyplot as plt + + np.random.seed(1) + + fig = plt.figure(figsize=(9, 3.25)) + + ax = plt.subplot(1, 3, 1, aspect=1, xlim=[0, 5], xticks=[], ylim=[0, 5], yticks=[]) + V = np.random.uniform(0, 5, (1600, 2)) + ax.scatter(V[:, 0], V[:, 1], s=5, edgecolor="None", facecolor="black") + ax.set_title("Uniform distribution (n=1600)") + + ax = plt.subplot(1, 3, 2, aspect=1, xlim=[0, 5], xticks=[], ylim=[0, 5], yticks=[]) + X, Y = np.meshgrid(np.linspace(0, 5, 40), np.linspace(0, 5, 40)) + X += np.random.normal(0, 0.04, X.shape) + Y += np.random.normal(0, 0.04, Y.shape) + ax.scatter(X, Y, s=5, edgecolor="None", facecolor="black") + ax.set_title("Jittered (n=1600)") + + ax = plt.subplot(1, 3, 3, aspect=1, xlim=[0, 5], xticks=[], ylim=[0, 5], yticks=[]) + V = generate([5, 5], 0.099) + ax.scatter(V[:, 0], V[:, 1], s=5, edgecolor="None", facecolor="black") + ax.set_title("Blue noise ditribution (n=%d)" % len(V)) + + plt.tight_layout() + plt.savefig("../../figures/beyond/bluenoise.pdf") + plt.show() diff --git a/code/beyond/dungeon.py b/code/beyond/dungeon.py new file mode 100644 index 0000000..47b6ebc --- /dev/null +++ b/code/beyond/dungeon.py @@ -0,0 +1,246 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import scipy.spatial +import shapely.geometry +import matplotlib.pyplot as plt +from matplotlib.collections import LineCollection +from matplotlib.patches import Polygon, Ellipse +import bluenoise + +# This is important because "cities" have been manually positionned +np.random.seed(1) + + +# Hatch pattern with given orientation (or random if None given) +def hatch(n=4, theta=None): + theta = theta or np.random.uniform(0, np.pi) + P = np.zeros((n, 2, 2)) + X = np.linspace(-0.5, +0.5, n, endpoint=True) + P[:, 0, 1] = -0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 1] = +0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 0] = X + np.random.normal(0, 0.025, n) + P[:, 0, 0] = X + np.random.normal(0, 0.025, n) + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[c, s], [-s, c]]) + return P @ Z.T + + +def seg_dists(p, a, b): + """Cartesian distance from point to line segment + + Args: + - p: np.array of single point, shape (2,) or 2D array, shape (x, 2) + - a: np.array of shape (x, 2) + - b: np.array of shape (x, 2) + """ + # normalized tangent vectors + d_ba = b - a + d = np.divide(d_ba, (np.hypot(d_ba[:, 0], d_ba[:, 1]).reshape(-1, 1))) + + # signed parallel distance components + # rowwise dot products of 2D vectors + s = np.multiply(a - p, d).sum(axis=1) + t = np.multiply(p - b, d).sum(axis=1) + + # clamped parallel distance + h = np.maximum.reduce([s, t, np.zeros(len(s))]) + + # perpendicular distance component + # rowwise cross products of 2D vectors + d_pa = p - a + c = d_pa[:, 0] * d[:, 1] - d_pa[:, 1] * d[:, 0] + + return np.hypot(h, c) + + +# Actual drawing +fig = plt.figure(figsize=(7, 7)) +fig.patch.set_facecolor("#ffffff") +ax = plt.subplot(1, 1, 1, aspect=1, frameon=False) + + +# Figure border using the hatch pattern. They are first spread according to +# a blue noise distribution, scaled according to the distance to the nearest +# neighbour and then lines composing the hatch are clipped against the +# corresponding Voronoi cell. +h = 4 # Number of line segments composing a hatch +radius = 0.2 # Minimum radius between points +# (the smaller, the longer to compute) + +P = bluenoise.generate((15, 15), radius=radius) - (0.5, 0.5) +Walls = np.array( + [ + [1, 1], + [5, 1], + [5, 3], + [8, 3], + [8, 2], + [11, 2], + [11, 5], + [10, 5], + [10, 6], + [12, 6], + [12, 8], + [13, 8], + [13, 10], + [11, 10], + [11, 12], + [2, 12], + [2, 10], + [1, 10], + [1, 7], + [4, 7], + [4, 10], + [3, 10], + [3, 11], + [10, 11], + [10, 10], + [9, 10], + [9, 8], + [11, 8], + [11, 7], + [9, 7], + [9, 5], + [8, 5], + [8, 4], + [5, 4], + [5, 6], + [1, 6], + [1, 1], + ] +) +walls = Polygon( + Walls, + closed=True, + zorder=10, + facecolor="white", + edgecolor="None", + lw=3, + joinstyle="round", +) +ax.add_patch(walls) + +for i in range(-5, 15): + ax.axhline( + i, + color=".5", + clip_path=walls, + zorder=20, + linestyle=(0, (1, 4)), + linewidth=1, + dash_capstyle="round", + ) + ax.axvline( + i, + color=".5", + clip_path=walls, + zorder=20, + linestyle=(0, (1, 4)), + linewidth=1, + dash_capstyle="round", + ) + +walls = Polygon( + Walls, + closed=True, + zorder=30, + facecolor="None", + edgecolor="black", + lw=3, + joinstyle="round", +) +ax.add_patch(walls) + +# ax.scatter([3.5], [7.5], s=250, marker="x", zorder=100, color="black", linewidth=5) +# ax.text(3.5, 7.5, "X", +# family="Morris Roman", size=24, zorder=20, ha="center", va="center") + +for i in range(30): + ellipse = Ellipse( + xy=np.random.uniform(1, 12, 2), + width=np.random.uniform(0.05, 0.15), + height=np.random.uniform(0.05, 0.15), + zorder=100, + facecolor="white", + edgecolor="black", + linewidth=1.25, + clip_on=True, + angle=np.random.uniform(0, 360), + ) + ax.add_artist(ellipse) + ellipse.set_clip_path(walls) + +for i in range(20): + ellipse = Ellipse( + xy=np.random.normal(2, 0.2, 2), + width=np.random.uniform(0.05, 0.15), + height=np.random.uniform(0.05, 0.15), + zorder=100, + facecolor="white", + edgecolor="black", + linewidth=1.25, + clip_on=True, + angle=np.random.uniform(0, 360), + ) + ax.add_artist(ellipse) + ellipse.set_clip_path(walls) + + +D = scipy.spatial.distance.cdist(P, P) +D.sort(axis=1) +S = [] +vor = scipy.spatial.Voronoi(P) +for i in range(len(vor.point_region)): + region = vor.regions[vor.point_region[i]] + if not -1 in region and min(seg_dists(vor.points[i], Walls[:-1], Walls[1:])) < 0.35: + verts = np.array([vor.vertices[i] for i in region]) + poly = shapely.geometry.Polygon(verts) + H = 1.25 * D[i, 1] * hatch(h) + P[i] + for i in range(len(H)): + line = shapely.geometry.LineString(H[i]) + intersect = poly.intersection(line) + if intersect: + S.append(intersect.coords) + +# Grey background using thick lines +hatches = LineCollection(S, color="#eeeeee", linewidth=7, capstyle="round", zorder=-20) +ax.add_collection(hatches) + +# Actual hatches +hatches = LineCollection(S, color="black", linewidth=1.5, capstyle="round", zorder=-10) +ax.add_collection(hatches) + +ax.text( + 6, + 0.75, + "Matplotlib Dungeon", + clip_on=False, + family="Morris Roman", + size=32, + zorder=20, + ha="left", + va="center", +) +ax.text( + 6, + 0.1, + "A brand new adventure in Scientific Python", + clip_on=False, + family="Morris Roman", + size=16, + zorder=20, + ha="left", + va="center", +) + + +ax.set_xlim(0, 14), ax.set_xticks([]) +ax.set_ylim(-0, 12.5), ax.set_yticks([]) + +plt.tight_layout() +plt.savefig("../../figures/beyond/dungeon.pdf") +plt.show() diff --git a/code/beyond/dungeon.svg b/code/beyond/dungeon.svg new file mode 100644 index 0000000..646ec2c --- /dev/null +++ b/code/beyond/dungeon.svg @@ -0,0 +1,17 @@ + + + + + Produced by OmniGraffle 7.18.5\n2021-08-11 14:14:05 +0000 + + Canvas 1 + + + Layer 1 + + Bézier + + + + + diff --git a/code/beyond/dyson-hatching.py b/code/beyond/dyson-hatching.py new file mode 100644 index 0000000..a64ef51 --- /dev/null +++ b/code/beyond/dyson-hatching.py @@ -0,0 +1,85 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import bluenoise +import numpy as np +import scipy.spatial +import shapely.geometry +import matplotlib.pyplot as plt +from matplotlib.collections import LineCollection + + +# Hatch pattern with given orientation (or random if None given) +def hatch(n=4, theta=None): + theta = theta or np.random.uniform(0, np.pi) + P = np.zeros((n, 2, 2)) + X = np.linspace(-0.5, +0.5, n, endpoint=True) + P[:, 0, 1] = -0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 1] = +0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 0] = X + np.random.normal(0, 0.025, n) + P[:, 0, 0] = X + np.random.normal(0, 0.025, n) + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[c, s], [-s, c]]) + return P @ Z.T + + +# Points (blue noise distribution) +P = bluenoise.generate((10, 10), radius=0.5) + +# Voronoi cells +V = scipy.spatial.Voronoi(P) + +# Sorted distances between pojnts +D = scipy.spatial.distance.cdist(P, P) +D.sort(axis=1) + + +# Actual drawing +fig = plt.figure(figsize=(9, 3.25)) + + +# Point + Voronoi cells +ax = plt.subplot(1, 3, 1, aspect=1, xlim=[0, 10], xticks=[], ylim=[0, 10], yticks=[]) +for i in range(len(V.point_region)): + region = V.regions[V.point_region[i]] + if not -1 in region: + verts = np.array([V.vertices[i] for i in region]) + cell = plt.Polygon(verts, edgecolor=".25", facecolor="None") + ax.add_artist(cell) +ax.scatter(P[:, 0], P[:, 1], s=5, color="black", zorder=10) +ax.set_title("Random points (blue noise)") + + +# Hatches (unclipped) +ax = plt.subplot(1, 3, 2, aspect=1, xlim=[0, 10], xticks=[], ylim=[0, 10], yticks=[]) +S = [] +for i in range(len(V.point_region)): + region = V.regions[V.point_region[i]] + if not -1 in region: + verts = np.array([V.vertices[i] for i in region]) + S.extend((1.25 * D[i, 1] * hatch(5) + P[i])) +ax.add_collection(LineCollection(S, color="black")) +ax.set_title("Unclipped hatches") + + +# Hatches (clipped) +ax = plt.subplot(1, 3, 3, aspect=1, xlim=[0, 10], xticks=[], ylim=[0, 10], yticks=[]) +S = [] +for i in range(len(V.point_region)): + region = V.regions[V.point_region[i]] + if not -1 in region: + verts = np.array([V.vertices[i] for i in region]) + poly = shapely.geometry.Polygon(verts) + for l in 1.25 * D[i, 1] * hatch(5) + P[i]: + line = shapely.geometry.LineString(l) + intersect = poly.intersection(line) + if intersect: + S.append(intersect.coords) +ax.add_collection(LineCollection(S, color="black")) +ax.set_title("Clipped hatches") + +plt.tight_layout() +plt.savefig("../../figures/beyond/dyson-hatching.pdf") +plt.show() diff --git a/code/beyond/interactive-loupe.py b/code/beyond/interactive-loupe.py new file mode 100644 index 0000000..ede8ddd --- /dev/null +++ b/code/beyond/interactive-loupe.py @@ -0,0 +1,107 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +from matplotlib.patches import Circle +from matplotlib.figure import Figure +from matplotlib.backends.backend_agg import FigureCanvas + + +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + + +P = np.random.uniform(0, 5, (500, 2)) +C = np.random.uniform(5, 25, 500) + + +def plot(ax): + ax.xaxis.set_major_locator(ticker.MultipleLocator(1.0)) + ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + ax.yaxis.set_major_locator(ticker.MultipleLocator(1.0)) + ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + ax.yaxis.set_minor_formatter(ticker.FormatStrFormatter("%.2f")) + for i, label in enumerate(ax.get_yticklabels(which="minor")): + label.set_size(7) + ax.xaxis.set_minor_formatter(ticker.FormatStrFormatter("%.2f")) + for i, label in enumerate(ax.get_xticklabels(which="minor")): + label.set_size(7) + ax.grid(True, "minor", color="0.85", linewidth=0.50, zorder=-20) + ax.grid(True, "major", color="0.65", linewidth=0.75, zorder=-10) + + ax.scatter(P[:, 0], P[:, 1], C, color="black", zorder=10) + + +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_subplot(xlim=(0.0, 5), ylim=(0.0, 5), aspect=1) +plot(ax) + +ifig = Figure(figsize=(6, 6), dpi=100, frameon=False) +canvas = FigureCanvas(ifig) +iax = ifig.add_subplot(xlim=(0.0, 5), ylim=(0.0, 5), aspect=1) +plot(iax) +plt.tight_layout() +canvas.draw() +Z = np.array(canvas.renderer.buffer_rgba()) +del ifig + + +background = fig.canvas.copy_from_bbox(ax.bbox) + + +def on_motion(event): + x, y = event.xdata, event.ydata + if x is None or y is None: + return + fig.canvas.restore_region(background) + + circle_bg.set_center((x, y)) + ax.draw_artist(circle_bg) + + circle_fg.set_center((x, y)) + ax.draw_artist(circle_fg) + + image.set_extent([0 - x, 10 - x, 0 - y, 10 - y]) + image.set_clip_path(circle_bg) + ax.draw_artist(image) + + fig.canvas.blit(ax.bbox) + + +cid = fig.canvas.mpl_connect("motion_notify_event", on_motion) + +circle_bg = Circle( + (1, 1), + 1, + transform=ax.transData, + zorder=20, + clip_on=False, + edgecolor="none", + facecolor="white", +) +ax.add_artist(circle_bg) + +circle_fg = Circle( + (1, 1), + 1, + transform=ax.transData, + zorder=30, + clip_on=False, + linewidth=2, + edgecolor="black", + facecolor="None", +) +ax.add_artist(circle_fg) + +image = ax.imshow(Z, extent=[0, 5, 0, 5], zorder=25, clip_on=True) +image.set_extent([0, 10, 0, 10]) +image.set_clip_path(circle_bg) + +plt.tight_layout() +plt.show() diff --git a/code/beyond/polygon-clipping.py b/code/beyond/polygon-clipping.py new file mode 100644 index 0000000..6526d6a --- /dev/null +++ b/code/beyond/polygon-clipping.py @@ -0,0 +1,194 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate polygon and clipping +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.path as mpath +import matplotlib.patches as mpatches + + +def circle(center, radius): + """ Regular circle path """ + + T = np.arange(0, np.pi * 2.0, 0.01) + T = T.reshape(-1, 1) + X = center[0] + radius * np.cos(T) + Y = center[1] + radius * np.sin(T) + vertices = np.hstack((X, Y)) + codes = np.ones(len(vertices), dtype=mpath.Path.code_type) * mpath.Path.LINETO + codes[0] = mpath.Path.MOVETO + return vertices, codes + + +def rectangle(center, size): + """ Regular rectangle path """ + + (x, y), (w, h) = center, size + vertices = np.array([(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)]) + codes = np.array( + [ + mpath.Path.MOVETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + ] + ) + return vertices, codes + + +def patch( + ax, path, facecolor="0.9", edgecolor="black", linewidth=1, linestyle="-", clip=None +): + """ Build a patch with potential clipping path """ + patch = mpatches.PathPatch( + path, + linewidth=linewidth, + linestyle=linestyle, + facecolor=facecolor, + edgecolor=edgecolor, + ) + if clip: + # If the path is not drawn, clipping doesn't work + clip_patch = mpatches.PathPatch( + clip, linewidth=0, facecolor="None", edgecolor="None" + ) + ax.add_patch(clip_patch) + patch.set_clip_path(clip_patch) + ax.add_patch(patch) + + +def subplot(cols, rows, index, title): + """ Shortcut to subplot to factorize options""" + ax = plt.subplot(cols, rows, index, aspect=1) + ax.text(-1.25, 0.75, "A", size="large", ha="center", va="center") + ax.text(+1.25, 0.75, "B", size="large", ha="center", va="center") + ax.set_title(title, weight="bold") + ax.set_xlim(-1.5, +1.5), ax.set_xticks([]) + ax.set_ylim(-1, +1), ax.set_yticks([]) + return ax + + +V1, C1 = rectangle((-1.5, -1), size=(3, 2)) +V2, C2 = circle((-0.5, 0), radius=0.75) +# For A, we use clock-wise circle vertices +A = mpath.Path(V2, C2) +# For ¬A, we use counter clock-wise circle vertices +A_ = mpath.Path(np.concatenate([V2[::-1], V1]), np.concatenate([C2, C1])) + +V2, C2 = circle((+0.5, 0), radius=0.75) +# For B, we use clock-wise circle vertices +B = mpath.Path(V2, C2) +# For ¬B, we use counter clock-wise circle vertices +B_ = mpath.Path(np.concatenate([V2[::-1], V1]), np.concatenate([C2, C1])) + + +fig = plt.figure(figsize=(9, 5)) +rows, cols = 3, 4 + +# A +ax = subplot(rows, cols, 1, "A") +patch(ax, A, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="-", linewidth=1.5) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) + +# B +ax = subplot(rows, cols, 2, "B") +patch(ax, B, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="-", linewidth=1.4) + +# ¬A +ax = subplot(rows, cols, 3, "¬A") +patch(ax, A_, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="-", linewidth=1.5) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +plt.setp(ax.spines.values(), linewidth=1.5) + +# ¬B +ax = subplot(rows, cols, 4, "¬B") +patch(ax, B_, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="-", linewidth=1.5) +plt.setp(ax.spines.values(), linewidth=1.5) + +# A ∪ B +ax = subplot(rows, cols, 5, "A ∪ B") +patch(ax, A, edgecolor="None") +patch(ax, B, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", edgecolor="black", clip=A_, linewidth=1.5) +patch(ax, A, facecolor="None", edgecolor="black", clip=B_, linewidth=1.5) + +# A ∪ ¬B +ax = subplot(rows, cols, 6, "A ∪ ¬B") +patch(ax, A, edgecolor="None") +patch(ax, B_, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", edgecolor="black", clip=A_, linewidth=1.5) +patch(ax, A, facecolor="None", edgecolor="black", clip=B, linewidth=1.5) +plt.setp(ax.spines.values(), linewidth=1.5) + +# ¬A ∪ B +ax = subplot(rows, cols, 7, "¬A ∪ B") +patch(ax, A_, edgecolor="None") +patch(ax, B, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, A, facecolor="None", edgecolor="black", clip=B_, linewidth=1.5) +patch(ax, B, facecolor="None", edgecolor="black", clip=A, linewidth=1.5) +plt.setp(ax.spines.values(), linewidth=1.5) + +# ¬A ∪ ¬B +ax = subplot(rows, cols, 8, "¬A ∪ ¬B") +patch(ax, A_, edgecolor="None") +patch(ax, B_, edgecolor="None") +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, A, facecolor="None", edgecolor="black", clip=B, linewidth=1.5) +patch(ax, B, facecolor="None", edgecolor="black", clip=A, linewidth=1.5) +plt.setp(ax.spines.values(), linewidth=1.5) + +# A ∩ B +ax = subplot(rows, cols, 9, "A ∩ B") +patch(ax, A, edgecolor="None", clip=B) +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, A, facecolor="None", edgecolor="black", clip=B, linewidth=1.5) +patch(ax, B, facecolor="None", edgecolor="black", clip=A, linewidth=1.5) + +# A ∩ ¬B +ax = subplot(rows, cols, 10, "A ∩ ¬B") +patch(ax, A, edgecolor="None", clip=B_) +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, A, facecolor="None", edgecolor="black", clip=B_, linewidth=1.5) +patch(ax, B, facecolor="None", edgecolor="black", clip=A, linewidth=1.5) + + +# ¬A ∩ B +ax = subplot(rows, cols, 11, "¬A ∩ B") +patch(ax, B, edgecolor="None", clip=A_) +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", edgecolor="black", clip=A_, linewidth=1.5) +patch(ax, A, facecolor="None", edgecolor="black", clip=B, linewidth=1.5) + +# ¬A ∩ ¬B +ax = subplot(rows, cols, 12, "¬A ∩ ¬B") +patch(ax, A_, edgecolor="None", clip=B_) +patch(ax, A, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", linestyle="--", linewidth=1) +patch(ax, B, facecolor="None", edgecolor="black", clip=A_, linewidth=1.5) +patch(ax, A, facecolor="None", edgecolor="black", clip=B_, linewidth=1.5) +plt.setp(ax.spines.values(), linewidth=1.5) + +plt.tight_layout() +plt.savefig("../../figures/beyond/polygon-clipping.pdf") +plt.show() diff --git a/code/beyond/radial-maze-path.npy b/code/beyond/radial-maze-path.npy new file mode 100644 index 0000000..3d631af Binary files /dev/null and b/code/beyond/radial-maze-path.npy differ diff --git a/code/beyond/radial-maze.py b/code/beyond/radial-maze.py new file mode 100644 index 0000000..5eaa019 --- /dev/null +++ b/code/beyond/radial-maze.py @@ -0,0 +1,165 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import os +import numpy as np +import matplotlib.path as path +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + +# Maze parameters +n_arms = 8 +n_squares = 5 +square_width = 1.5 +square_height = 1.5 +inner_radius = 3 +outer_radius = inner_radius + n_squares * square_height +delta = 0.1 # cosmetic to widen a bit maze arms + +W, C = [], [] +for i in range(n_arms): + theta = i / n_arms * 2 * np.pi + + # Build external walls + N = (square_width / 2 + delta) * np.array([np.sin(theta), -np.cos(theta)]) + V_in = inner_radius * np.array([np.cos(theta), np.sin(theta)]) + V_out = (outer_radius + delta) * np.array([np.cos(theta), np.sin(theta)]) + W.extend([V_in + N, V_out + N, V_out - N, V_in - N]) + + # Build arm start (pie) + t0 = theta - 0.5 / n_arms * 2 * np.pi + t1 = theta + 0.5 / n_arms * 2 * np.pi + V = [] + for t in np.linspace(t0, t1, 25): + V.append(1.00 * inner_radius * np.array([np.cos(t), np.sin(t)])) + for t in np.linspace(t1, t0, 25): + V.append(0.25 * inner_radius * np.array([np.cos(t), np.sin(t)])) + V.append(V[-1]) + C.append(V) + + # Build arms squares + N = square_width / 2 * np.array([np.sin(theta), -np.cos(theta)]) + T = square_height / 2 * np.array([np.cos(theta), np.sin(theta)]) + for j in range(n_squares): + r = inner_radius + (j + 0.5) / n_squares * (outer_radius - inner_radius) + V = r * np.array([np.cos(theta), np.sin(theta)]) + C.append([V - T + N, V + T + N, V + T - N, V - T - N]) + +W.append(W[0]) +W = np.array(W) + + +class PathTracer: + """ Path tracer on a figure + + You can trace a path using your mouse: + 1. Click to start + 2. Move th mouse + 3. Click to stop and save (if save=True) + """ + + def __init__(self, line=None, load=True, save=True, filename="path.npy"): + self.line = line + self.save = save + self.filename = filename + self.xs = [] + self.ys = [] + self.active = False + if load and os.path.exists(self.filename): + P = np.load(self.filename) + self.xs = P[:, 0].tolist() + self.ys = P[:, 1].tolist() + self.line.set_data(self.xs, self.ys) + self.line.figure.canvas.draw() + line.figure.canvas.mpl_connect("button_press_event", self.on_press) + line.figure.canvas.mpl_connect("motion_notify_event", self.on_motion) + + def on_press(self, event): + if event.inaxes != self.line.axes: + return + if not self.active: + self.active = True + self.xs = [event.xdata] + self.ys = [event.ydata] + else: + self.active = False + if self.save: + P = np.c_[self.xs, self.ys] + np.save(self.filename, P) + self.line.set_data(self.xs, self.ys) + self.line.figure.canvas.draw() + + def on_motion(self, event): + if event.inaxes != self.line.axes or not self.active: + return + x, y = event.xdata, event.ydata + d = np.sqrt((x - self.xs[-1]) ** 2 + (y - self.ys[-1]) ** 2) + if d < 0.1: + return + self.xs.append(event.xdata) + self.ys.append(event.ydata) + self.line.set_data(self.xs, self.ys) + self.line.figure.canvas.draw() + + +# The maze-path as been drawn by hand using the path tracer above +# Here we count the number of path points contained in each part +# of the same such as to set the color usng a colormap +P = np.load("radial-maze-path.npy") +N = np.zeros(len(C)) +for i, vertices in enumerate(C): + codes = [path.Path.MOVETO] + [path.Path.LINETO,] * (len(vertices) - 1) + p = path.Path(vertices, codes) + N[i] = p.contains_points(P).sum() +N = 1 - (N - N.min()) / (N.max() - N.min()) +cmap = plt.cm.get_cmap("viridis") +colors = cmap(N) + +# Alternatively, you can also set colors on each arm +# colors = [] # this will be a list of n_arms * n_squares colors +# for arm in range(n_arms): +# for square in range(n_squares+1): +# if arm < 6: +# color = cmap(square/n_squares) +# else: +# color = 0.00, 0.0, 0.00, 0.05 +# colors.append(color) + +# ------------------------------------------------------- +fig = plt.figure(figsize=(8, 8)) +ax = plt.subplot(1, 1, 1, aspect=1, frameon=False, xticks=[], yticks=[]) + +# Borders +plt.plot(W[:, 0], W[:, 1], color="black", linewidth=2, zorder=10) + +# Squares +collection = PolyCollection( + C, closed=True, linewidth=2, facecolors=colors, edgecolors="white" +) +ax.add_collection(collection) + +# Letters +for i in range(n_arms): + theta = i / n_arms * 2 * np.pi + x = (outer_radius + 1) * np.cos(theta) + y = (outer_radius + 1) * np.sin(theta) + ax.text( + x, + y, + chr(ord("A") + i), + weight="bold", + rotation=i / n_arms * 360 - 90, + va="center", + ha="center", + size="xx-large", + ) + +# Path tracker click to start and end (clear previous path) +(line,) = ax.plot([], [], linestyle="-", color="black", linewidth=1.0, alpha=0.75) +tracer = PathTracer(line, save=False, filename="radial-maze-path.npy") + +plt.tight_layout() +plt.savefig("../../figures/beyond/radial-maze.pdf") +plt.show() diff --git a/code/beyond/stamp.py b/code/beyond/stamp.py new file mode 100644 index 0000000..f48f5bd --- /dev/null +++ b/code/beyond/stamp.py @@ -0,0 +1,53 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import imageio +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import FancyBboxPatch + +I = imageio.imread("../data/mona-lisa.png") +height, width = np.array(I.shape) + +cmap = plt.get_cmap("cividis") +fig = plt.figure(figsize=(width / 150, height / 150)) +ax = fig.add_axes( + [0, 0, 1, 1], + aspect=1, + frameon=False, + xlim=[0, width], + xticks=[], + ylim=[0, height], + yticks=[], +) + +box = FancyBboxPatch( + (0, 0), + width, + height, + zorder=40, + boxstyle="roundtooth,tooth_size=24", + lw=8, + ec="white", + fc="None", +) +ax.add_patch(box) +box = FancyBboxPatch( + (0, 0), + width, + height, + zorder=50, + boxstyle="roundtooth,tooth_size=24", + lw=3, + ec="black", + fc="None", +) +ax.add_patch(box) + +im = ax.imshow(I, extent=[0, width, 0, height], zorder=30, cmap=cmap) +im.set_clip_path(box) + +plt.savefig("../../figures/beyond/stamp.png", dpi=300) +plt.show() diff --git a/code/beyond/tikz-dashes.py b/code/beyond/tikz-dashes.py new file mode 100644 index 0000000..88fe404 --- /dev/null +++ b/code/beyond/tikz-dashes.py @@ -0,0 +1,136 @@ +import numpy as np +import matplotlib.pyplot as plt + + +def style(index, e=0.001): + patterns = [ + ("densely dotted", (-0.5, (e, 2 - e))), + ("dotted", (-0.5, (e, 3 - e))), + ("loosely dotted", (-0.5, (e, 5 - e))), + ("densely dashed", (-0.5, (2, 3))), + ("dashed", (-0.5, (2, 4))), + ("loosely dashed", (-0.5, (2, 7))), + ("densely dashdotted", (-0.5, (2, 2, e, 2 - e))), + ("dashdotted", (-0.5, (2, 3, e, 2 - e))), + ("loosely dashdotted", (-0.5, (2, 5, e, 5 - e))), + ("densely dashdotdotted", (-0.5, (2, 2, e, 2 - e, e, 2 - e))), + ("dashdotdotted", (-0.5, (2, 3, e, 3 - e, e, 3 - e))), + ("loosely dashdotdotted", (-0.5, (2, 5, e, 5 - e, e, 5 - e))), + ] + return patterns[index] + + +fig = plt.figure(figsize=(4.25, 1.5)) +ax = fig.add_axes( + [0, 0, 1, 1], + xlim=[-1, 23], + ylim=[-1, 7], + frameon=False, + xticks=[], + yticks=[], + aspect=1, +) + +for i in range(4): + for j in range(3): + name, pattern = style(i * 3 + j) + X = np.array([j * 8, j * 8 + 6]) + Y = np.array([i * 2, i * 2]) + plt.plot( + X, + Y + 0.25, + color="0.4", + linewidth=2.0, + dash_capstyle="projecting", + linestyle=pattern, + ) + plt.plot( + X, Y, color="0.3", linewidth=2.0, dash_capstyle="round", linestyle=pattern + ) + plt.text(j * 8 + 3, i * 2 - 0.5, name, ha="center", va="top", size="x-small") + + # Because of butt capstlye, we have to modify patterns a bit + name, pattern = style(i * 3 + j, e=0.25) + plt.plot( + X, + Y + 0.5, + color="0.5", + linewidth=2.0, + dash_capstyle="butt", + linestyle=pattern, + ) + +plt.savefig("tikz-dashes.pdf") +plt.show() + + +# # Line width +# # ----------------------------------------------------------------------------- +# y = 18 +# for i,linewidth in enumerate([1,2,3,4,5]): +# X,Y = [.5+4.25*i, .5+4.25*i+3], [y,y] +# plt.plot(X, Y, color="black", linewidth=linewidth, solid_capstyle="round") +# plt.text((X[0]+X[1])/2, y-.75, "%.1f" % linewidth, +# size="x-small", ha="center", va="top") +# plt.text(0.5, y+0.75, "Line width", size="small", ha="left", va="bottom") + +# # Solid cap style +# # ----------------------------------------------------------------------------- +# y = 14 +# for i,capstyle in enumerate(["butt", "round", "projecting"]): +# X,Y = [.75+7*i, .75+7*i+5.5], [y,y] +# plt.plot(X, Y, color=".85", linewidth=8, +# solid_capstyle="projecting") +# plt.plot(X, Y, color="black", linewidth=8, +# solid_capstyle=capstyle) +# plt.plot(X, Y, color="white", linewidth=1, marker="|", markersize=5.0) +# plt.text((X[0]+X[1])/2, y-.75, capstyle, +# size="x-small", ha="center", va="top") +# plt.text(0.5, y+0.75, "Solid cap style", size="small", ha="left", va="bottom") + +# # Dash cap style +# # ----------------------------------------------------------------------------- +# y = 10 +# for i,capstyle in enumerate(["butt", "round", "projecting"]): +# X,Y = [.75+7*i, .75+7*i+5.5], [y,y] +# plt.plot(X, Y, color=".85", linewidth=8, linestyle="--", +# solid_capstyle=capstyle, dash_capstyle="projecting") +# plt.plot(X, Y, color="black", linewidth=8, linestyle="--", +# solid_capstyle=capstyle, dash_capstyle=capstyle) +# plt.text((X[0]+X[1])/2, y-.75, capstyle, +# size="x-small", ha="center", va="top") +# plt.text(0.5, y+0.75, "Dash cap style", size="small", ha="left", va="bottom") + + +# # Dash style +# # ----------------------------------------------------------------------------- +# y = 6 +# for i,linestyle in enumerate(["-","--",":","-.", (0,(0.01,2))]): +# X,Y = [.5+4.25*i, .5+4.25*i+3], [y,y] +# plt.plot(X, Y, color="black", linewidth=2.0, linestyle=linestyle, +# solid_capstyle="round", dash_capstyle="round") +# if isinstance(linestyle,str): +# plt.text((X[0]+X[1])/2, y-.75, '"%s"' % str(linestyle), +# size="x-small", ha="center", va="top", family="monospace") +# else: +# plt.text((X[0]+X[1])/2, y-.75, "(0,(0.01,2))", +# size="x-small", ha="center", va="top") +# plt.text(0.5, y+0.75, "Classic dash style", size="small", ha="left", va="bottom") + + +# # Color cycle +# # ----------------------------------------------------------------------------- +# y = 2 +# X = np.linspace(1,20,10) +# Y = np.ones(len(X))*y +# C = ["C%d" % i for i in range(10)] +# plt.scatter(X, Y, s=200, color=C) +# for x,c in zip(X,C): +# plt.text(x, y-0.75, '"%s"' % c, +# size="x-small", ha="center", va="top", family="monospace") +# plt.text(0.5, y+0.75, "Color cycle", size="small", ha="left", va="bottom") + + +# plt.tight_layout() +# plt.savefig("reference-lines.pdf", dpi=600) +# plt.show() diff --git a/code/beyond/tinybot.py b/code/beyond/tinybot.py new file mode 100644 index 0000000..e912f82 --- /dev/null +++ b/code/beyond/tinybot.py @@ -0,0 +1,285 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.path import Path +from matplotlib.patches import PathPatch +from matplotlib.ticker import MultipleLocator +from matplotlib.patches import Circle, Rectangle +from matplotlib.collections import LineCollection + + +def line_intersect(p1, p2, P3, P4): + + p1 = np.atleast_2d(p1) + p2 = np.atleast_2d(p2) + P3 = np.atleast_2d(P3) + P4 = np.atleast_2d(P4) + + x1, y1 = p1[:, 0], p1[:, 1] + x2, y2 = p2[:, 0], p2[:, 1] + X3, Y3 = P3[:, 0], P3[:, 1] + X4, Y4 = P4[:, 0], P4[:, 1] + + D = (Y4 - Y3) * (x2 - x1) - (X4 - X3) * (y2 - y1) + + # Colinearity test + C = D != 0 + UA = (X4 - X3) * (y1 - Y3) - (Y4 - Y3) * (x1 - X3) + UA = np.divide(UA, D, where=C) + UB = (x2 - x1) * (y1 - Y3) - (y2 - y1) * (x1 - X3) + UB = np.divide(UB, D, where=C) + + # Test if intersections are inside each segment + C = C * (UA > 0) * (UA < 1) * (UB > 0) * (UB < 1) + + X = np.where(C, x1 + UA * (x2 - x1), np.inf) + Y = np.where(C, y1 + UA * (y2 - y1), np.inf) + return np.stack([X, Y], axis=1) + + +class Maze: + """ + A simple 8-maze made of straight walls (line segments) + """ + + def __init__(self): + self.walls = np.array( + [ + # Surrounding walls + [(0, 0), (0, 500)], + [(0, 500), (300, 500)], + [(300, 500), (300, 0)], + [(300, 0), (0, 0)], + # Bottom hole + [(100, 100), (200, 100)], + [(200, 100), (200, 200)], + [(200, 200), (100, 200)], + [(100, 200), (100, 100)], + # Top hole + [(100, 300), (200, 300)], + [(200, 300), (200, 400)], + [(200, 400), (100, 400)], + [(100, 400), (100, 300)], + # Moving walls (invisibles) to constraing bot path + [(0, 250), (100, 200)], + [(200, 300), (300, 250)], + ] + ) + + def draw(self, ax, grid=True, margin=5): + """ + Render the maze + """ + + # Buidling a filled patch from walls + V, C, S = [], [], self.walls + V.extend(S[0 + i, 0] for i in [0, 1, 2, 3, 0]) + V.extend(S[4 + i, 0] for i in [0, 1, 2, 3, 0]) + V.extend(S[8 + i, 0] for i in [0, 1, 2, 3, 0]) + C = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] * 3 + path = Path(V, C) + patch = PathPatch( + path, clip_on=False, linewidth=1.5, edgecolor="black", facecolor="white" + ) + + # Set figure limits, grid and ticks + ax.set_axisbelow(True) + ax.add_artist(patch) + ax.set_xlim(0 - margin, 300 + margin) + ax.set_ylim(0 - margin, 500 + margin) + if grid: + ax.xaxis.set_major_locator(MultipleLocator(100)) + ax.xaxis.set_minor_locator(MultipleLocator(10)) + ax.yaxis.set_major_locator(MultipleLocator(100)) + ax.yaxis.set_minor_locator(MultipleLocator(10)) + ax.grid(True, "major", color="0.75", linewidth=1.00, clip_on=False) + ax.grid(True, "minor", color="0.75", linewidth=0.50, clip_on=False) + ax.set_xticklabels([]) + ax.set_yticklabels([]) + ax.tick_params(axis="both", which="major", size=0) + ax.tick_params(axis="both", which="minor", size=0) + + +class Bot: + def __init__(self): + self.size = 10 + self.position = 150, 250 + self.orientation = 0 + self.n_sensors = 8 + A = np.linspace(-np.pi / 2, +np.pi / 2, self.n_sensors + 2, endpoint=True)[1:-1] + self.sensors = { + "angle": A, + "range": 75 * np.ones((self.n_sensors, 1)), + "value": np.ones((self.n_sensors, 1)), + } + + def draw(self, ax): + + # Sensors + n = 2 * len(self.sensors["angle"]) + sensors = LineCollection( + np.zeros((n, 2, 2)), + colors=["0.75", "0.00"] * n, + linewidths=[0.75, 1.00] * n, + linestyles=["--", "-"] * n, + ) + # Body + body = Circle( + self.position, + self.size, + zorder=20, + edgecolor="black", + facecolor=(1, 1, 1, 0.75), + ) + + # Head + P = np.zeros((1, 2, 2)) + P[0, 0] = self.position + P[0, 1] = P[0, 1] + self.size * np.array( + [np.cos(self.orientation), np.sin(self.orientation)] + ) + head = LineCollection(P, colors="black", zorder=30) + + # List of artists to be rendered (sensors, body, head) + self.artists = [sensors, body, head] + ax.add_collection(sensors) + ax.add_artist(body) + ax.add_artist(head) + + def update(self, maze): + + sensors, body, head = self.artists + + # Sensors + verts = sensors.get_segments() + linewidths = sensors.get_linewidth() + A = self.sensors["angle"] + self.orientation + T = np.stack([np.cos(A), np.sin(A)], axis=1) + P1 = self.position + self.size * T + P2 = P1 + self.sensors["range"] * T + P3, P4 = maze.walls[:, 0], maze.walls[:, 1] + for i, (p1, p2) in enumerate(zip(P1, P2)): + verts[2 * i] = verts[2 * i + 1] = p1, p2 + linewidths[2 * i + 1] = 1 + C = line_intersect(p1, p2, P3, P4) + index = np.argmin(((C - p1) ** 2).sum(axis=1)) + p = C[index] + if p[0] < np.inf: + verts[2 * i + 1] = p1, p + self.sensors["value"][i] = np.sqrt(((p1 - p) ** 2).sum()) + self.sensors["value"][i] /= self.sensors["range"][i] + else: + self.sensors["value"][i] = 1 + sensors.set_verts(verts) + sensors.set_linewidths(linewidths) + + # Body + body.set_center(self.position) + + # Head + P = np.zeros((1, 2, 2)) + P[0, 0] = self.position + P[0, 1] = P[0, 0] + self.size * np.array( + [np.cos(self.orientation), np.sin(self.orientation)] + ) + head.set_verts(P) + + +# ------------------------------------------------------------------------------ +if __name__ == "__main__": + from matplotlib.gridspec import GridSpec + import matplotlib.animation as animation + + maze = Maze() + bot = Bot() + bot.position = 150, 250 + bot.orientation = 0 + bot.sensors["range"][3:5] *= 1.25 + + fig = plt.figure(figsize=(10, 5), frameon=False) + G = GridSpec(8, 2, width_ratios=(1, 2)) + ax = plt.subplot(G[:, 0], aspect=1, frameon=False) + + # Maze + bot + maze.draw(ax, grid=True, margin=15) + bot.draw(ax) + + # Sensor plots + plots = [] + P = np.zeros((500, 2)) + (trace,) = ax.plot([], [], color="0.5", zorder=10, linewidth=1, linestyle=":") + for i in range(bot.n_sensors): + ax = plt.subplot(G[i, 1]) + ax.set_xticks([]), ax.set_yticks([]) + ax.set_ylabel("Sensor %d" % (i + 1), fontsize="x-small") + (plot,) = ax.plot([], [], linewidth=0.75) + ax.set_xlim(0, 500) + ax.set_ylim(0, 1.1) + plots.append(plot) + X = np.arange(500) + Y = np.zeros((bot.n_sensors, 500)) + + def update(frame): + + # Update bot position according to sensors + dv = (bot.sensors["value"].ravel() * [-4.0, -3, -2, -1, 1, 2, 3, 4]).sum() + if abs(dv) > 0.5: + bot.orientation += 0.01 * dv + bot.position += 2 * np.array([np.cos(bot.orientation), np.sin(bot.orientation)]) + bot.update(maze) + + # Alternate wall moving to bias bot behavior + if bot.position[1] < 100: + maze.walls[12:] = [[(0, 250), (100, 300)], [(200, 200), (300, 250)]] + elif bot.position[1] > 400: + maze.walls[12:] = [[(0, 250), (100, 200)], [(200, 300), (300, 250)]] + + # Sensor plots + n = len(bot.sensors["angle"]) + if frame < 500: + P[frame] = bot.position + trace.set_xdata(P[:frame, 0]) + trace.set_ydata(P[:frame, 1]) + for i in range(n): + Y[i, frame] = bot.sensors["value"][i] + plots[i].set_ydata(Y[i, :frame]) + plots[i].set_xdata(X[:frame]) + else: + P[:-1] = P[1:] + P[-1] = bot.position + trace.set_xdata(P[:, 0]) + trace.set_ydata(P[:, 1]) + Y[:, 0:-1] = Y[:, 1:] + for i in range(n): + Y[i, -1] = bot.sensors["value"][i] + plots[i].set_ydata(Y[i]) + plots[i].set_xdata(X) + + return bot.artists, trace, plots + + anim = animation.FuncAnimation(fig, update, frames=100_000, interval=10) + plt.tight_layout() + + # Animation saving + if 0: + fig.set_frameon(True) + anim = animation.FuncAnimation(fig, update, frames=1000, interval=10) + import tqdm + + writer = animation.FFMpegWriter(fps=30) + print("Encoding animation (maze.mp4") + bar = tqdm.tqdm(total=1000) + anim.save( + "maze.mp4", + writer=writer, + dpi=100, + progress_callback=lambda i, n: bar.update(1), + ) + bar.close() + fig.set_frameon(False) + + plt.show() diff --git a/code/colors/alpha-scatter.py b/code/colors/alpha-scatter.py new file mode 100644 index 0000000..66dd778 --- /dev/null +++ b/code/colors/alpha-scatter.py @@ -0,0 +1,64 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.backends.backend_agg import FigureCanvas +from scipy.ndimage import gaussian_filter + +# Initialize random generator +np.random.seed(123) + +# Create a figure that pyplot does not know about. +fig = Figure(figsize=(10, 10)) +# attach a non-interactive Agg canvas to the figure +# (as a side-effect of the ``__init__``) +canvas = FigureCanvas(fig) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) + +n = 50_000 +X = np.random.normal(0, 1, n) +Y = np.random.normal(0, 1, n) +ax.scatter(X, Y, s=75, facecolors="C0", alpha=0.10, linewidth=0) +ax.set_xlim(-2, 2), ax.set_ylim(-2, 2) + +# ax.plot([1, 2, 3]) +canvas.draw() +I = np.array(canvas.renderer.buffer_rgba())[..., 0] +I = 1.0 - I / I.max() + +# now display the array X as an Axes in a new figure +fig = plt.figure(figsize=(12, 4.5)) + +ax = plt.subplot(1, 3, 1, aspect=1) +ax.scatter(X, Y, s=10, facecolors="black", alpha=0.10, linewidth=0, rasterized=True) +ax.set_xlim(-2, 2), ax.set_ylim(-2, 2) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_title("Scatter plot (n=50,000)") + +ax = plt.subplot(1, 3, 2) +ax.imshow(I, cmap="gray_r", extent=[-2, 2, -2, 2]) +ax.set_xlim(-2, 2), ax.set_ylim(-2, 2) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_title("Scatter/Image plot (n=50,000)") + +ax = plt.subplot(1, 3, 3) +image = ax.imshow(gaussian_filter(I, 25), cmap="gray_r", extent=[-2, 2, -2, 2]) +ax.contour( + gaussian_filter(I, 25), + extent=[-2, 2, -2, 2], + linestyles="--", + linewidths=1.0, + colors="white", + alpha=0.5, +) +ax.set_xlim(-2, 2), ax.set_ylim(-2, 2) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_title("Density plot") + +plt.tight_layout() +plt.savefig("../../figures/colors/alpha-scatter.pdf", dpi=600) +plt.show() diff --git a/code/colors/alpha-vs-color.py b/code/colors/alpha-vs-color.py new file mode 100644 index 0000000..d85f495 --- /dev/null +++ b/code/colors/alpha-vs-color.py @@ -0,0 +1,219 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +# Open-color colors from https://yeun.github.io/open-color/ +colors = { + "gray": { + 0: "#f8f9fa", + 1: "#f1f3f5", + 2: "#e9ecef", + 3: "#dee2e6", + 4: "#ced4da", + 5: "#adb5bd", + 6: "#868e96", + 7: "#495057", + 8: "#343a40", + 9: "#212529", + }, + "red": { + 0: "#fff5f5", + 1: "#ffe3e3", + 2: "#ffc9c9", + 3: "#ffa8a8", + 4: "#ff8787", + 5: "#ff6b6b", + 6: "#fa5252", + 7: "#f03e3e", + 8: "#e03131", + 9: "#c92a2a", + }, + "pink": { + 0: "#fff0f6", + 1: "#ffdeeb", + 2: "#fcc2d7", + 3: "#faa2c1", + 4: "#f783ac", + 5: "#f06595", + 6: "#e64980", + 7: "#d6336c", + 8: "#c2255c", + 9: "#a61e4d", + }, + "grape": { + 0: "#f8f0fc", + 1: "#f3d9fa", + 2: "#eebefa", + 3: "#e599f7", + 4: "#da77f2", + 5: "#cc5de8", + 6: "#be4bdb", + 7: "#ae3ec9", + 8: "#9c36b5", + 9: "#862e9c", + }, + "violet": { + 0: "#f3f0ff", + 1: "#e5dbff", + 2: "#d0bfff", + 3: "#b197fc", + 4: "#9775fa", + 5: "#845ef7", + 6: "#7950f2", + 7: "#7048e8", + 8: "#6741d9", + 9: "#5f3dc4", + }, + "indigo": { + 0: "#edf2ff", + 1: "#dbe4ff", + 2: "#bac8ff", + 3: "#91a7ff", + 4: "#748ffc", + 5: "#5c7cfa", + 6: "#4c6ef5", + 7: "#4263eb", + 8: "#3b5bdb", + 9: "#364fc7", + }, + "blue": { + 0: "#e7f5ff", + 1: "#d0ebff", + 2: "#a5d8ff", + 3: "#74c0fc", + 4: "#4dabf7", + 5: "#339af0", + 6: "#228be6", + 7: "#1c7ed6", + 8: "#1971c2", + 9: "#1864ab", + }, + "cyan": { + 0: "#e3fafc", + 1: "#c5f6fa", + 2: "#99e9f2", + 3: "#66d9e8", + 4: "#3bc9db", + 5: "#22b8cf", + 6: "#15aabf", + 7: "#1098ad", + 8: "#0c8599", + 9: "#0b7285", + }, + "teal": { + 0: "#e6fcf5", + 1: "#c3fae8", + 2: "#96f2d7", + 3: "#63e6be", + 4: "#38d9a9", + 5: "#20c997", + 6: "#12b886", + 7: "#0ca678", + 8: "#099268", + 9: "#087f5b", + }, + "green": { + 0: "#ebfbee", + 1: "#d3f9d8", + 2: "#b2f2bb", + 3: "#8ce99a", + 4: "#69db7c", + 5: "#51cf66", + 6: "#40c057", + 7: "#37b24d", + 8: "#2f9e44", + 9: "#2b8a3e", + }, + "lime": { + 0: "#f4fce3", + 1: "#e9fac8", + 2: "#d8f5a2", + 3: "#c0eb75", + 4: "#a9e34b", + 5: "#94d82d", + 6: "#82c91e", + 7: "#74b816", + 8: "#66a80f", + 9: "#5c940d", + }, + "yellow": { + 0: "#fff9db", + 1: "#fff3bf", + 2: "#ffec99", + 3: "#ffe066", + 4: "#ffd43b", + 5: "#fcc419", + 6: "#fab005", + 7: "#f59f00", + 8: "#f08c00", + 9: "#e67700", + }, + "orange": { + 0: "#fff4e6", + 1: "#ffe8cc", + 2: "#ffd8a8", + 3: "#ffc078", + 4: "#ffa94d", + 5: "#ff922b", + 6: "#fd7e14", + 7: "#f76707", + 8: "#e8590c", + 9: "#d9480f", + }, +} + + +# Setup +fig = plt.figure(figsize=(6, 2)) + +X = np.linspace(0, 2 * np.pi, 256) +Y1 = np.zeros((50, len(X))) +Y2 = np.zeros((len(Y1), len(X))) +noise = 0.5 +for i in range(len(Y1)): + Y1[i] = np.cos(X) + noise * np.random.uniform(-1, 1, len(X)) + Y2[i] = np.sin(X) + noise * np.random.uniform(-1, 1, len(X)) + + +# Usage of transparency +ax = plt.subplot(1, 2, 1) + +Y, SD = Y1.mean(axis=0), Y1.std(axis=0) +ax.fill_between(X, Y + SD, Y - SD, facecolor="C0", alpha=0.25, zorder=-40) +ax.plot(X, Y, color="C0", zorder=-30) + +Y, SD = Y2.mean(axis=0), Y2.std(axis=0) +ax.fill_between(X, Y + SD, Y - SD, facecolor="C1", alpha=0.25, zorder=-20) +ax.plot(X, Y, color="C1", zorder=-10) + +ax.set_xticks([]), ax.set_yticks([]) + + +# Usage of explicite colors +ax = plt.subplot(1, 2, 2) + +Y, SD = Y1.mean(axis=0), Y1.std(axis=0) +ax.fill_between(X, Y + SD, Y - SD, facecolor=colors["blue"][1], zorder=-40) +ax.plot(X, Y, color=colors["blue"][7], zorder=-30) + +Y, SD = Y2.mean(axis=0), Y2.std(axis=0) +ax.fill_between( + X, + Y + SD, + Y - SD, + facecolor=colors["orange"][1], + edgecolor="white", + linewidth=0.75, + zorder=-20, +) +ax.plot(X, Y, color=colors["orange"][7], zorder=-10) + +ax.set_xticks([]), ax.set_yticks([]) + +plt.tight_layout() +plt.savefig("../../figures/colors/alpha-vs-color.pdf") +plt.show() diff --git a/code/colors/color-gradients.py b/code/colors/color-gradients.py new file mode 100644 index 0000000..56ff512 --- /dev/null +++ b/code/colors/color-gradients.py @@ -0,0 +1,129 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate the different colorspaces (sRGB, RGB and Lab) +# ---------------------------------------------------------------------------- +import math +import numpy as np +from skimage.color import rgb2lab, lab2rgb, rgb2xyz, xyz2rgb +import matplotlib.pyplot as plt + +plt.rc("font", family="Roboto") + + +def sRGB_to_Lab(color): + color = np.asarray(color, dtype=float) + shape = color.shape + if len(shape) == 1: + color = color.reshape(1, 1, shape[0]) + elif len(shape) == 2: + color = color.reshape(1, shape[0], shape[1]) + return rgb2lab(color) + + +def Lab_to_sRGB(color): + color = np.asarray(color, dtype=float) + shape = color.shape + if len(shape) == 1: + color = color.reshape(1, 1, shape[0]) + elif len(shape) == 2: + color = color.reshape(1, shape[0], shape[1]) + return lab2rgb(color) + + +def sRGB_to_RGB(color): + color = np.asarray(color, dtype=float).reshape(-1, 3) + R, G, B = color[..., 0], color[..., 1], color[..., 2] + R = np.where(R > 0.04045, np.power((R + 0.055) / 1.055, 2.4), R / 12.92) + G = np.where(G > 0.04045, np.power((G + 0.055) / 1.055, 2.4), G / 12.92) + B = np.where(B > 0.04045, np.power((B + 0.055) / 1.055, 2.4), B / 12.92) + return np.c_[R, G, B] + + +def RGB_to_sRGB(color): + color = np.asarray(color, dtype=float).reshape(-1, 3) + R, G, B = color[..., 0], color[..., 1], color[..., 2] + R = np.where(R > 0.0031308, 1.055 * np.power(R, 1 / 2.4) - 0.055, R * 12.92) + G = np.where(G > 0.0031308, 1.055 * np.power(G, 1 / 2.4) - 0.055, G * 12.92) + B = np.where(B > 0.0031308, 1.055 * np.power(B, 1 / 2.4) - 0.055, B * 12.92) + return np.c_[R, G, B] + + +def gradient(color0, color1, mode="sRGB", n=256): + T = np.linspace(0, 1, n).reshape(n, 1) + if mode == "Lab": + C = (1 - T) * sRGB_to_Lab(color0) + T * sRGB_to_Lab(color1) + return Lab_to_sRGB(C) + elif mode == "RGB": + C = (1 - T) * sRGB_to_RGB(color0) + T * sRGB_to_RGB(color1) + return RGB_to_sRGB(C) + else: + return (1 - T) * color0 + T * color1 + + +def hex(color): + color = (np.asarray(color) * 255).astype(int) + r, g, b = color + return ("#%02x%02x%02x" % (r, g, b)).upper() + + +def plot(ax, color0, color1, yticks=True): + rows, cols = 16, 256 + Z = np.zeros((3, rows, cols, 3)) + Z[0] = gradient(color0, color1, "sRGB") + Z[2] = gradient(color0, color1, "RGB") + Z[1] = gradient(color0, color1, "Lab") + + ax.tick_params(axis="both", length=0, labelsize="xx-small") + ax.imshow(Z.reshape(3 * rows, cols, 3), extent=[0, cols, 0, 3 * rows]) + + if yticks: + ax.set_yticks([rows // 2, rows // 2 + rows, rows // 2 + 2 * rows]) + ax.set_yticklabels(["Lab", "RGB", "sRGB"]) + else: + ax.set_yticks([]) + ax.set_xticks([]) + plt.text(0, -2, hex(color0), ha="left", va="top", fontsize="xx-small") + plt.text(cols, -2, hex(color1), ha="right", va="top", fontsize="xx-small") + + +fig = plt.figure(figsize=(4.25, 3.5)) +plt.rcParams["axes.linewidth"] = 0.5 + +rows, cols = 6, 2 + +ax = plt.subplot(rows, cols, 1) +plot(ax, (1, 1, 1), (1, 0, 0)) +ax = plt.subplot(rows, cols, 2) +plot(ax, (1, 0, 0), (0, 0, 0), False) + +ax = plt.subplot(rows, cols, 3) +plot(ax, (1, 1, 1), (0, 1, 0)) +ax = plt.subplot(rows, cols, 4) +plot(ax, (0, 1, 0), (0, 0, 0), False) + +ax = plt.subplot(rows, cols, 5) +plot(ax, (1, 1, 1), (0, 0, 1)) +ax = plt.subplot(rows, cols, 6) +plot(ax, (0, 0, 1), (0, 0, 0), False) + +ax = plt.subplot(rows, cols, 7) +plot(ax, (1, 0, 0), (0, 1, 0)) +ax = plt.subplot(rows, cols, 8) +plot(ax, (0, 1, 0), (0, 0, 1), False) + +ax = plt.subplot(rows, cols, 9) +plot(ax, (1, 0, 1), (1, 1, 0)) +ax = plt.subplot(rows, cols, 10) +plot(ax, (1, 1, 0), (0, 1, 1), False) + +ax = plt.subplot(rows, cols, 11) +plot(ax, (1, 1, 1), (0, 0, 0)) +ax = plt.subplot(rows, cols, 12) +plot(ax, (0, 0, 0), (1, 1, 1), False) + +plt.tight_layout() +plt.savefig("../../figures/colors/color-gradients.pdf", dpi=600) +plt.show() diff --git a/code/colors/color-wheel.py b/code/colors/color-wheel.py new file mode 100644 index 0000000..48c6195 --- /dev/null +++ b/code/colors/color-wheel.py @@ -0,0 +1,222 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Color wheel in HSV +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as colors +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch +from matplotlib.collections import QuadMesh +from matplotlib.font_manager import FontProperties + +# Curved label (polar coordinates) +def polar_text(text, angle, radius=1, scale=0.005, family="sans"): + prop = FontProperties(family=family, weight="regular") + path = TextPath((0, 0), text, size=1, prop=prop) + V = path.vertices + xmin, xmax = V[:, 0].min(), V[:, 0].max() + V[:, 0] = angle - (V[:, 0] - (xmin + xmax) / 2) * scale + V[:, 1] = radius + V[:, 1] * scale + patch = PathPatch(path, facecolor="black", linewidth=0, clip_on=False) + ax.add_artist(patch) + + +# Polar imshow using quadmesh +def polar_imshow( + ax, Z, extents=[0, 1, 0, 2 * np.pi], vmin=None, vmax=None, cmap="viridis" +): + + Z = np.atleast_3d(Z) + nr, nt, d = Z.shape + rmin, rmax, tmin, tmax = extents + + if d == 1: + cmap = plt.get_cmap(cmap) + vmin = vmin or Z.min() + vmax = vmax or Z.max() + norm = colors.Normalize(vmin=vmin, vmax=vmax) + facecolors = cmap(norm(Z)) + else: + facecolors = Z.reshape(nr, nt, 3).reshape(-1, 3) + + R = np.linspace(rmin, rmax, nr + 1) + T = np.linspace(tmin, tmax, nt + 1) + T, R = np.meshgrid(T, R) + nr, nt = R.shape + R, T = R.ravel(), T.ravel() + coords = np.column_stack((T, R)) + collection = QuadMesh( + nt - 1, + nr - 1, + coords, + rasterized=True, + facecolors=facecolors, + edgecolors="None", + linewidth=0, + antialiased=False, + ) + ax.add_collection(collection) + return collection + + +radius = 1.05 +scale = 0.085 +family = "Yanone Kaffeesatz" + +fig = plt.figure(figsize=(8, 4)) + +n = 50 +R = np.linspace(0, 1, 2 * n) +T = np.linspace(0, 1, 10 * n) +T, R = np.meshgrid(T, R) +H, S, V = T, R, np.ones_like(T) + +# ----------------------------------------------------------------------------- +ax = fig.add_subplot(2, 4, 1, polar=True, frameon=True) +Z = colors.hsv_to_rgb(np.dstack([H, S, V])) +ax.set_xticks([]), ax.set_yticks([]) +polar_text( + "<––– hue –––", family=family, angle=np.pi / 4, radius=radius, scale=2 * scale +) +ax.text( + 3 * np.pi / 4, + 1.5, + "value = 1.00", + family=family, + size=10, + bbox={ + "pad": 1.5, + "linewidth": 0.5, + "boxstyle": "round,pad=.2", + "edgecolor": "black", + "facecolor": "white", + }, +) + +ax.text(np.pi, 0.0, "––– saturation –––> ", size=8, family=family) +polar_imshow(ax, Z) + +# ----------------------------------------------------------------------------- +ax = fig.add_subplot(2, 4, 2, polar=True, frameon=True) +Z = colors.hsv_to_rgb(np.dstack([H, S, 0.75 * V])) +ax.set_xticks([]), ax.set_yticks([]) +polar_text( + "<––– hue –––", family=family, angle=np.pi / 4, radius=radius, scale=2 * scale +) +ax.text( + 3 * np.pi / 4, + 1.5, + "value = 0.75", + family=family, + size=10, + bbox={ + "pad": 1.5, + "linewidth": 0.5, + "boxstyle": "round,pad=.2", + "edgecolor": "None", + "facecolor": "0.75", + }, +) +ax.text(np.pi, 0.0, "––– saturation –––> ", size=8, family=family) +polar_imshow(ax, Z) + +# ----------------------------------------------------------------------------- +ax = fig.add_subplot(2, 4, 5, polar=True, frameon=True) +Z = colors.hsv_to_rgb(np.dstack([H, S, 0.5 * V])) +ax.set_xticks([]), ax.set_yticks([]) +polar_text( + "<––– hue –––", family=family, angle=np.pi / 4, radius=radius, scale=2 * scale +) +ax.text( + 3 * np.pi / 4, + 1.5, + "value = 0.50", + family=family, + size=10, + color="white", + bbox={ + "pad": 1.5, + "linewidth": 0.5, + "boxstyle": "round,pad=.2", + "edgecolor": "None", + "facecolor": "0.5", + }, +) +ax.text(np.pi, 0.0, "––– saturation –––> ", size=8, family=family, color="1.0") +polar_imshow(ax, Z) + +# ----------------------------------------------------------------------------- +ax = fig.add_subplot(2, 4, 6, polar=True, frameon=True) +Z = colors.hsv_to_rgb(np.dstack([H, S, 0.25 * V])) +ax.set_xticks([]), ax.set_yticks([]) +polar_text( + "<––– hue –––", family=family, angle=np.pi / 4, radius=radius, scale=2 * scale +) +ax.text( + 3 * np.pi / 4, + 1.5, + "value = 0.25", + family=family, + size=10, + color="white", + bbox={ + "pad": 1.5, + "linewidth": 0.5, + "boxstyle": "round,pad=.2", + "edgecolor": "None", + "facecolor": "0.25", + }, +) +ax.text(np.pi, 0.0, "-–– saturation –––> ", size=8, family=family, color="1.0") +polar_imshow(ax, Z) + +# ----------------------------------------------------------------------------- +ax = fig.add_subplot(1, 2, 2, polar=True, frameon=False) +n = 100 +R = np.linspace(0, 1, n) +R -= R % (1 / 5.99) +T = np.linspace(0, 1, 10 * n) +T -= T % (1 / 11.99) +T, R = np.meshgrid(T, R) +H, S, V = T, R, np.ones_like(T) +Z = colors.hsv_to_rgb(np.dstack([H, S, V])) +polar_imshow(ax, Z) +ax.set_xticks(np.linspace(0, 2 * np.pi, 13)) +ax.set_yticks(np.linspace(0, 1, 7)) +ax.set_yticklabels([]) +ax.set_xticklabels([]) +ax.grid(linewidth=1, color="white") +labels = [ + "red", + "orange", + "yellow", + "lime", + "green", + "turquoise", + "cyan", + "skyblue", + "blue", + "violet", + "purple", + "magenta", +] +for label, x in zip(labels, np.linspace(0.5, 12.5, 12)): + label = label.upper() + angle = x / 13 * 2 * np.pi + polar_text( + label.upper(), family=family, angle=angle, radius=radius, scale=1.25 * scale + ) +ax.set_theta_offset(-np.pi / 12) + +R = np.ones(100) +T = np.linspace(0, 2 * np.pi, 100) +ax.plot(T, R, color="white") + +plt.tight_layout() +plt.savefig("../../figures/colors/color-wheel.png", dpi=600) +plt.savefig("../../figures/colors/color-wheel.pdf", dpi=600) +plt.show() diff --git a/code/colors/colored-hist.py b/code/colors/colored-hist.py new file mode 100644 index 0000000..cf26f44 --- /dev/null +++ b/code/colors/colored-hist.py @@ -0,0 +1,296 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches + +material = { + "red": { + 0: "#ffebee", + 1: "#ffcdd2", + 2: "#ef9a9a", + 3: "#e57373", + 4: "#ef5350", + 5: "#f44336", + 6: "#e53935", + 7: "#d32f2f", + 8: "#c62828", + 9: "#b71c1c", + }, + "pink": { + 0: "#fce4ec", + 1: "#f8bbd0", + 2: "#f48fb1", + 3: "#f06292", + 4: "#ec407a", + 5: "#e91e63", + 6: "#d81b60", + 7: "#c2185b", + 8: "#ad1457", + 9: "#880e4f", + }, + "purple": { + 0: "#f3e5f5", + 1: "#e1bee7", + 2: "#ce93d8", + 3: "#ba68c8", + 4: "#ab47bc", + 5: "#9c27b0", + 6: "#8e24aa", + 7: "#7b1fa2", + 8: "#6a1b9a", + 9: "#4a148c", + }, + "deep purple": { + 0: "#ede7f6", + 1: "#d1c4e9", + 2: "#b39ddb", + 3: "#9575cd", + 4: "#7e57c2", + 5: "#673ab7", + 6: "#5e35b1", + 7: "#512da8", + 8: "#4527a0", + 9: "#311b92", + }, + "indigo": { + 0: "#e8eaf6", + 1: "#c5cae9", + 2: "#9fa8da", + 3: "#7986cb", + 4: "#5c6bc0", + 5: "#3f51b5", + 6: "#3949ab", + 7: "#303f9f", + 8: "#283593", + 9: "#1a237e", + }, + "blue": { + 0: "#e3f2fd", + 1: "#bbdefb", + 2: "#90caf9", + 3: "#64b5f6", + 4: "#42a5f5", + 5: "#2196f3", + 6: "#1e88e5", + 7: "#1976d2", + 8: "#1565c0", + 9: "#0d47a1", + }, + "light blue": { + 0: "#e1f5fe", + 1: "#b3e5fc", + 2: "#81d4fa", + 3: "#4fc3f7", + 4: "#29b6f6", + 5: "#03a9f4", + 6: "#039be5", + 7: "#0288d1", + 8: "#0277bd", + 9: "#01579b", + }, + "cyan": { + 0: "#e0f7fa", + 1: "#b2ebf2", + 2: "#80deea", + 3: "#4dd0e1", + 4: "#26c6da", + 5: "#00bcd4", + 6: "#00acc1", + 7: "#0097a7", + 8: "#00838f", + 9: "#006064", + }, + "teal": { + 0: "#e0f2f1", + 1: "#b2dfdb", + 2: "#80cbc4", + 3: "#4db6ac", + 4: "#26a69a", + 5: "#009688", + 6: "#00897b", + 7: "#00796b", + 8: "#00695c", + 9: "#004d40", + }, + "green": { + 0: "#e8f5e9", + 1: "#c8e6c9", + 2: "#a5d6a7", + 3: "#81c784", + 4: "#66bb6a", + 5: "#4caf50", + 6: "#43a047", + 7: "#388e3c", + 8: "#2e7d32", + 9: "#1b5e20", + }, + "light green": { + 0: "#f1f8e9", + 1: "#dcedc8", + 2: "#c5e1a5", + 3: "#aed581", + 4: "#9ccc65", + 5: "#8bc34a", + 6: "#7cb342", + 7: "#689f38", + 8: "#558b2f", + 9: "#33691e", + }, + "lime": { + 0: "#f9fbe7", + 1: "#f0f4c3", + 2: "#e6ee9c", + 3: "#dce775", + 4: "#d4e157", + 5: "#cddc39", + 6: "#c0ca33", + 7: "#afb42b", + 8: "#9e9d24", + 9: "#827717", + }, + "yellow": { + 0: "#fffde7", + 1: "#fff9c4", + 2: "#fff59d", + 3: "#fff176", + 4: "#ffee58", + 5: "#ffeb3b", + 6: "#fdd835", + 7: "#fbc02d", + 8: "#f9a825", + 9: "#f57f17", + }, + "amber": { + 0: "#fff8e1", + 1: "#ffecb3", + 2: "#ffe082", + 3: "#ffd54f", + 4: "#ffca28", + 5: "#ffc107", + 6: "#ffb300", + 7: "#ffa000", + 8: "#ff8f00", + 9: "#ff6f00", + }, + "orange": { + 0: "#fff3e0", + 1: "#ffe0b2", + 2: "#ffcc80", + 3: "#ffb74d", + 4: "#ffa726", + 5: "#ff9800", + 6: "#fb8c00", + 7: "#f57c00", + 8: "#ef6c00", + 9: "#e65100", + }, + "deep orange": { + 0: "#fbe9e7", + 1: "#ffccbc", + 2: "#ffab91", + 3: "#ff8a65", + 4: "#ff7043", + 5: "#ff5722", + 6: "#f4511e", + 7: "#e64a19", + 8: "#d84315", + 9: "#bf360c", + }, + "brown": { + 0: "#efebe9", + 1: "#d7ccc8", + 2: "#bcaaa4", + 3: "#a1887f", + 4: "#8d6e63", + 5: "#795548", + 6: "#6d4c41", + 7: "#5d4037", + 8: "#4e342e", + 9: "#3e2723", + }, + "grey": { + 0: "#fafafa", + 1: "#f5f5f5", + 2: "#eeeeee", + 3: "#e0e0e0", + 4: "#bdbdbd", + 5: "#9e9e9e", + 6: "#757575", + 7: "#616161", + 8: "#424242", + 9: "#212121", + }, + "blue grey": { + 0: "#eceff1", + 1: "#cfd8dc", + 2: "#b0bec5", + 3: "#90a4ae", + 4: "#78909c", + 5: "#607d8b", + 6: "#546e7a", + 7: "#455a64", + 8: "#37474f", + 9: "#263238", + }, +} + + +np.random.seed(123) +plt.figure(figsize=(8, 4)) +ax = plt.subplot(1, 1, 1, frameon=False) + + +def bars(origin, color, n, index): + X = origin + np.arange(n) + H = 20 * np.random.uniform(1.0, 5.0, n) + H.sort() + + ax.bar( + X, + H, + width=1.0, + align="edge", + color=[material[color][1 + 2 * i] for i in range(n)], + ) + ax.plot([origin - 0.5, origin + n + 0.5], [0, 0], color="black", lw=2.5) + ax.text(origin + n / 2, -1, "Group " + index, va="top", ha="center") + + +ax.axhline(50, 0.05, 1, color="0.5", linewidth=0.5, linestyle="--", zorder=-10) +ax.axhline(100, 0.05, 1, color="0.5", linewidth=0.5, linestyle="--", zorder=-10) + +n = 3 +bars(0 * (n + 2), "red", n, "A") +bars(1 * (n + 2), "indigo", n, "B") +bars(2 * (n + 2), "orange", n, "C") +bars(3 * (n + 2), "teal", n, "D") +bars(4 * (n + 2), "pink", n, "E") +bars(5 * (n + 2), "cyan", n, "F") + +ax.set_xlim(-2, 6 * (n + 2) - 1.5) +ax.set_xticks([]) +ax.set_ylim(0, 111) +ax.set_yticks([0, 50, 100]) +ax.set_yticklabels(["0%", "50%", "100%"]) + + +plt.legend( + # bbox_to_anchor=(0.0, 1.0, 1.0, 0.1), + loc="upper right", + borderaxespad=0.0, + ncol=3, + handles=[ + mpatches.Patch(color=material["blue grey"][1], label="2018"), + mpatches.Patch(color=material["blue grey"][3], label="2019"), + mpatches.Patch(color=material["blue grey"][5], label="2020"), + ], +) + + +plt.tight_layout() +plt.savefig("../../figures/colors/colored-hist.pdf") +plt.show() diff --git a/code/colors/colored-plot.py b/code/colors/colored-plot.py new file mode 100644 index 0000000..55f3099 --- /dev/null +++ b/code/colors/colored-plot.py @@ -0,0 +1,33 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate colored plots +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import LineCollection + + +def plot(ax, X, Y, cmap, alpha): + P = np.array([X, Y]).T.reshape(-1, 1, 2) + S = np.concatenate([P[:-1], P[1:]], axis=1) + C = cmap(np.linspace(0, 1, len(S))) + L = LineCollection(S, color=C, alpha=alpha, linewidth=1.25) + ax.add_collection(L) + + +fig = plt.figure(figsize=(12, 3)) +fig.patch.set_facecolor("black") +ax = fig.add_axes([0, 0, 1, 1], frameon=False) +X = np.linspace(-5 * np.pi, +5 * np.pi, 2500) +for d in np.linspace(0, 1, 15): + dy = d / 2 + (1 - np.abs(X) / X.max()) ** 2 + dx = 1 + d / 3 + Y = dy * np.sin(dx * X) + 0.1 * np.cos(3 + 5 * X) + plot(ax, X, Y, plt.get_cmap("rainbow"), d) +ax.set_xlim(X.min(), X.max()) +ax.set_ylim(-2.0, 2.0) +plt.savefig("../../figures/colors/colored-plot.pdf") +plt.show() diff --git a/code/colors/flower-polar.py b/code/colors/flower-polar.py new file mode 100644 index 0000000..3ecd57d --- /dev/null +++ b/code/colors/flower-polar.py @@ -0,0 +1,124 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Flower polar / flower power (poly collection and polar projection) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as colors +from matplotlib.collections import PolyCollection + + +def flower(ax, n_branches=24, n_sections=4, lw=1): + R = np.linspace(0.1, 1.0, 25) + paths = [] + facecolors = [] + n_sections += 1 + + for i in range(n_branches): + for j in range(n_sections - 1): + R_, T_ = [], [] + T = np.linspace( + i * 2 * np.pi / n_branches, + (i + n_sections / 2) * 2 * np.pi / n_branches, + len(R), + ) + t = np.interp( + np.linspace(j + 1, j + 2, 20), np.linspace(0, n_sections, len(T)), T + ) + r = np.interp( + np.linspace(j + 1, j + 2, 20), np.linspace(0, n_sections, len(R)), R + ) + R_.extend(r[::1].tolist()) + T_.extend(t[::1].tolist()) + + T = np.linspace( + (i + 1 + j + 1) * 2 * np.pi / n_branches, + (i + 1 + j + 1 - n_sections / 2) * 2 * np.pi / n_branches, + len(R), + ) + t = np.interp( + np.linspace(j + 1, j + 2, 20), np.linspace(0, n_sections, len(T)), T + ) + r = np.interp( + np.linspace(j + 1, j + 2, 20), np.linspace(0, n_sections, len(R)), R + ) + R_.extend(r[::-1].tolist()) + T_.extend(t[::-1].tolist()) + + T = np.linspace( + (i + 1) * 2 * np.pi / n_branches, + (i + 1 + n_sections / 2) * 2 * np.pi / n_branches, + len(R), + ) + t = np.interp( + np.linspace(j, j + 1, 20), np.linspace(0, n_sections, len(T)), T + ) + r = np.interp( + np.linspace(j, j + 1, 20), np.linspace(0, n_sections, len(R)), R + ) + R_.extend(r[::-1].tolist()) + T_.extend(t[::-1].tolist()) + + T = np.linspace( + (i + 1 + j) * 2 * np.pi / n_branches, + (i + 1 + j - n_sections / 2) * 2 * np.pi / n_branches, + len(R), + ) + t = np.interp( + np.linspace(j, j + 1, 20), np.linspace(0, n_sections, len(T)), T + ) + r = np.interp( + np.linspace(j, j + 1, 20), np.linspace(0, n_sections, len(R)), R + ) + R_.extend(r[::1].tolist()) + T_.extend(t[::1].tolist()) + + P = np.dstack([T_, R_]).squeeze() + paths.append(P) + h = i / n_branches + s = 0.5 + 0.5 * j / (n_sections - 1) + v = 1.00 + facecolors.append(colors.hsv_to_rgb([h, s, v])) + + collection = PolyCollection( + paths, linewidths=5.5 * lw, facecolors="None", edgecolors="black" + ) + ax.add_collection(collection) + + collection = PolyCollection( + paths, linewidths=4 * lw, facecolors="None", edgecolors="white" + ) + ax.add_collection(collection) + + ax.fill_between(np.linspace(0, 2 * np.pi, 100), 0.0, 0.5, facecolor="white") + + collection = PolyCollection( + paths, linewidths=lw, facecolors=facecolors, edgecolors="white" + ) + ax.add_collection(collection) + + +fig = plt.figure(figsize=(12, 4)) + +ax = fig.add_subplot(1, 3, 1, polar=True, frameon=False) +flower(ax, 6, 3, lw=2.0) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_rlim(0, 1.1) + +ax = fig.add_subplot(1, 3, 2, polar=True, frameon=False) +flower(ax, 12, 4, lw=1.5) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_rlim(0, 1.1) + +ax = fig.add_subplot(1, 3, 3, polar=True, frameon=False) +flower(ax, 24, 5, lw=1.0) +ax.set_xticks([]), ax.set_yticks([]) +ax.set_rlim(0, 1.1) + +plt.tight_layout() +plt.savefig("../../figures/colors/flower-polar.pdf", dpi=600) +# plt.savefig("../../figures/colors/flower-polar.png", dpi=300) +plt.show() diff --git a/code/colors/material-colors.py b/code/colors/material-colors.py new file mode 100644 index 0000000..3306276 --- /dev/null +++ b/code/colors/material-colors.py @@ -0,0 +1,294 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatch +from matplotlib.patches import FancyBboxPatch + +colors = { + "red": { + 0: "#ffebee", + 1: "#ffcdd2", + 2: "#ef9a9a", + 3: "#e57373", + 4: "#ef5350", + 5: "#f44336", + 6: "#e53935", + 7: "#d32f2f", + 8: "#c62828", + 9: "#b71c1c", + }, + "pink": { + 0: "#fce4ec", + 1: "#f8bbd0", + 2: "#f48fb1", + 3: "#f06292", + 4: "#ec407a", + 5: "#e91e63", + 6: "#d81b60", + 7: "#c2185b", + 8: "#ad1457", + 9: "#880e4f", + }, + "purple": { + 0: "#f3e5f5", + 1: "#e1bee7", + 2: "#ce93d8", + 3: "#ba68c8", + 4: "#ab47bc", + 5: "#9c27b0", + 6: "#8e24aa", + 7: "#7b1fa2", + 8: "#6a1b9a", + 9: "#4a148c", + }, + "d.purple": { + 0: "#ede7f6", + 1: "#d1c4e9", + 2: "#b39ddb", + 3: "#9575cd", + 4: "#7e57c2", + 5: "#673ab7", + 6: "#5e35b1", + 7: "#512da8", + 8: "#4527a0", + 9: "#311b92", + }, + "indigo": { + 0: "#e8eaf6", + 1: "#c5cae9", + 2: "#9fa8da", + 3: "#7986cb", + 4: "#5c6bc0", + 5: "#3f51b5", + 6: "#3949ab", + 7: "#303f9f", + 8: "#283593", + 9: "#1a237e", + }, + "blue": { + 0: "#e3f2fd", + 1: "#bbdefb", + 2: "#90caf9", + 3: "#64b5f6", + 4: "#42a5f5", + 5: "#2196f3", + 6: "#1e88e5", + 7: "#1976d2", + 8: "#1565c0", + 9: "#0d47a1", + }, + "l.blue": { + 0: "#e1f5fe", + 1: "#b3e5fc", + 2: "#81d4fa", + 3: "#4fc3f7", + 4: "#29b6f6", + 5: "#03a9f4", + 6: "#039be5", + 7: "#0288d1", + 8: "#0277bd", + 9: "#01579b", + }, + "cyan": { + 0: "#e0f7fa", + 1: "#b2ebf2", + 2: "#80deea", + 3: "#4dd0e1", + 4: "#26c6da", + 5: "#00bcd4", + 6: "#00acc1", + 7: "#0097a7", + 8: "#00838f", + 9: "#006064", + }, + "teal": { + 0: "#e0f2f1", + 1: "#b2dfdb", + 2: "#80cbc4", + 3: "#4db6ac", + 4: "#26a69a", + 5: "#009688", + 6: "#00897b", + 7: "#00796b", + 8: "#00695c", + 9: "#004d40", + }, + "green": { + 0: "#e8f5e9", + 1: "#c8e6c9", + 2: "#a5d6a7", + 3: "#81c784", + 4: "#66bb6a", + 5: "#4caf50", + 6: "#43a047", + 7: "#388e3c", + 8: "#2e7d32", + 9: "#1b5e20", + }, + "l.green": { + 0: "#f1f8e9", + 1: "#dcedc8", + 2: "#c5e1a5", + 3: "#aed581", + 4: "#9ccc65", + 5: "#8bc34a", + 6: "#7cb342", + 7: "#689f38", + 8: "#558b2f", + 9: "#33691e", + }, + "lime": { + 0: "#f9fbe7", + 1: "#f0f4c3", + 2: "#e6ee9c", + 3: "#dce775", + 4: "#d4e157", + 5: "#cddc39", + 6: "#c0ca33", + 7: "#afb42b", + 8: "#9e9d24", + 9: "#827717", + }, + "yellow": { + 0: "#fffde7", + 1: "#fff9c4", + 2: "#fff59d", + 3: "#fff176", + 4: "#ffee58", + 5: "#ffeb3b", + 6: "#fdd835", + 7: "#fbc02d", + 8: "#f9a825", + 9: "#f57f17", + }, + "amber": { + 0: "#fff8e1", + 1: "#ffecb3", + 2: "#ffe082", + 3: "#ffd54f", + 4: "#ffca28", + 5: "#ffc107", + 6: "#ffb300", + 7: "#ffa000", + 8: "#ff8f00", + 9: "#ff6f00", + }, + "orange": { + 0: "#fff3e0", + 1: "#ffe0b2", + 2: "#ffcc80", + 3: "#ffb74d", + 4: "#ffa726", + 5: "#ff9800", + 6: "#fb8c00", + 7: "#f57c00", + 8: "#ef6c00", + 9: "#e65100", + }, + "d.orange": { + 0: "#fbe9e7", + 1: "#ffccbc", + 2: "#ffab91", + 3: "#ff8a65", + 4: "#ff7043", + 5: "#ff5722", + 6: "#f4511e", + 7: "#e64a19", + 8: "#d84315", + 9: "#bf360c", + }, + "brown": { + 0: "#efebe9", + 1: "#d7ccc8", + 2: "#bcaaa4", + 3: "#a1887f", + 4: "#8d6e63", + 5: "#795548", + 6: "#6d4c41", + 7: "#5d4037", + 8: "#4e342e", + 9: "#3e2723", + }, + "grey": { + 0: "#fafafa", + 1: "#f5f5f5", + 2: "#eeeeee", + 3: "#e0e0e0", + 4: "#bdbdbd", + 5: "#9e9e9e", + 6: "#757575", + 7: "#616161", + 8: "#424242", + 9: "#212121", + }, + "blue grey": { + 0: "#eceff1", + 1: "#cfd8dc", + 2: "#b0bec5", + 3: "#90a4ae", + 4: "#78909c", + 5: "#607d8b", + 6: "#546e7a", + 7: "#455a64", + 8: "#37474f", + 9: "#263238", + }, +} + +nx, dx = 10, 5 +ny, dy = 19, 5 + +fig = plt.figure(figsize=(8, 10), dpi=100) +ax = plt.subplot( + 1, + 1, + 1, + frameon=False, + xlim=(-1, nx * dx), + ylim=(-1, (ny - 1) * dy + 2), + xticks=[], + yticks=[], +) + + +for palette, y in zip(colors.keys(), range(0, ny * dy, dy)): + for level, x in zip(range(10), range(0, 10 * dx, dx)): + fancy = FancyBboxPatch( + (x, y), + dx - 1, + 0.3 * dy, + facecolor=colors[palette][level], + edgecolor="black", + linewidth=0, + ) + ax.add_patch(fancy) + + name = "%s %s" % (palette.upper(), level) + ax.text( + x, + y - 0.15 * dy, + name, + size=8, + family="Roboto", + weight="regular", + ha="left", + va="top", + ) + + value = colors[palette][level].upper() + ax.text( + x, + y - 0.375 * dy, + value, + size=7.5, + color="0.5", + family="Roboto", + weight="regular", + ha="left", + va="top", + ) + + +plt.tight_layout() +plt.savefig("../../figures/colors/material-colors.pdf") +plt.savefig("../../figures/colors/material-colors.png", dpi=300) +plt.show() diff --git a/code/colors/mona-lisa.py b/code/colors/mona-lisa.py new file mode 100644 index 0000000..6b8ef75 --- /dev/null +++ b/code/colors/mona-lisa.py @@ -0,0 +1,57 @@ +import imageio +import numpy as np +import matplotlib.pyplot as plt + +I = imageio.imread("../data/mona-lisa.png") + + +def plot(ax, cmap, name=None): + ax.imshow(I, cmap=plt.get_cmap(cmap), rasterized=True) + ax.text( + 0.5, + 0.025, + name or cmap, + transform=ax.transAxes, + color="white", + ha="center", + va="bottom", + size="large", + family="Roboto Slab", + ) + ax.set_xticks([]), ax.set_yticks([]) + + +rows, cols = 3, 4 +plt.figure(figsize=(9, 10.25)) + +ax = plt.subplot(rows, cols, 1, frameon=False) +plot(ax, "cividis", "Cividis") +ax = plt.subplot(rows, cols, 2, frameon=False) +plot(ax, "viridis", "Viridis") +ax = plt.subplot(rows, cols, 3, frameon=False) +plot(ax, "magma", "Magma") +ax = plt.subplot(rows, cols, 4, frameon=False) +plot(ax, "inferno", "Inferno") + +ax = plt.subplot(rows, cols, 5, frameon=False) +plot(ax, "bone") +ax = plt.subplot(rows, cols, 6, frameon=False) +plot(ax, "YlGn_r", "YlGn\n(Yellow Green)") +ax = plt.subplot(rows, cols, 7, frameon=False) +plot(ax, "RdPu_r", "RdPu\n(Red Purple)") +ax = plt.subplot(rows, cols, 8, frameon=False) +plot(ax, "OrRd_r", "OrRd\n(Orange Red)") + + +ax = plt.subplot(rows, cols, 9, frameon=False) +plot(ax, "Greys_r", "Greys") +ax = plt.subplot(rows, cols, 10, frameon=False) +plot(ax, "GnBu_r", "GnBu\n(Green Blue)") +ax = plt.subplot(rows, cols, 11, frameon=False) +plot(ax, "BuPu_r", "BuPu\n(Blue Purple)") +ax = plt.subplot(rows, cols, 12, frameon=False) +plot(ax, "YlOrBr_r", "YlOrBr\n(Yellow Orange Brown)") + +plt.subplots_adjust(left=0, bottom=0, right=1, top=1, hspace=0.00, wspace=0.00) +plt.savefig("../../figures/colors/mona-lisa.pdf", dpi=300) +plt.show() diff --git a/code/colors/open-colors.py b/code/colors/open-colors.py new file mode 100644 index 0000000..c6e2398 --- /dev/null +++ b/code/colors/open-colors.py @@ -0,0 +1,224 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatch +from matplotlib.patches import FancyBboxPatch + +# Open-color colors from https://yeun.github.io/open-color/ +colors = { + "gray": { + 0: "#f8f9fa", + 1: "#f1f3f5", + 2: "#e9ecef", + 3: "#dee2e6", + 4: "#ced4da", + 5: "#adb5bd", + 6: "#868e96", + 7: "#495057", + 8: "#343a40", + 9: "#212529", + }, + "red": { + 0: "#fff5f5", + 1: "#ffe3e3", + 2: "#ffc9c9", + 3: "#ffa8a8", + 4: "#ff8787", + 5: "#ff6b6b", + 6: "#fa5252", + 7: "#f03e3e", + 8: "#e03131", + 9: "#c92a2a", + }, + "pink": { + 0: "#fff0f6", + 1: "#ffdeeb", + 2: "#fcc2d7", + 3: "#faa2c1", + 4: "#f783ac", + 5: "#f06595", + 6: "#e64980", + 7: "#d6336c", + 8: "#c2255c", + 9: "#a61e4d", + }, + "grape": { + 0: "#f8f0fc", + 1: "#f3d9fa", + 2: "#eebefa", + 3: "#e599f7", + 4: "#da77f2", + 5: "#cc5de8", + 6: "#be4bdb", + 7: "#ae3ec9", + 8: "#9c36b5", + 9: "#862e9c", + }, + "violet": { + 0: "#f3f0ff", + 1: "#e5dbff", + 2: "#d0bfff", + 3: "#b197fc", + 4: "#9775fa", + 5: "#845ef7", + 6: "#7950f2", + 7: "#7048e8", + 8: "#6741d9", + 9: "#5f3dc4", + }, + "indigo": { + 0: "#edf2ff", + 1: "#dbe4ff", + 2: "#bac8ff", + 3: "#91a7ff", + 4: "#748ffc", + 5: "#5c7cfa", + 6: "#4c6ef5", + 7: "#4263eb", + 8: "#3b5bdb", + 9: "#364fc7", + }, + "blue": { + 0: "#e7f5ff", + 1: "#d0ebff", + 2: "#a5d8ff", + 3: "#74c0fc", + 4: "#4dabf7", + 5: "#339af0", + 6: "#228be6", + 7: "#1c7ed6", + 8: "#1971c2", + 9: "#1864ab", + }, + "cyan": { + 0: "#e3fafc", + 1: "#c5f6fa", + 2: "#99e9f2", + 3: "#66d9e8", + 4: "#3bc9db", + 5: "#22b8cf", + 6: "#15aabf", + 7: "#1098ad", + 8: "#0c8599", + 9: "#0b7285", + }, + "teal": { + 0: "#e6fcf5", + 1: "#c3fae8", + 2: "#96f2d7", + 3: "#63e6be", + 4: "#38d9a9", + 5: "#20c997", + 6: "#12b886", + 7: "#0ca678", + 8: "#099268", + 9: "#087f5b", + }, + "green": { + 0: "#ebfbee", + 1: "#d3f9d8", + 2: "#b2f2bb", + 3: "#8ce99a", + 4: "#69db7c", + 5: "#51cf66", + 6: "#40c057", + 7: "#37b24d", + 8: "#2f9e44", + 9: "#2b8a3e", + }, + "lime": { + 0: "#f4fce3", + 1: "#e9fac8", + 2: "#d8f5a2", + 3: "#c0eb75", + 4: "#a9e34b", + 5: "#94d82d", + 6: "#82c91e", + 7: "#74b816", + 8: "#66a80f", + 9: "#5c940d", + }, + "yellow": { + 0: "#fff9db", + 1: "#fff3bf", + 2: "#ffec99", + 3: "#ffe066", + 4: "#ffd43b", + 5: "#fcc419", + 6: "#fab005", + 7: "#f59f00", + 8: "#f08c00", + 9: "#e67700", + }, + "orange": { + 0: "#fff4e6", + 1: "#ffe8cc", + 2: "#ffd8a8", + 3: "#ffc078", + 4: "#ffa94d", + 5: "#ff922b", + 6: "#fd7e14", + 7: "#f76707", + 8: "#e8590c", + 9: "#d9480f", + }, +} + + +nx, dx = 10, 5 +ny, dy = 13, 5 + +fig = plt.figure(figsize=(8, 8), dpi=100) +ax = plt.subplot( + 1, + 1, + 1, + frameon=False, + xlim=(-1, nx * dx), + ylim=(-1, (ny - 1) * dy + 2), + xticks=[], + yticks=[], +) + + +for palette, y in zip(colors.keys(), range(0, ny * dy, dy)): + for level, x in zip(range(10), range(0, nx * dx, dx)): + fancy = FancyBboxPatch( + (x, y), + dx - 1, + 0.3 * dy, + facecolor=colors[palette][level], + edgecolor="black", + linewidth=0, + ) + ax.add_patch(fancy) + + name = "%s %d" % (palette.upper(), level) + ax.text( + x, + y - 0.15 * dy, + name, + size=8, + family="Roboto", + weight="regular", + ha="left", + va="top", + ) + + value = colors[palette][level].upper() + ax.text( + x, + y - 0.35 * dy, + value, + size=8, + color="0.5", + family="Roboto", + weight="regular", + ha="left", + va="top", + ) + + +plt.tight_layout() +plt.savefig("../../figures/colors/open-colors.pdf") +plt.savefig("../../figures/colors/open-colors.png", dpi=300) +plt.show() diff --git a/code/colors/stacked-plots.py b/code/colors/stacked-plots.py new file mode 100644 index 0000000..7513a5a --- /dev/null +++ b/code/colors/stacked-plots.py @@ -0,0 +1,277 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from scipy.ndimage import gaussian_filter1d + +material = { + "red": { + 0: "#ffebee", + 1: "#ffcdd2", + 2: "#ef9a9a", + 3: "#e57373", + 4: "#ef5350", + 5: "#f44336", + 6: "#e53935", + 7: "#d32f2f", + 8: "#c62828", + 9: "#b71c1c", + }, + "pink": { + 0: "#fce4ec", + 1: "#f8bbd0", + 2: "#f48fb1", + 3: "#f06292", + 4: "#ec407a", + 5: "#e91e63", + 6: "#d81b60", + 7: "#c2185b", + 8: "#ad1457", + 9: "#880e4f", + }, + "purple": { + 0: "#f3e5f5", + 1: "#e1bee7", + 2: "#ce93d8", + 3: "#ba68c8", + 4: "#ab47bc", + 5: "#9c27b0", + 6: "#8e24aa", + 7: "#7b1fa2", + 8: "#6a1b9a", + 9: "#4a148c", + }, + "deep purple": { + 0: "#ede7f6", + 1: "#d1c4e9", + 2: "#b39ddb", + 3: "#9575cd", + 4: "#7e57c2", + 5: "#673ab7", + 6: "#5e35b1", + 7: "#512da8", + 8: "#4527a0", + 9: "#311b92", + }, + "indigo": { + 0: "#e8eaf6", + 1: "#c5cae9", + 2: "#9fa8da", + 3: "#7986cb", + 4: "#5c6bc0", + 5: "#3f51b5", + 6: "#3949ab", + 7: "#303f9f", + 8: "#283593", + 9: "#1a237e", + }, + "blue": { + 0: "#e3f2fd", + 1: "#bbdefb", + 2: "#90caf9", + 3: "#64b5f6", + 4: "#42a5f5", + 5: "#2196f3", + 6: "#1e88e5", + 7: "#1976d2", + 8: "#1565c0", + 9: "#0d47a1", + }, + "light blue": { + 0: "#e1f5fe", + 1: "#b3e5fc", + 2: "#81d4fa", + 3: "#4fc3f7", + 4: "#29b6f6", + 5: "#03a9f4", + 6: "#039be5", + 7: "#0288d1", + 8: "#0277bd", + 9: "#01579b", + }, + "cyan": { + 0: "#e0f7fa", + 1: "#b2ebf2", + 2: "#80deea", + 3: "#4dd0e1", + 4: "#26c6da", + 5: "#00bcd4", + 6: "#00acc1", + 7: "#0097a7", + 8: "#00838f", + 9: "#006064", + }, + "teal": { + 0: "#e0f2f1", + 1: "#b2dfdb", + 2: "#80cbc4", + 3: "#4db6ac", + 4: "#26a69a", + 5: "#009688", + 6: "#00897b", + 7: "#00796b", + 8: "#00695c", + 9: "#004d40", + }, + "green": { + 0: "#e8f5e9", + 1: "#c8e6c9", + 2: "#a5d6a7", + 3: "#81c784", + 4: "#66bb6a", + 5: "#4caf50", + 6: "#43a047", + 7: "#388e3c", + 8: "#2e7d32", + 9: "#1b5e20", + }, + "light green": { + 0: "#f1f8e9", + 1: "#dcedc8", + 2: "#c5e1a5", + 3: "#aed581", + 4: "#9ccc65", + 5: "#8bc34a", + 6: "#7cb342", + 7: "#689f38", + 8: "#558b2f", + 9: "#33691e", + }, + "lime": { + 0: "#f9fbe7", + 1: "#f0f4c3", + 2: "#e6ee9c", + 3: "#dce775", + 4: "#d4e157", + 5: "#cddc39", + 6: "#c0ca33", + 7: "#afb42b", + 8: "#9e9d24", + 9: "#827717", + }, + "yellow": { + 0: "#fffde7", + 1: "#fff9c4", + 2: "#fff59d", + 3: "#fff176", + 4: "#ffee58", + 5: "#ffeb3b", + 6: "#fdd835", + 7: "#fbc02d", + 8: "#f9a825", + 9: "#f57f17", + }, + "amber": { + 0: "#fff8e1", + 1: "#ffecb3", + 2: "#ffe082", + 3: "#ffd54f", + 4: "#ffca28", + 5: "#ffc107", + 6: "#ffb300", + 7: "#ffa000", + 8: "#ff8f00", + 9: "#ff6f00", + }, + "orange": { + 0: "#fff3e0", + 1: "#ffe0b2", + 2: "#ffcc80", + 3: "#ffb74d", + 4: "#ffa726", + 5: "#ff9800", + 6: "#fb8c00", + 7: "#f57c00", + 8: "#ef6c00", + 9: "#e65100", + }, + "deep orange": { + 0: "#fbe9e7", + 1: "#ffccbc", + 2: "#ffab91", + 3: "#ff8a65", + 4: "#ff7043", + 5: "#ff5722", + 6: "#f4511e", + 7: "#e64a19", + 8: "#d84315", + 9: "#bf360c", + }, + "brown": { + 0: "#efebe9", + 1: "#d7ccc8", + 2: "#bcaaa4", + 3: "#a1887f", + 4: "#8d6e63", + 5: "#795548", + 6: "#6d4c41", + 7: "#5d4037", + 8: "#4e342e", + 9: "#3e2723", + }, + "grey": { + 0: "#fafafa", + 1: "#f5f5f5", + 2: "#eeeeee", + 3: "#e0e0e0", + 4: "#bdbdbd", + 5: "#9e9e9e", + 6: "#757575", + 7: "#616161", + 8: "#424242", + 9: "#212121", + }, + "blue grey": { + 0: "#eceff1", + 1: "#cfd8dc", + 2: "#b0bec5", + 3: "#90a4ae", + 4: "#78909c", + 5: "#607d8b", + 6: "#546e7a", + 7: "#455a64", + 8: "#37474f", + 9: "#263238", + }, +} + + +np.random.seed(1) +plt.figure(figsize=(8, 4)) +ax = plt.subplot(1, 1, 1) + +X = np.linspace(0, 1, 500) + +Y0 = np.ones(len(X)) +for i in range(10): + Y = Y0 + np.random.uniform(0, 1 / (i + 1), len(X)) + Y = gaussian_filter1d(Y, 3) + ax.fill_between( + X, + Y, + Y0, + edgecolor="white", + linewidth=0.25, + facecolor=material["blue grey"][9 - i], + ) + ax.fill_between( + X[325:425], Y[325:425], Y0[325:425], facecolor=material["yellow"][9 - i] + ) + ax.plot(X[325:425], Y[325:425], color="black", linewidth=0.25) + Y0 = Y + +ax.axvline(X[325], color="black", linestyle="--") +ax.axvline(X[424], color="black", linestyle="--") + +ax.set_xlim(0, 1) +ax.set_xticks([]) + +ax.set_ylim(1, 3) +ax.set_yticks([]) + +plt.tight_layout() +plt.savefig("../figures/stacked-plots.pdf") +plt.show() diff --git a/code/coordinates/collage.py b/code/coordinates/collage.py new file mode 100644 index 0000000..b60ad64 --- /dev/null +++ b/code/coordinates/collage.py @@ -0,0 +1,52 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import imageio +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.transforms as transforms + + +def imshow(ax, I, position=(0, 0), scale=1, angle=0, zorder=10): + height, width = I.shape + extent = scale * np.array([-width / 2, width / 2, -height / 2, height / 2]) + im = ax.imshow(I, extent=extent, zorder=zorder, cmap="cividis") + transform = transforms.Affine2D().rotate_deg(angle).translate(*position) + trans_data = transform + ax.transData + im.set_transform(trans_data) + x1, x2, y1, y2 = im.get_extent() + ax.plot( + [x1, x2, x2, x1, x1], + [y1, y1, y2, y2, y1], + "white", + linewidth=25 * scale, + transform=trans_data, + zorder=zorder - 0.1, + ) + ax.plot( + [x1, x2, x2, x1, x1], + [y1, y1, y2, y2, y1], + "black", + alpha=0.25, + linewidth=40 * scale, + transform=trans_data, + zorder=zorder - 0.2, + ) + + +fig = plt.figure(figsize=(5, 5)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1, frameon=False, xlim=[0, 1000], ylim=[0, 1000]) + + +np.random.seed(123) +I = imageio.imread("../data/mona-lisa.png") +for i in range(200): + position = np.random.uniform(-100, 1100, 2) + scale = np.random.uniform(0.20, 0.25) + angle = np.random.uniform(-75, +75) + imshow(ax, I, position, scale, angle, zorder=10 + i) + +plt.savefig("../../figures/coordinates/collage.png", dpi=300) +plt.show() diff --git a/code/coordinates/transforms-blend.py b/code/coordinates/transforms-blend.py new file mode 100644 index 0000000..2c00c8a --- /dev/null +++ b/code/coordinates/transforms-blend.py @@ -0,0 +1,29 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.transforms import blended_transform_factory, ScaledTranslation + +fig = plt.figure(figsize=(6, 4)) + +ax = fig.add_subplot(1, 1, 1, aspect=1) +ax.set_xlim(0, 10) +ax.set_xticks(range(11)) +ax.set_ylim(0, 5) +ax.set_xticks(range(11)) + +point = 1 / 72 +fontsize = 12 +dx, dy = 0, -1.5 * fontsize * point +offset = ScaledTranslation(dx, dy, fig.dpi_scale_trans) +transform = blended_transform_factory(ax.transData, ax.transAxes + offset) + +for x in range(11): + plt.text(x, 0, "↑", transform=transform, ha="center", va="top", fontsize=fontsize) + +plt.tight_layout() +plt.savefig("../../figures/coordinates/transforms-blend.pdf") +plt.show() diff --git a/code/coordinates/transforms-exercise-1.py b/code/coordinates/transforms-exercise-1.py new file mode 100644 index 0000000..1910dac --- /dev/null +++ b/code/coordinates/transforms-exercise-1.py @@ -0,0 +1,30 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +fig = plt.figure(figsize=(8, 2)) + +ymin, ymax = [0, 2] +xmin, xmax = [0, 8] +ax = plt.subplot(1, 1, 1, aspect=1, xlim=[xmin, xmax], ylim=[ymin, ymax]) +point = fig.dpi / 72 +X = 0.5 + np.arange(8) +Y = np.ones(len(X)) + +# Marker size is expressed in point^2 and a point is defined as fig.dpi/72 +# This means that a size of 10 really means (10*point)^2 +# Problem is thus to convert points into data coordinates: +# PT_to_DC = lambda x: x * ax.get_window_extent().width / (xmax-xmin) +# Note that the DC_to_PR is validonly for a given window size + +DC_to_PT = lambda x: x * ax.get_window_extent().width / (xmax - xmin) / point +S = DC_to_PT(1) ** 2 +plt.scatter(X, Y, s=S, facecolor="none", edgecolor="black", linewidth=1) + +plt.savefig("../../figures/coordinates/transforms-exercise-1.pdf") +plt.show() diff --git a/code/coordinates/transforms-floating-axis.py b/code/coordinates/transforms-floating-axis.py new file mode 100644 index 0000000..4596191 --- /dev/null +++ b/code/coordinates/transforms-floating-axis.py @@ -0,0 +1,76 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate rotated & translated axis (using axisartists toolkit) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.transforms import Affine2D +from matplotlib.ticker import MultipleLocator +import mpl_toolkits.axisartist.floating_axes as floating_axes + + +fig = plt.figure(figsize=(8, 8)) + +# Main axis +ax1 = plt.subplot(1, 1, 1, aspect=1, xlim=[0, 10], ylim=[0, 10]) +ax1.xaxis.set_major_locator(MultipleLocator(1.00)) +ax1.xaxis.set_minor_locator(MultipleLocator(0.50)) +ax1.yaxis.set_major_locator(MultipleLocator(1.00)) +ax1.yaxis.set_minor_locator(MultipleLocator(0.50)) +ax1.grid(linewidth=0.75, linestyle="--") + +# Floating axis +center = np.array([5, 5]) # data coordinates +size = np.array([5, 3]) # data coordinates +orientation = -30 # degrees +T = size / 2 * [(-1, -1), (+1, -1), (+1, +1), (-1, +1), (-1, -1)] +rotation = Affine2D().rotate_deg(orientation) +P = center + rotation.transform(T) + +# Floating axis bounding box visualization +# T = rotation.transform(T) +# xmin, xmax = T[:,0].min(), T[:,0].max() +# ymin, ymax = T[:,1].min(), T[:,1].max() +# T = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin), (xmin, ymin)] +# P = center + T +# plt.plot(P[:,0], P[:,1], color="black", linewidth=0.75) + +# Actual floating axis +DC_to_FC = ax1.transData.transform +FC_to_NFC = fig.transFigure.inverted().transform +DC_to_NFC = lambda x: FC_to_NFC(DC_to_FC(x)) +xmin, ymin = DC_to_NFC((P[:, 0].min(), P[:, 1].min())) +xmax, ymax = DC_to_NFC((P[:, 0].max(), P[:, 1].max())) +transform = Affine2D().scale(1, 1).rotate_deg(orientation) +helper = floating_axes.GridHelperCurveLinear(transform, (0, size[0], 0, size[1])) +ax2 = floating_axes.FloatingSubplot(fig, 111, grid_helper=helper, zorder=0) +ax2.set_position((xmin, ymin, xmax - xmin, ymax - xmin)) +fig.add_subplot(ax2) + + +# Cosmetic changes +ax2.axis["bottom"].major_ticks.set_tick_out(True) +ax2.axis["bottom"].major_ticklabels.set_visible(False) +ax2.axis["top"].major_ticks.set_visible(False) +ax2.axis["left"].major_ticks.set_tick_out(True) +ax2.axis["left"].major_ticklabels.set_visible(False) +ax2.axis["right"].major_ticks.set_visible(False) +aux = ax2.get_aux_axes(transform) +aux.text( + 0.1, + 0.1, + "Floating & rotated axis", + ha="left", + va="bottom", + size=10, + rotation=orientation, + rotation_mode="anchor", +) + +aux.set_xticks([0, 1]) + +plt.savefig("../../figures/coordinates/transform-example.pdf") +plt.show() diff --git a/code/coordinates/transforms-hist.py b/code/coordinates/transforms-hist.py new file mode 100644 index 0000000..e5b2889 --- /dev/null +++ b/code/coordinates/transforms-hist.py @@ -0,0 +1,163 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate rotated & translated axis (using axisartists toolkit) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon +from matplotlib.transforms import Affine2D +import mpl_toolkits.axisartist.floating_axes as floating_axes + + +# Reroducibility seed +np.random.seed(123) + +# Generate some data +Z = np.random.normal(0, (1.25, 0.75), (150, 2)) +Z = Affine2D().rotate_deg(35).transform(Z) +Zm = Z.mean(axis=0) + + +# Principal components analysis +# Note that for some seeds, the PC1 and PC2 needs to be inverted +# It could be fixed by looking at the orientation but I'm lazy +W, V = np.linalg.eig(np.cov(Z.T)) +PC1, PC2 = V[np.argsort(abs(W))] +rotation = 180 * np.arctan2(*PC1) / np.pi +T = np.array([PC1[1], PC1[0]]) # tangent to PCA1 +O = np.array([T[1], -T[0]]) # orthogonal to PCA1 + + +# Draw +fig = plt.figure(figsize=(5, 5)) +ax1 = fig.add_axes([0.05, 0.05, 0.9, 0.9], aspect=1) + +# Main scatter plot +ax1.scatter(Z[:, 0], Z[:, 1], s=50, fc="C0", ec="white", lw=0.75) +ax1.set_xlim([-3, 6]) +ax1.set_xticks([-3, 6]) +ax1.set_xticklabels([]) +ax1.set_ylim([-3, 6]) +ax1.set_yticks([-3, 6]) +ax1.set_yticklabels([]) +ax1.spines["top"].set_visible(False) +ax1.spines["right"].set_visible(False) + + +# Draw main PCA axis +P = np.vstack([Zm - T * 10, Zm + T * 10]) +ax1.plot(P[:, 0], P[:, 1], color="black", linestyle="--", linewidth=0.75, zorder=10) + +# Compute the width of the distribution along orthogonal direction to the PCA +# main axis. This is made by rotating points and taking max on the Y axis. +transform = Affine2D().rotate_deg(-rotation) +P = transform.transform(Z - Z.mean(axis=0)) +d = np.abs(P[:, 1]).max() + +# Draw a rectangle surrounding the distribution & oriented along PCA main axis +P = np.vstack( + [ + Zm - 10 * T - d * O, + Zm + (6 - d) * T - d * O, + Zm + (6 - d) * T + d * O, + Zm - 10 * T + d * O, + ] +) +ax1.add_patch( + Polygon( + P, + closed=True, + fill=True, + edgecolor="None", + facecolor="C0", + alpha=0.1, + zorder=-50, + ) +) +P = np.vstack([Zm - 10 * T, Zm + (6 - d) * T]) - d * O +plt.plot(P[:, 0], P[:, 1], color="C0", linestyle="-", linewidth=0.75, alpha=0.25) +P = np.vstack([Zm - 10 * T, Zm + (6 - d) * T]) + d * O +plt.plot(P[:, 0], P[:, 1], color="C0", linestyle="-", linewidth=0.75, alpha=0.25) + +# Some markers on the axis to show the mean (we could compute exactly the delta +# for placing the marker but it is not the point of this example) +ax1.scatter(Zm[0], -2.85, s=50, color="black", marker="v", clip_on=False) +ax1.scatter(-2.85, Zm[1], s=50, color="black", marker="<", clip_on=False) + + +# Now the complicated stuff to orientate and translate the secondary axis + +# 1. Compute the center of the histogram +C = Zm + 6 * T + +# 2. Compute the coordinate and the size in normalized figure coordinates +x, y = fig.transFigure.inverted().transform(ax1.transData.transform(C)) +xo, yo = fig.transFigure.inverted().transform(ax1.transData.transform(C + 2 * d * O)) +h = w = np.sqrt((xo - x) ** 2 + (yo - y) ** 2) + +# 3. Create the secondary axis +# Warning: it must be squared, ie. xmax-xmin = ymax-ymin +# It is possible to have non squared axis, but it would complicate things. +xmin, xmax = -16, 16 +ymin, ymax = 0, xmax - xmin +transform = Affine2D().rotate_deg(rotation - 90) +helper = floating_axes.GridHelperCurveLinear(transform, (xmin, xmax, ymin, ymax)) +ax2 = floating_axes.FloatingSubplot(fig, 111, grid_helper=helper, zorder=0) + +# This auxiliary axis is necessary to draw stuff (no real idea why) +ax2_aux = ax2.get_aux_axes(transform) + +# 4. We know the size of the axis we want but it is rotated. When we specify +# the size and position, it related to the non-rotate axis and we thus need +# to compute the bounding box. To do that, we rotate the four coordinates +# from which we deduce the bounding box coordinates. +transform = Affine2D().rotate_deg(rotation - 90) +R = transform.transform( + [ + (x - w / 2, y - h / 2), + (x + w / 2, y - h / 2), + (x - w / 2, y + h / 2), + (x + w / 2, y + h / 2), + ] +) +w = R[:, 0].max() - R[:, 0].min() +h = R[:, 1].max() - R[:, 1].min() +ax2.set_position((x - w / 2, y - h / 2, w, h)) +fig.add_subplot(ax2) + +# 5. Some decoration the secondary axis +ax2.axis["left"].major_ticklabels.set_visible(False) +ax2.axis["bottom"].major_ticklabels.set_visible(False) +ax2.axis["bottom"].major_ticks.set_tick_out(True) +ax2.axis["left"].set_visible(False) +ax2.axis["right"].set_visible(False) +ax2.axis["top"].set_visible(False) +ax2.set_xticks([0, 1]) +ax2.patch.set_visible(False) + +# 6. Display the histogram, taking care of the extents of the X axis +counts, bins = np.histogram(-Z @ PC1, bins=12) +X = (bins - bins[0]) / (bins[-1] - bins[0]) +X = xmin + (xmax - xmin) * X +Y = np.array(counts) +ax2_aux.hist(X[:-1], X, weights=Y, facecolor="C0", edgecolor="white", linewidth=0.25) + +# 7. Adding some labels +dx, dy = (X[1] - X[0]) / 2, 0.75 +for x, y in zip(X, Y): + ax2_aux.text( + x + dx, + y + dy, + "%d" % y, + ha="center", + va="center", + size=8, + rotation=rotation - 90, + ) + +# Save +plt.savefig("../../figures/coordinates/transforms-hist.pdf") +plt.show() diff --git a/code/coordinates/transforms-letter.py b/code/coordinates/transforms-letter.py new file mode 100644 index 0000000..96c4f0f --- /dev/null +++ b/code/coordinates/transforms-letter.py @@ -0,0 +1,41 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.transforms import ScaledTranslation + +fig = plt.figure(figsize=(6, 4)) + +ax = fig.add_subplot(2, 1, 1) +plt.text( + 0.1, + 0.1, + "A", + fontsize="x-large", + weight="bold", + ha="left", + va="baseline", + transform=ax.transAxes, +) + +ax = fig.add_subplot(2, 1, 2) +dx, dy = 10 / fig.dpi, 10 / fig.dpi +offset = ScaledTranslation(dx, dy, fig.dpi_scale_trans) + +plt.text( + 0, + 0, + "B", + fontsize="x-large", + weight="bold", + ha="left", + va="baseline", + transform=ax.transAxes + offset, +) + +plt.tight_layout() +plt.savefig("../../figures/coordinates/transforms-letter.pdf") +plt.show() diff --git a/code/coordinates/transforms-polar.py b/code/coordinates/transforms-polar.py new file mode 100644 index 0000000..a469060 --- /dev/null +++ b/code/coordinates/transforms-polar.py @@ -0,0 +1,32 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.transforms as transforms + +fig = plt.figure(figsize=(5, 5), dpi=100) +ax = fig.add_subplot(1, 1, 1, projection="polar") +ax.set_ylim(-1, 1), ax.set_yticks([-1, -0.5, 0, 0.5, 1]) + +FC_to_DC = ax.transData.inverted().transform +NDC_to_FC = ax.transAxes.transform +NDC_to_DC = lambda x: FC_to_DC(NDC_to_FC(x)) + +P = NDC_to_DC([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]) +plt.plot( + P[:, 0], + P[:, 1], + clip_on=False, + color="k", + linewidth=1.0, + linestyle="--", + zorder=-10, +) +plt.scatter(P[:-1, 0], P[:-1, 1], clip_on=False, facecolor="w", edgecolor="k") + +plt.tight_layout() +plt.savefig("../../figures/coordinates/transforms-polar.pdf") +plt.show() diff --git a/code/coordinates/transforms.py b/code/coordinates/transforms.py new file mode 100644 index 0000000..f043eac --- /dev/null +++ b/code/coordinates/transforms.py @@ -0,0 +1,34 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.transforms as transforms + +fig = plt.figure(figsize=(6, 5), dpi=100) +ax = fig.add_subplot(1, 1, 1) + +ax.set_xlim(0, 360) +ax.set_ylim(-1, 1) + +DC_to_FC = ax.transData.transform +FC_to_DC = ax.transData.inverted().transform + +NDC_to_FC = ax.transAxes.transform +FC_to_NDC = ax.transAxes.inverted().transform + +NFC_to_FC = fig.transFigure.transform +FC_to_NFC = fig.transFigure.inverted().transform + + +print(NFC_to_FC([1, 1])) # (600,500) +print(NDC_to_FC([1, 1])) # (540,440) +print(DC_to_FC([360, 1])) # (540,440) + +DC_to_NDC = lambda x: FC_to_NDC(DC_to_FC(x)) + +print(DC_to_NDC([0, -1])) # (0.0, 0.0) +print(DC_to_NDC([180, 0])) # (0.5, 0.5) +print(DC_to_NDC([360, 1])) # (1.0, 1.0) diff --git a/code/data/.DS_Store b/code/data/.DS_Store new file mode 100644 index 0000000..f1ee552 Binary files /dev/null and b/code/data/.DS_Store differ diff --git a/code/data/John-Hunter-comic.png b/code/data/John-Hunter-comic.png new file mode 100644 index 0000000..6b6044a Binary files /dev/null and b/code/data/John-Hunter-comic.png differ diff --git a/code/data/John-Hunter.png b/code/data/John-Hunter.png new file mode 100644 index 0000000..42ec3cd Binary files /dev/null and b/code/data/John-Hunter.png differ diff --git a/code/data/John-Hunter.pxd/QuickLook/Icon.tiff b/code/data/John-Hunter.pxd/QuickLook/Icon.tiff new file mode 100644 index 0000000..9269fa8 Binary files /dev/null and b/code/data/John-Hunter.pxd/QuickLook/Icon.tiff differ diff --git a/code/data/John-Hunter.pxd/QuickLook/Thumbnail.tiff b/code/data/John-Hunter.pxd/QuickLook/Thumbnail.tiff new file mode 100644 index 0000000..0a87fc0 Binary files /dev/null and b/code/data/John-Hunter.pxd/QuickLook/Thumbnail.tiff differ diff --git a/code/data/John-Hunter.pxd/data/661EDC0D-0CB2-48AB-A474-4F1BCD77C183 b/code/data/John-Hunter.pxd/data/661EDC0D-0CB2-48AB-A474-4F1BCD77C183 new file mode 100644 index 0000000..81f71af Binary files /dev/null and b/code/data/John-Hunter.pxd/data/661EDC0D-0CB2-48AB-A474-4F1BCD77C183 differ diff --git a/code/data/John-Hunter.pxd/metadata.info b/code/data/John-Hunter.pxd/metadata.info new file mode 100644 index 0000000..0fe7efc Binary files /dev/null and b/code/data/John-Hunter.pxd/metadata.info differ diff --git a/code/data/mona-lisa.png b/code/data/mona-lisa.png new file mode 100644 index 0000000..5e1f465 Binary files /dev/null and b/code/data/mona-lisa.png differ diff --git a/code/data/poppy.png b/code/data/poppy.png new file mode 100755 index 0000000..d64466c Binary files /dev/null and b/code/data/poppy.png differ diff --git a/code/defaults/defaults-exercice-1.py b/code/defaults/defaults-exercice-1.py new file mode 100644 index 0000000..aea2e79 --- /dev/null +++ b/code/defaults/defaults-exercice-1.py @@ -0,0 +1,66 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / Custom defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) +p = plt.rcParams + +p["figure.figsize"] = 6, 2.5 +p["figure.facecolor"] = "#fff" + +p["axes.axisbelow"] = True +p["axes.linewidth"] = 1 +p["axes.facecolor"] = "#f9f9f9" +p["axes.ymargin"] = 0.0 + +p["axes.grid"] = True +p["axes.grid.axis"] = "x" +p["grid.color"] = "#999999" +p["grid.linestyle"] = "--" + +p["axes.spines.bottom"] = True +p["axes.spines.left"] = True +p["axes.spines.right"] = False +p["axes.spines.top"] = False +p["font.sans-serif"] = ["Fira Sans Condensed"] + +p["xtick.bottom"] = True +p["xtick.top"] = False +p["xtick.direction"] = "out" +p["xtick.major.size"] = 0 +p["xtick.major.width"] = 1 +p["xtick.major.pad"] = 65 + +p["ytick.left"] = True +p["ytick.right"] = False +p["ytick.direction"] = "out" +p["ytick.major.size"] = 5 +p["ytick.major.width"] = 1 + +p["lines.linewidth"] = 2 +p["lines.marker"] = "o" +p["lines.markeredgewidth"] = 1.5 +p["lines.markeredgecolor"] = "auto" +p["lines.markerfacecolor"] = "white" +p["lines.markersize"] = 6 + + +fig = plt.figure() +ax = plt.subplot(1, 1, 1, aspect=1) +ax.plot(X, C, markevery=(0, 64), clip_on=False, zorder=10) +ax.plot(X, S, markevery=(0, 64), clip_on=False, zorder=10) +ax.set_yticks([-1, 0, 1]) +ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi]) +ax.set_xticklabels(["-π", "-π/2", "0", "+π/2", "+π"]) +ax.spines["bottom"].set_position(("data", 0)) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-exercice-1.pdf") +plt.show() diff --git a/code/defaults/defaults-step-1.py b/code/defaults/defaults-step-1.py new file mode 100644 index 0000000..b9f0bd4 --- /dev/null +++ b/code/defaults/defaults-step-1.py @@ -0,0 +1,18 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / Implicit defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) +plt.plot(X, C) +plt.plot(X, S) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-step-1.pdf") +plt.show() diff --git a/code/defaults/defaults-step-2.py b/code/defaults/defaults-step-2.py new file mode 100644 index 0000000..267dc13 --- /dev/null +++ b/code/defaults/defaults-step-2.py @@ -0,0 +1,59 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / explicit defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) +p = plt.rcParams + +fig = plt.figure( + figsize=p["figure.figsize"], + dpi=p["figure.dpi"], + facecolor=p["figure.facecolor"], + edgecolor=p["figure.edgecolor"], + frameon=p["figure.frameon"], +) + +ax = plt.subplot(1, 1, 1) + +ax.plot( + X, C, color="C0", linewidth=p["lines.linewidth"], linestyle=p["lines.linestyle"] +) + +ax.plot( + X, S, color="C1", linewidth=p["lines.linewidth"], linestyle=p["lines.linestyle"] +) + +xmin, xmax = X.min(), X.max() +xmargin = p["axes.xmargin"] * (xmax - xmin) +ax.set_xlim(xmin - xmargin, xmax + xmargin) + +ymin, ymax = min(C.min(), S.min()), max(C.max(), S.max()) +ymargin = p["axes.ymargin"] * (ymax - ymin) +ax.set_ylim(ymin - ymargin, ymax + ymargin) + +ax.tick_params( + axis="x", + which="major", + direction=p["xtick.direction"], + length=p["xtick.major.size"], + width=p["xtick.major.width"], +) + +ax.tick_params( + axis="y", + which="major", + direction=p["ytick.direction"], + length=p["ytick.major.size"], + width=p["ytick.major.width"], +) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-step-2.pdf") +plt.show() diff --git a/code/defaults/defaults-step-3.py b/code/defaults/defaults-step-3.py new file mode 100644 index 0000000..dd6e8fd --- /dev/null +++ b/code/defaults/defaults-step-3.py @@ -0,0 +1,65 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / Custom defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) +p = plt.rcParams + +p["figure.figsize"] = 6, 2.5 +p["figure.edgecolor"] = "black" +p["figure.facecolor"] = "#f9f9f9" + +p["axes.linewidth"] = 1 +p["axes.facecolor"] = "#f9f9f9" +p["axes.ymargin"] = 0.1 +p["axes.spines.bottom"] = True +p["axes.spines.left"] = True +p["axes.spines.right"] = False +p["axes.spines.top"] = False +p["font.sans-serif"] = ["Fira Sans Condensed"] + +p["axes.grid"] = False +p["grid.color"] = "black" +p["grid.linewidth"] = 0.1 + +p["xtick.bottom"] = True +p["xtick.top"] = False +p["xtick.direction"] = "out" +p["xtick.major.size"] = 5 +p["xtick.major.width"] = 1 +p["xtick.minor.size"] = 3 +p["xtick.minor.width"] = 0.5 +p["xtick.minor.visible"] = True + +p["ytick.left"] = True +p["ytick.right"] = False +p["ytick.direction"] = "out" +p["ytick.major.size"] = 5 +p["ytick.major.width"] = 1 +p["ytick.minor.size"] = 3 +p["ytick.minor.width"] = 0.5 +p["ytick.minor.visible"] = True + +p["lines.linewidth"] = 2 +p["lines.marker"] = "o" +p["lines.markeredgewidth"] = 1.5 +p["lines.markeredgecolor"] = "auto" +p["lines.markerfacecolor"] = "white" +p["lines.markersize"] = 6 + +fig = plt.figure(linewidth=1) +ax = plt.subplot(1, 1, 1, aspect=1) +ax.plot(X, C, markevery=(0, 32)) +ax.plot(X, S, markevery=(0, 32)) +ax.set_yticks([-1, 0, 1]) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-step-3.pdf") +plt.show() diff --git a/code/defaults/defaults-step-4.py b/code/defaults/defaults-step-4.py new file mode 100644 index 0000000..f864192 --- /dev/null +++ b/code/defaults/defaults-step-4.py @@ -0,0 +1,24 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / Custom defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +plt.style.use("./mystyle.txt") + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure(linewidth=1) +ax = plt.subplot(1, 1, 1, aspect=1) +ax.plot(X, C, markevery=(0, 32)) +ax.plot(X, S, markevery=(0, 32)) +ax.set_yticks([-1, 0, 1]) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-step-4.pdf") +plt.show() diff --git a/code/defaults/defaults-step-5.py b/code/defaults/defaults-step-5.py new file mode 100644 index 0000000..20d394c --- /dev/null +++ b/code/defaults/defaults-step-5.py @@ -0,0 +1,34 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Defaults settings / Custom defaults +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +plt.style.use("./mystyle.txt") + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure() +ax = plt.subplot(1, 1, 1, aspect=1) +ax.plot(X, C) +ax.plot(X, S) + +ax.set_yticks([-1, 1]) +ax.set_yticklabels(["-1", "+1"]) +ax.set_xticks([-np.pi, -np.pi / 2, np.pi / 2, np.pi]) +ax.set_xticklabels(["-π", "-π/2", "+π/2", "+π"]) + +ax.spines["bottom"].set_position(("data", 0)) +ax.spines["left"].set_position(("data", 0)) + +ax.plot(1, 0, ">k", transform=ax.get_yaxis_transform(), clip_on=False) +ax.plot(0, 1, "^k", transform=ax.get_xaxis_transform(), clip_on=False) + +plt.tight_layout() +plt.savefig("../../figures/defaults/defaults-step-5.pdf") +plt.show() diff --git a/code/defaults/mystyle.txt b/code/defaults/mystyle.txt new file mode 100644 index 0000000..1581872 --- /dev/null +++ b/code/defaults/mystyle.txt @@ -0,0 +1,36 @@ + +figure.figsize: 6,2.5 + +axes.linewidth: 1 +axes.axisbelow: True +axes.ymargin: 0.1 +axes.spines.bottom: True +axes.spines.left: True +axes.spines.right: False +axes.spines.top: False +font.sans-serif: Fira Sans Condensed + +axes.grid: False +grid.color: black +grid.linewidth: .1 + +xtick.bottom: True +xtick.top: False +xtick.direction: out +xtick.major.size: 5 +xtick.major.width: 1 +xtick.minor.size: 3 +xtick.minor.width: .5 +xtick.minor.visible: True + +ytick.left: True +ytick.right: False +ytick.direction: out +ytick.major.size: 5 +ytick.major.width: 1 +ytick.minor.size: 3 +ytick.minor.width: .5 +ytick.minor.visible: True + +lines.linewidth: 2 +lines.markersize: 5 diff --git a/code/introduction/matplotlib-timeline.py b/code/introduction/matplotlib-timeline.py new file mode 100644 index 0000000..84d4a22 --- /dev/null +++ b/code/introduction/matplotlib-timeline.py @@ -0,0 +1,86 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +def annotate(ax, x, y, text, fc="#ff7777", y0=0): + y = y - 0.5 + ax.annotate( + " " + text + " ", + xy=(x, y), + xycoords="data", + xytext=(0, 12), + textcoords="offset points", + color="white", + size="x-small", + va="center", + ha="center", + weight="bold", + bbox=dict(boxstyle="round", fc=fc, ec="none"), + arrowprops=dict( + arrowstyle="wedge,tail_width=1.", fc=fc, ec="none", patchA=None + ), + ) + plt.plot([x, x], [y, y0], color="black", linestyle=":", linewidth=0.75) + + +fig = plt.figure(figsize=(5, 2)) +ax = fig.add_subplot(111, xlim=(2002.5, 2021.5), ylim=(0, 6.5), yticks=([])) +ax.tick_params("x", labelsize="x-small", which="major") +plt.plot([2002.5, 2021.5], [0, 0], color="black", linewidth=1.0, clip_on=False) +X = np.arange(2003, 2022) +Y = np.zeros(len(X)) +plt.scatter( + X, + Y, + s=50, + linewidth=1.0, + zorder=10, + clip_on=False, + edgecolor="black", + facecolor="white", +) + +annotate(ax, 2021, 4, "3.4") +annotate(ax, 2020, 3, "3.3") +annotate(ax, 2019, 4, "3.2") +annotate(ax, 2019, 2, "3.1") +annotate(ax, 2018, 3, "3.0", y0=1.5) +annotate(ax, 2018, 1, "2.2", fc="#777777") +annotate(ax, 2017, 4, "2.1", y0=2.5) +annotate(ax, 2017, 2, "2.0") +annotate(ax, 2015, 2, "1.5") +annotate(ax, 2014, 1, "1.4") +annotate(ax, 2013, 2, "1.3") +annotate(ax, 2012, 1, "1.2") +annotate(ax, 2011, 3, "1.1", y0=2.5) +annotate(ax, 2011, 2, "1.0") +annotate(ax, 2009, 1, "0.99") +annotate(ax, 2003, 1, "0.10") + +x0, x1 = 2002.5, 2011.9 +ax.plot([x0, x1], [5, 5], color="black", linewidth=1, marker="|", clip_on=False) +ax.text((x0 + x1) / 2, 5.1, "J.D. Hunter", ha="center", va="bottom", size="x-small") + +x0, x1 = 2012.1, 2017.9 +ax.plot([x0, x1], [5, 5], color="black", linewidth=1, marker="|", clip_on=False) +ax.text((x0 + x1) / 2, 5.1, "M. Droettboom", ha="center", va="bottom", size="x-small") + +x0, x1 = 2014.1, 2021.5 +ax.plot([x0, x1 + 1], [6, 6], color="black", linewidth=1, marker="|") +ax.text((x0 + x1) / 2, 6.1, "T. Caswell", ha="center", va="bottom", size="x-small") + +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["bottom"].set_visible(False) +ax.set_xticks(np.arange(2003, 2022, 2)) + +plt.tight_layout() +plt.savefig("../../figures/introduction/matplotlib-timeline.pdf") +plt.savefig("../../figures/introduction/matplotlib-timeline.png", dpi=300) +plt.show() diff --git a/code/introduction/visualization-landscape.graffle b/code/introduction/visualization-landscape.graffle new file mode 100644 index 0000000..876fd1c Binary files /dev/null and b/code/introduction/visualization-landscape.graffle differ diff --git a/code/layout/complex-layout-bare.py b/code/layout/complex-layout-bare.py new file mode 100644 index 0000000..ae4770d --- /dev/null +++ b/code/layout/complex-layout-bare.py @@ -0,0 +1,58 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + + +fig = plt.figure(figsize=(6.5, 6)) +gspec = gridspec.GridSpec( + ncols=7, nrows=7, figure=fig, height_ratios=[1, 0.25, 1, 1, 1, 1, 1] +) + +plt.rc("axes", edgecolor=".75") +ax = plt.subplot( + gspec[1, :2], frameon=True, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] +) +ax = plt.subplot( + gspec[0, 2:4], frameon=True, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] +) +ax = plt.subplot( + gspec[0, 5:7], frameon=True, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] +) +plt.rc("axes", edgecolor="black") + +for col in range(2, 7): + ax = plt.subplot( + gspec[1, col], frameon=True, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] + ) + +for row in range(2, 7): + ax = plt.subplot( + gspec[row, :2], frameon=True, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] + ) + for col in range(2, 7): + ax = plt.subplot( + gspec[row, col], + aspect=1, + frameon=True, + xlim=[0, 1], + xticks=[], + ylim=[0, 1], + yticks=[], + ) + + +ax = plt.subplot( + gspec[0, 4], frameon=True, aspect=1, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] +) + +ax = plt.subplot( + gspec[0, :2], frameon=True, xlim=[0, 10], xticks=[], ylim=[0, 4], yticks=[] +) + +plt.savefig("../../figures/layout/complex-layout-bare.pdf") +plt.show() diff --git a/code/layout/complex-layout.py b/code/layout/complex-layout.py new file mode 100644 index 0000000..fccd95c --- /dev/null +++ b/code/layout/complex-layout.py @@ -0,0 +1,185 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + + +fig = plt.figure(figsize=(6.5, 6)) +gspec = gridspec.GridSpec( + ncols=7, nrows=7, figure=fig, height_ratios=[1, 0.25, 1, 1, 1, 1, 1] +) +cmap = plt.get_cmap("Blues") + + +for row in range(2, 7): + ax = plt.subplot( + gspec[row, :2], frameon=False, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] + ) + ax.text( + 1, + 1, + "Very long text using\n newlines and small font", + family="Roboto Condensed", + horizontalalignment="right", + verticalalignment="top", + ) + + for col in range(2, 7): + ax = plt.subplot( + gspec[row, col], + aspect=1, + frameon=False, + xlim=[0, 1], + xticks=[], + ylim=[0, 1], + yticks=[], + ) + ax.axhline(0.5, color="white", lw=1) + ax.axvline(0.5, color="white", lw=1) + + Z = np.random.uniform(0.25, 0.75, (2, 2)) + ax.imshow(Z, extent=[0, 1, 0, 1], cmap=cmap, vmin=0, vmax=1) + if row == 2: + ax.text( + 0.5, + 1.1, + "Subject %d" % (col - 1), + ha="center", + va="bottom", + size="small", + weight="bold", + ) + + +ax = plt.subplot( + gspec[0, 4], frameon=True, aspect=1, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[] +) +ax.axhline(0.5, color="black", lw=0.75) +ax.axvline(0.5, color="black", lw=0.75) + + +# First quadrant +ax.text( + -0.5, 0.75, "First quadrant", ha="right", va="center", size="small", weight="bold" +) +ax.text( + -0.5, + 0.55, + "Explanation of first quadrant", + ha="right", + va="center", + size="x-small", + family="Roboto Condensed", +) +ax.plot( + [-0.25, 0.25], + [0.75, 0.75], + color="black", + marker="o", + markevery=[-1], + clip_on=False, + linewidth=0.75, + markersize=2, +) + +# Second quadrant +ax.text( + -0.5, 0.25, "Second quadrant", ha="right", va="center", size="small", weight="bold" +) +ax.text( + -0.5, + 0.05, + "Explanation of second quadrant", + ha="right", + va="center", + size="x-small", + family="Roboto Condensed", +) +ax.plot( + [-0.25, 0.25], + [0.25, 0.25], + color="black", + marker="o", + markevery=[-1], + clip_on=False, + linewidth=0.75, + markersize=2, +) + +# Third quadrant +ax.text( + 1.5, 0.75, "Third quadrant", ha="left", va="center", size="small", weight="bold" +) +ax.text( + 1.5, + 0.55, + "Explanation of third quadrant", + ha="left", + va="center", + size="x-small", + family="Roboto Condensed", +) +ax.plot( + [1.25, 0.75], + [0.75, 0.75], + color="black", + marker="o", + markevery=[-1], + clip_on=False, + linewidth=0.75, + markersize=2, +) + +# Fourth quadrant +ax.text( + 1.5, 0.25, "Fourth quadrant", ha="left", va="center", size="small", weight="bold" +) +ax.text( + 1.5, + 0.05, + "Explanation of fourth quadrant", + ha="left", + va="center", + size="x-small", + family="Roboto Condensed", +) +ax.plot( + [1.25, 0.75], + [0.25, 0.25], + color="black", + marker="o", + markevery=[-1], + clip_on=False, + linewidth=0.75, + markersize=2, +) + + +# Legend +ax = plt.subplot( + gspec[0, :2], frameon=False, xlim=[0, 10], xticks=[], ylim=[0, 4], yticks=[] +) +ax.scatter([1, 1, 1], [3, 2, 1], color=[cmap(0.25), cmap(0.50), cmap(0.75)]) +ax.text( + 2, 1, "Large", ha="left", va="center", size="small", weight="bold", family="Roboto" +) +ax.text( + 2, 2, "Medium", ha="left", va="center", size="small", weight="bold", family="Roboto" +) +ax.text( + 2, + 3, + "Limited", + ha="left", + va="center", + size="small", + weight="bold", + family="Roboto", +) + +plt.savefig("../../figures/layout/complex-layout.pdf") +plt.show() diff --git a/code/layout/layout-aspect.py b/code/layout/layout-aspect.py new file mode 100644 index 0000000..3dcce82 --- /dev/null +++ b/code/layout/layout-aspect.py @@ -0,0 +1,64 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + +p = plt.rcParams +p["figure.figsize"] = 7, 7 +p["figure.dpi"] = 100 +p["figure.facecolor"] = "#ffffff" +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" + +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True +p["axes.grid"] = True +p["grid.color"] = "0.5" +p["grid.linewidth"] = 0.5 + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure(constrained_layout=True) +nrows, ncols = 2, 2 +gspec = gridspec.GridSpec( + ncols=ncols, nrows=nrows, figure=fig, width_ratios=[1, 2], height_ratios=[1, 2] +) + + +def plot(ax, xmax=1, ymax=1): + ax.set_xlim(0, xmax) + ax.set_xticks(np.linspace(0, xmax, 4 * xmax + 1)) + ax.set_xlabel("X Label") + ax.set_ylim(0, ymax) + ax.set_yticks(np.linspace(0, ymax, 4 * ymax + 1)) + ax.set_ylabel("Y Label") + ax.set_title("Title", family="Roboto", weight=500) + + +if 0: # No constraints + plot(plt.subplot(gspec[0, 0])) + plot(plt.subplot(gspec[1, 0])) + plot(plt.subplot(gspec[0, 1])) + plot(plt.subplot(gspec[1, 1])) + +if 0: # Aspect is constrained + plot(plt.subplot(gspec[0, 0])) + plot(plt.subplot(gspec[1, 0], aspect=1)) + plot(plt.subplot(gspec[0, 1], aspect=1)) + plot(plt.subplot(gspec[1, 1], aspect=1)) + +if 1: # Aspect is constrained but limits fit + plot(plt.subplot(gspec[0, 0])) + plot(plt.subplot(gspec[1, 0], aspect=1), ymax=2) + plot(plt.subplot(gspec[0, 1], aspect=1), xmax=2) + plot(plt.subplot(gspec[1, 1], aspect=1), xmax=2, ymax=2) + +plt.savefig("../../figures/layout/layout-aspect-3.pdf") +plt.show() diff --git a/code/layout/layout-classical.py b/code/layout/layout-classical.py new file mode 100644 index 0000000..f79edad --- /dev/null +++ b/code/layout/layout-classical.py @@ -0,0 +1,49 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +p = plt.rcParams +p["figure.figsize"] = 7, 7 +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True +p["axes.grid"] = True +p["grid.color"] = "0.5" +p["grid.linewidth"] = 0.5 + + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure() +nrows, ncols = 3, 3 + + +def plot(ax, text): + ax.set_xlim(0, 1) + ax.set_xticks(np.linspace(0, 1, 5)) + ax.set_xlabel("X Label") + ax.set_ylim(0, 1) + ax.set_yticks(np.linspace(0, 1, 5)) + ax.set_ylabel("Y Label") + ax.text( + 0.5, 0.5, text, alpha=0.75, ha="center", va="center", weight="bold", size=12 + ) + ax.set_title("Title", family="Roboto", weight=500) + + +for i in range(1, nrows * ncols + 1): + plot( + plt.subplot(nrows, ncols, i, aspect=1), "subplot(%d,%d,%d)" % (nrows, ncols, i) + ) + +plt.tight_layout() +plt.savefig("../../figures/layout/layout-classical.pdf") +plt.show() diff --git a/code/layout/layout-gridspec.py b/code/layout/layout-gridspec.py new file mode 100644 index 0000000..ec5057d --- /dev/null +++ b/code/layout/layout-gridspec.py @@ -0,0 +1,51 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + +p = plt.rcParams +p["figure.figsize"] = 7, 7 +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True +p["axes.grid"] = True +p["grid.color"] = "0.5" +p["grid.linewidth"] = 0.5 + + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure(constrained_layout=True) +nrows, ncols = 3, 3 +gspec = gridspec.GridSpec(ncols=ncols, nrows=nrows, figure=fig) + + +def plot(ax, text): + ax.set_xlim(0, 1) + ax.set_xticks(np.linspace(0, 1, 5)) + ax.set_xlabel("X Label") + ax.set_ylim(0, 1) + ax.set_yticks(np.linspace(0, 1, 5)) + ax.set_ylabel("Y Label") + ax.text( + 0.5, 0.5, text, alpha=0.75, ha="center", va="center", weight="bold", size=12 + ) + ax.set_title("Title", family="Roboto", weight=500) + + +for i in range(ncols): + plot(plt.subplot(gspec[0, i]), "subplot(gspec[0,%d])" % i) +for i in range(1, nrows): + plot(plt.subplot(gspec[i, 0]), "subplot(gspec[%d,0])" % i) +plot(plt.subplot(gspec[1:, 1:]), "subplot(gspec[1:,1:])") + +plt.savefig("../../figures/layout/layout-gridspec.pdf") +plt.show() diff --git a/code/layout/standard-layout-1.py b/code/layout/standard-layout-1.py new file mode 100644 index 0000000..2b5edb8 --- /dev/null +++ b/code/layout/standard-layout-1.py @@ -0,0 +1,56 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + +p = plt.rcParams +p["figure.figsize"] = 7, 7 +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True + +# Some data +n = 64 +X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), +) +Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + +def f(x, y): + return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2) - y ** 2) + + +x = np.linspace(-3, 3, 100) +y = np.linspace(-3, 3, 100) +X, Y = np.meshgrid(x, y) +Z = 0.5 * f(X, Y) + +fig = plt.figure(constrained_layout=True) +nrows, ncols = 1, 2 +w1, w2 = 20, 1 +gspec = gridspec.GridSpec(ncols=ncols, nrows=nrows, figure=fig, width_ratios=[w1, w2]) + +ax = plt.subplot(gspec[0, 0], aspect=1) +ax.set_xlim(0, 1) +ax.set_xticks(np.linspace(0, 1, 4 + 1)) +ax.set_xlabel("X Label") +ax.set_ylim(0, 1) +ax.set_yticks(np.linspace(0, 1, 4 + 1)) +ax.set_ylabel("Y Label") +ax.set_title("Title", family="Roboto", weight=500) +I = ax.imshow(Z, extent=[0, 1, 0, 1]) + +ax = plt.subplot(gspec[0, 1], aspect=w1) +plt.colorbar(I, cax=ax) + +plt.savefig("../../figures/layout/standard-layout-1.pdf") +plt.show() diff --git a/code/layout/standard-layout-2.py b/code/layout/standard-layout-2.py new file mode 100644 index 0000000..55236b6 --- /dev/null +++ b/code/layout/standard-layout-2.py @@ -0,0 +1,63 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + +p = plt.rcParams +p["figure.figsize"] = 6, 6 +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True + +X = np.random.normal(0.5, 0.15, 5000) +Y = np.random.normal(0.5, 0.15, 5000) + +fig = plt.figure(constrained_layout=True) +nrows, ncols, ratio = 2, 2, 5 +gspec = gridspec.GridSpec( + ncols=ncols, + nrows=nrows, + figure=fig, + height_ratios=[1, ratio], + width_ratios=[ratio, 1], +) + +ax = plt.subplot(gspec[1, 0]) +ax.scatter(X, Y, s=15, facecolor="black", linewidth=0, alpha=0.25) +ax.set_xlim(0, 1), ax.set_xticks(np.linspace(0, 1, 5)) +ax.set_ylim(0, 1), ax.set_yticks(np.linspace(0, 1, 5)) + +ax = plt.subplot(gspec[0, 0]) +ax.set_xlim(0, 1), ax.set_xticks(np.linspace(0, 1, 5)) +ax.set_xticklabels([]), ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.hist( + X, bins=np.linspace(0, 1, 21), facecolor="0.75", edgecolor="white", linewidth=0.5 +) + +ax = plt.subplot(gspec[1, 1]) +ax.set_ylim(0, 1), ax.set_yticks(np.linspace(0, 1, 5)) +ax.set_yticklabels([]), ax.set_xticks([]) +ax.spines["right"].set_visible(False) +ax.spines["bottom"].set_visible(False) +ax.spines["top"].set_visible(False) +H = ax.hist( + Y, + bins=np.linspace(0, 1, 21), + facecolor="0.75", + edgecolor="white", + orientation="horizontal", + linewidth=0.5, +) + +plt.savefig("../../figures/layout/standard-layout-2.pdf") +plt.show() diff --git a/code/optimization/line-benchmark.py b/code/optimization/line-benchmark.py new file mode 100644 index 0000000..03d73ae --- /dev/null +++ b/code/optimization/line-benchmark.py @@ -0,0 +1,49 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from timeit import default_timer as timer + +n_lines, n_points = 1_000, 2 +X = [np.random.uniform(0, 1, n_points) for i in range(n_lines)] +Y = [np.random.uniform(0, 1, n_points) for i in range(n_lines)] + +fig = plt.figure(figsize=(9, 3.5)) + +# ----------------------------- +ax = fig.add_subplot(1, 3, 1, aspect=1, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[]) +start = timer() +for x, y in zip(X, Y): + ax.plot(x, y, color="blue", alpha=0.1, linewidth=0.5) +end = timer() +ax.set_title("Individual plots: %.4fs" % (end - start)) + + +# ----------------------------- +ax = fig.add_subplot(1, 3, 2, aspect=1, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[]) +start = timer() +X_, Y_ = [], [] +for x, y in zip(X, Y): + X_.extend(x), X_.extend([None]) + Y_.extend(y), Y_.extend([None]) +ax.plot(X_, Y_, color="blue", alpha=0.1, linewidth=0.5) +end = timer() +ax.set_title("Unified plot: %.4fs" % (end - start)) + +# ----------------------------- +from matplotlib.collections import LineCollection + +ax = fig.add_subplot(1, 3, 3, aspect=1, xlim=[0, 1], xticks=[], ylim=[0, 1], yticks=[]) +start = timer() +V = [np.stack([x, y], axis=1) for x, y in zip(X, Y)] +lines = LineCollection(V, color="blue", alpha=0.1, linewidth=0.5) +ax.add_collection(lines) +end = timer() +ax.set_title("Line collection: %.4fs" % (end - start)) + +plt.tight_layout() +plt.savefig("../../figures/optimization/line-benchmark.png", dpi=600) +plt.show() diff --git a/code/optimization/multisample.py b/code/optimization/multisample.py new file mode 100644 index 0000000..fe82591 --- /dev/null +++ b/code/optimization/multisample.py @@ -0,0 +1,133 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate multisample on data or screen space (using imshow) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import LineCollection + +plt.rc("font", family="Roboto") + +fig = plt.figure(figsize=(2 * 4.25, 2 * 2.5 * 4.25 / 4), dpi=100) + +xmin, xmax = 0 * np.pi, 5 * np.pi +ymin, ymax = -1.1, +1.1 + + +def f(x): + return np.sin(np.power(x, 3)) * np.sin(x) + + +# --- X linear space ---------------------------------------------------------- +ax = plt.subplot( + 311, + xticks=[], + yticks=[], + aspect=1, + frameon=False, + xlim=[xmin, xmax], + ylim=[ymin, ymax], +) + +X = np.linspace(xmin, xmax, 10000) +Y = f(X) +plt.plot(X, Y, linewidth=0.5, alpha=0.25, color="black") +ax.set_title("Line plot", ha="left", loc="left") + +# --- X linear space ---------------------------------------------------------- +ax = plt.subplot( + 312, + xticks=[], + yticks=[], + aspect=1, + frameon=False, + xlim=[xmin, xmax], + ylim=[ymin, ymax], +) + +X = np.linspace(xmin, xmax, 10000) +Y = f(X) + +segments = np.zeros((len(X) - 1, 2, 2)) +segments[:, 0, 0], segments[:, 0, 1] = X[:-1], Y[:-1] +segments[:, 1, 0], segments[:, 1, 1] = X[1:], Y[1:] + +ax.add_collection(LineCollection(segments, linewidths=0.5, alpha=0.25, colors="black")) +ax.set_title("Line collection", ha="left", loc="left") + + +# --- Multisample (screen space) ---------------------------------------------- +n_samples = 8 +ax = plt.subplot( + 313, + xticks=[], + yticks=[], + aspect=1, + frameon=False, + xlim=[xmin, xmax], + ylim=[ymin, ymax], +) +x0, y0 = ax.transAxes.transform((0, 0)).astype(int) +x1, y1 = ax.transAxes.transform((1, 1)).astype(int) +rows, cols = y1 - y0, x1 - x0 +shape = n_samples * rows, n_samples * cols +Z = np.zeros(shape) + +I, J = np.meshgrid(np.arange(shape[1]), np.arange(shape[0])) +# N = np.random.uniform(-0.5,0.5,I.shape) +N = np.random.normal(0, 0.5, I.shape) +X = xmin + ((I + N) / (shape[1] - 1)) * (xmax - xmin) +Y = np.floor(((f(X) - ymin) / (ymax - ymin)) * shape[0]).astype(int) +Z[Y, I] = np.minimum(np.abs(Y - J), 1) +ax.imshow( + Z, + extent=[xmin, xmax, ymin, ymax], + origin="lower", + cmap="gray_r", + interpolation="lanczos", + vmin=0, + vmax=1.5, +) +ax.set_title("Multisample (imshow)", ha="left", loc="left") + + +plt.tight_layout() +plt.savefig("../../figures/optimization/multisample.png", dpi=600) +plt.show() + + +# # --- Multisample (data space) ------------------------------------------------ +# ax = plt.subplot(412, xticks=[], yticks=[], aspect=1, +# xlim=[xmin, xmax], ylim=[ymin, ymax]) + +# x0, y0 = ax.transAxes.transform((0,0)).astype(int) +# x1, y1 = ax.transAxes.transform((1,1)).astype(int) +# rows, cols = y1-y0, x1-x0 + +# X = np.linspace(0, cols-1, n_samples*n_samples*cols) #1000) +# #X += np.random.normal(0,.5) +# X = xmin + (X/(cols-1))*(xmax-xmin) +# Y = f(X) + + +# #dx = (xmax-xmin)/((x1-x0) * n_samples * n_samples) +# #for i in range(n_samples*n_samples): +# # X = np.linspace(xmin,xmax,1000) +# # Y = f(X+dx*i/n_samples) +# plt.plot(X,Y, linewidth=0.1, alpha=1.0, color="black") +# ax.set_title("Multisample (data space)") + + +# # --- X log space ------------------------------------------------------------- +# ax = plt.subplot(413, xticks=[], yticks=[], aspect=1, +# xlim=[xmin, xmax], ylim=[ymin, ymax]) + +# X = 1 - np.logspace(0,3,10000,endpoint=True)/1000 +# X = xmin + X*(xmax-xmin) +# Y = f(X) +# plt.plot(X,Y, linewidth=0.25, alpha=0.5, color="black") +# plt.plot(X,Y, linewidth=0.25, alpha=0.5, color="black") +# ax.set_title("X log space") diff --git a/code/optimization/multithread.py b/code/optimization/multithread.py new file mode 100644 index 0000000..6a59215 --- /dev/null +++ b/code/optimization/multithread.py @@ -0,0 +1,57 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.backends.backend_agg import FigureCanvas + +X = np.random.normal(4.5, 2, 5_000_000) +Y = np.random.normal(4.5, 2, 5_000_000) + + +def plot(extent): + """ Offline rendering """ + + xmin, xmax, ymin, ymax = extent + fig = Figure(figsize=(2, 2)) + canvas = FigureCanvas(fig) + ax = fig.add_axes( + [0, 0, 1, 1], + frameon=False, + xlim=[xmin, xmax], + xticks=[], + ylim=[ymin, ymax], + yticks=[], + ) + epsilon = 0.1 + I = np.argwhere( + (X >= (xmin - epsilon)) + & (X <= (xmax + epsilon)) + & (Y >= (ymin - epsilon)) + & (Y <= (ymax + epsilon)) + ) + ax.scatter( + X[I], Y[I], 3, clip_on=False, color="black", edgecolor="None", alpha=0.0025 + ) + canvas.draw() + return np.array(canvas.renderer.buffer_rgba()) + + +if __name__ == "__main__": + from multiprocessing import Pool + + extents = [[x, x + 3, y, y + 3] for x in range(0, 9, 3) for y in range(0, 9, 3)] + pool = Pool() + images = pool.map(plot, extents) + pool.close() + + fig = plt.figure(figsize=(6, 6)) + ax = plt.subplot(xlim=[0, 9], ylim=[0, 9]) + for img, extent in zip(images, extents): + ax.imshow(img, extent=extent, interpolation="None") + + plt.savefig("../../figures/optimization/multithread.png", dpi=600) + plt.show() diff --git a/code/optimization/scatter-benchmark.py b/code/optimization/scatter-benchmark.py new file mode 100644 index 0000000..4097c73 --- /dev/null +++ b/code/optimization/scatter-benchmark.py @@ -0,0 +1,46 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from timeit import default_timer as timer + +n_points = 100_000 +np.random.seed(1) +X = np.random.normal(0, 1, n_points) +Y = np.random.normal(0, 1, n_points) + +fig = plt.figure(figsize=(9, 3.5)) + +# ----------------------------- +ax = fig.add_subplot( + 1, 3, 2, aspect=1, xlim=[-5, 5], xticks=[], ylim=[-5, 5], yticks=[] +) +start = timer() +ax.scatter(X, Y, s=4, color="black", alpha=0.008, linewidth=0) +end = timer() +ax.set_title("Scatter: %.4fs" % (end - start)) + +# ----------------------------- +ax = fig.add_subplot( + 1, 3, 3, aspect=1, xlim=[-5, 5], xticks=[], ylim=[-5, 5], yticks=[] +) +start = timer() +ax.plot(X, Y, "o", markersize=2, color="black", alpha=0.008, markeredgewidth=0) +end = timer() +ax.set_title("Plot: %.4fs" % (end - start)) + +# ----------------------------- +ax = fig.add_subplot( + 1, 3, 1, aspect=1, xlim=[-5, 5], xticks=[], ylim=[-5, 5], yticks=[] +) +start = timer() +ax.hist2d(X, Y, 128, cmap="gray_r") +end = timer() +ax.set_title("Hist2d: %.4fs" % (end - start)) + +plt.tight_layout() +plt.savefig("../../figures/optimization/scatter-benchmark.png", dpi=600) +plt.show() diff --git a/code/optimization/scatters.py b/code/optimization/scatters.py new file mode 100644 index 0000000..b5da809 --- /dev/null +++ b/code/optimization/scatters.py @@ -0,0 +1,65 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +n = [10_000, 100_000, 1_000_000] +alpha = [0.2, 0.02, 0.002] +fig = plt.figure(figsize=(9, 9)) + +index = 1 +for i in range(3): + + X = np.random.normal(0, 2, n[i]) + Y = np.random.normal(0, 2, n[i]) + + ax = plt.subplot(3, 3, index, aspect=1) + ax.scatter( + X, Y, 5, facecolor="black", edgecolor="None", alpha=alpha[i], antialiased=True + ) + ax.set_xlim(-5, 5), ax.set_xticks([]) + ax.set_ylim(-5, 5), ax.set_yticks([]) + ax.text( + -5, + 4.75, + " scatter, n={:,}, alpha={:.3f}".format(n[i], alpha[i]), + ha="left", + va="top", + size="small", + ) + index += 1 + + ax = plt.subplot(3, 3, index, aspect=1) + ax.hist2d(X, Y, 128, cmap="gray_r") + ax.set_xlim(-5, 5), ax.set_xticks([]) + ax.set_ylim(-5, 5), ax.set_yticks([]) + ax.text( + -5, + 4.75, + " hist2d (128x128 bins), n={:,}".format(n[i]), + ha="left", + va="top", + size="small", + ) + index += 1 + + ax = plt.subplot(3, 3, index, aspect=1) + ax.hexbin(X, Y, gridsize=64, cmap="gray_r", linewidth=0, antialiased=0) + ax.set_xlim(-5, 5), ax.set_xticks([]) + ax.set_ylim(-5, 5), ax.set_yticks([]) + ax.text( + -5, + 4.75, + " hexbin (64x64 bins), n={:,}".format(n[i]), + ha="left", + va="top", + size="small", + ) + index += 1 + +plt.tight_layout() +plt.savefig("../../figures/optimization/scatters.png", dpi=600) +plt.show() diff --git a/code/optimization/self-cover.py b/code/optimization/self-cover.py new file mode 100644 index 0000000..6cb73dd --- /dev/null +++ b/code/optimization/self-cover.py @@ -0,0 +1,56 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.path import Path +import matplotlib.patches as patches + +fig = plt.figure(figsize=(8, 1.5)) + +n = 3 +X = np.append(np.linspace(0, n * 2 * np.pi, 500), [0]) +Y = np.sin(X) + +ax = plt.subplot( + 1, + 2, + 1, + frameon=False, + xlim=(0, n * 2 * np.pi + 1), + xticks=[], + ylim=(-1, 1.1), + yticks=[], +) +ax.plot(X, Y, "C0", linewidth=8, alpha=0.5, solid_capstyle="round", clip_on=False) +ax.set_title("No self covering") + +ax = plt.subplot( + 1, + 2, + 2, + frameon=False, + xlim=(0, n * 2 * np.pi), + xticks=[], + ylim=(-1, 1.1), + yticks=[], +) +ax.plot( + X[:-1], Y[:-1], "C0", linewidth=8, alpha=0.5, solid_capstyle="round", clip_on=False +) +ax.plot( + [X[-2], X[-1]], + [Y[-2], Y[-1]], + "C0", + linewidth=8, + alpha=0.5, + solid_capstyle="round", + clip_on=False, +) +ax.set_title("Simulated self covering") + +plt.tight_layout() +plt.savefig("../../figures/optimization/self-cover.pdf") +plt.show() diff --git a/code/optimization/transparency.py b/code/optimization/transparency.py new file mode 100644 index 0000000..b3801b2 --- /dev/null +++ b/code/optimization/transparency.py @@ -0,0 +1,60 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(7, 1.25)) +ax = plt.subplot(frameon=False) +ax.set_xlim(0.5, 7.5), ax.set_xticks([]) +ax.set_ylim(0, 1.5), ax.set_yticks([]) + + +X, Y = [1], [1] +alpha = 1 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point\nalpha={:.2f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [2], [1] +alpha = 0.1 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [3] * 10, [1] * 10 +alpha = 0.1 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point(s)\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [4] * 25, [1] * 25 +alpha = 0.1 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point(s)\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [5] * 10, [1] * 10 +alpha = 0.002 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point(s)\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [6] * 64, [1] * 64 +alpha = 0.002 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point(s)\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + +X, Y = [7] * 512, [1] * 512 +alpha = 0.002 +ax.scatter(X, Y, 256, marker="o", facecolor="black", edgecolor="None", alpha=alpha) +text = "{:d} point(s)\nalpha={:.3f}".format(len(X), alpha) +ax.text(X[0], 0.75, text, ha="center", va="top", size="small") + + +plt.tight_layout() +plt.savefig("../../figures/optimization/transparency.pdf") +plt.show() diff --git a/code/ornaments/annotate-regular.py b/code/ornaments/annotate-regular.py new file mode 100644 index 0000000..7ead5ae --- /dev/null +++ b/code/ornaments/annotate-regular.py @@ -0,0 +1,65 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(6, 4)) +ax = plt.subplot( + 1, + 1, + 1, + xlim=[-1, +1], + xticks=[-1, 0, 1], + ylim=[-1, +1], + yticks=[-1, 0, 1], + aspect=1, +) + +np.random.seed(123) +X = np.random.normal(0, 0.35, 1000) +Y = np.random.normal(0, 0.35, 1000) + +ax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + + +I = np.random.choice(len(X), size=8, replace=False) +Px, Py = X[I], Y[I] +I = np.argsort(Y[I])[::-1] +Px, Py = Px[I], Py[I] + +ax.scatter(Px, Py, edgecolor="black", facecolor="white", zorder=20) +ax.scatter(Px, Py, edgecolor="None", facecolor="C1", alpha=0.5, zorder=30) + +y, dy = 1.0, 0.125 +style = "arc,angleA=-0,angleB=0,armA=-100,armB=0,rad=0" + +for i in range(len(I)): + ax.annotate( + "Point " + chr(ord("A") + i), + xy=(Px[i], Py[i]), + xycoords="data", + xytext=(1.25, y - i * dy), + textcoords="data", + arrowprops=dict( + arrowstyle="->", + color="black", + linewidth=0.75, + shrinkA=25, + shrinkB=5, + patchA=None, + patchB=None, + connectionstyle=style, + ), + ) + +ax.spines["right"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["left"].set_position(("data", -1.1)) +ax.spines["bottom"].set_position(("data", -1.1)) + +# plt.tight_layout() +plt.savefig("../../figures/ornaments/legend-regular.pdf") +plt.show() diff --git a/code/ornaments/annotation-direct.py b/code/ornaments/annotation-direct.py new file mode 100644 index 0000000..f0a4917 --- /dev/null +++ b/code/ornaments/annotation-direct.py @@ -0,0 +1,52 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patheffects as path_effects + +fig = plt.figure(figsize=(4, 4)) +ax = plt.subplot(1, 1, 1, xlim=[-1, +1], xticks=[], ylim=[-1, +1], yticks=[], aspect=1) + +np.random.seed(123) +X = np.random.normal(0, 0.35, 1000) +Y = np.random.normal(0, 0.35, 1000) + +ax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + +I = np.random.choice(len(X), size=5, replace=False) +Px, Py = X[I], Y[I] +I = np.argsort(Y[I])[::-1] +Px, Py = Px[I], Py[I] + +ax.scatter(Px, Py, edgecolor="black", facecolor="white", zorder=20) +ax.scatter(Px, Py, edgecolor="None", facecolor="C1", alpha=0.5, zorder=30) + +y, dy = 1.0, 0.125 +style = "arc,angleA=-0,angleB=0,armA=-100,armB=0,rad=0" + +for i in range(len(I)): + text = ax.annotate( + "Point " + chr(ord("A") + i), + xy=(Px[i], Py[i]), + xycoords="data", + xytext=(0, 18), + textcoords="offset points", + ha="center", + size="small", + arrowprops=dict( + arrowstyle="->", shrinkA=0, shrinkB=5, color="black", linewidth=0.75 + ), + ) + text.set_path_effects( + [path_effects.Stroke(linewidth=2, foreground="white"), path_effects.Normal()] + ) + text.arrow_patch.set_path_effects( + [path_effects.Stroke(linewidth=2, foreground="white"), path_effects.Normal()] + ) + +plt.tight_layout() +plt.savefig("../../figures/ornaments/annotation-direct.pdf") +plt.show() diff --git a/code/ornaments/annotation-side.py b/code/ornaments/annotation-side.py new file mode 100644 index 0000000..a034759 --- /dev/null +++ b/code/ornaments/annotation-side.py @@ -0,0 +1,55 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patheffects as path_effects + + +fig = plt.figure(figsize=(5.5, 4)) +ax = plt.subplot(1, 1, 1, xlim=[-1, +1], xticks=[], ylim=[-1, +1], yticks=[], aspect=1) + +np.random.seed(123) +X = np.random.normal(0, 0.35, 1000) +Y = np.random.normal(0, 0.35, 1000) + +ax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + +I = np.random.choice(len(X), size=5, replace=False) +Px, Py = X[I], Y[I] +I = np.argsort(Y[I])[::-1] +Px, Py = Px[I], Py[I] + +ax.scatter(Px, Py, edgecolor="black", facecolor="white", zorder=20) +ax.scatter(Px, Py, edgecolor="None", facecolor="C1", alpha=0.5, zorder=30) + +y, dy = 0.25, 0.125 +style = "arc,angleA=-0,angleB=0,armA=-100,armB=0,rad=0" +for i in range(len(I)): + text = ax.annotate( + "Point " + chr(ord("A") + i), + xy=(Px[i], Py[i]), + xycoords="data", + xytext=(1.25, y - i * dy), + textcoords="data", + arrowprops=dict( + arrowstyle="->", + color="black", + linewidth=0.75, + shrinkA=20, + shrinkB=5, + patchA=None, + patchB=None, + connectionstyle=style, + ), + ) + text.arrow_patch.set_path_effects( + [path_effects.Stroke(linewidth=2, foreground="white"), path_effects.Normal()] + ) + + +plt.tight_layout() +plt.savefig("../../figures/ornaments/annotation-side.pdf") +plt.show() diff --git a/code/ornaments/annotation-zoom.py b/code/ornaments/annotation-zoom.py new file mode 100644 index 0000000..75d5c23 --- /dev/null +++ b/code/ornaments/annotation-zoom.py @@ -0,0 +1,84 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +from matplotlib.patches import Rectangle +from matplotlib.patches import ConnectionPatch + + +fig = plt.figure(figsize=(5, 4)) + +n = 5 +gs = GridSpec(n, n + 1) + +ax = plt.subplot( + gs[:n, :n], xlim=[-1, +1], xticks=[], ylim=[-1, +1], yticks=[], aspect=1 +) + +np.random.seed(123) +X = np.random.normal(0, 0.35, 1000) +Y = np.random.normal(0, 0.35, 1000) +ax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + +I = np.random.choice(len(X), size=n, replace=False) +Px, Py = X[I], Y[I] +I = np.argsort(Y[I])[::-1] +Px, Py = Px[I], Py[I] + +ax.scatter(Px, Py, edgecolor="black", facecolor="None", linewidth=0.75) + +dx, dy = 0.075, 0.075 +for i, (x, y) in enumerate(zip(Px, Py)): + sax = plt.subplot( + gs[i, n], + xlim=[x - dx, x + dx], + xticks=[], + ylim=[y - dy, y + dy], + yticks=[], + aspect=1, + ) + sax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + sax.scatter(Px, Py, edgecolor="black", facecolor="None", linewidth=0.75) + + sax.text( + 1.1, + 0.5, + "Point " + chr(ord("A") + i), + rotation=90, + size=8, + ha="left", + va="center", + transform=sax.transAxes, + ) + + rect = Rectangle( + (x - dx, y - dy), + 2 * dx, + 2 * dy, + edgecolor="black", + facecolor="None", + linestyle="--", + linewidth=0.75, + ) + ax.add_patch(rect) + + con = ConnectionPatch( + xyA=(x, y), + coordsA=ax.transData, + xyB=(0, 0.5), + coordsB=sax.transAxes, + linestyle="--", + linewidth=0.75, + patchA=rect, + arrowstyle="->", + ) + fig.add_artist(con) + + +plt.tight_layout() +plt.savefig("../../figures/ornaments/annotation-zoom.pdf") +plt.show() diff --git a/code/ornaments/bessel-functions.py b/code/ornaments/bessel-functions.py new file mode 100644 index 0000000..3616e09 --- /dev/null +++ b/code/ornaments/bessel-functions.py @@ -0,0 +1,110 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +from scipy.special import jn, jn_zeros +import matplotlib.pyplot as plt + +plt.rc("font", family="Roboto Condensed") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") + +fig = plt.figure(figsize=(6, 3), dpi=100) +ax = plt.subplot(xlim=[0, 20], ylim=[-0.5, 1]) + +X = np.linspace(0, 20, 1000, endpoint=True) +n = 6 +for i in range(n): + Ji = jn(i, X) + linewidth = 1.5 if i == 0 else 1 + linestyle = "-" if i == 0 else "-" + color = "C1" if i == 0 else "%.2f" % (i / n) + label = r"$J_%d$" % i + + ax.plot(X, Ji, color="white", clip_on=False, zorder=10 - i, linewidth=2.5) + + ax.plot( + X, + Ji, + color=color, + clip_on=False, + zorder=10 - i, + linewidth=linewidth, + linestyle=linestyle, + label=label, + ) + + k = np.argmax(Ji) + ax.text( + X[k], + Ji[k] + 0.05, + label, + color=color, + usetex=True, + ha="center", + va="bottom", + size="small", + ) + # k = np.argmin(Ji) + # ax.text(X[k], Ji[k]-0.05, label, color=color, + # ha="center", va="top", size="small") + + Zx = [x for x in jn_zeros(i, 6) if x < 20] + Zy = np.zeros(len(Zx)) + ax.scatter(Zx, Zy, s=15, zorder=20, edgecolor=color, facecolor="white", linewidth=1) + + if i == 0: + ax.annotate( + "Root", + (Zx[0], Zy[0]), + size="small", + xytext=(-30, -30), + textcoords="offset points", + arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=-0.3"), + ) + + Zy = -0.6 * np.ones(len(Zx)) + ax.scatter( + Zx, + Zy, + s=30, + zorder=20, + clip_on=False, + marker="|", + facecolor="black", + linewidth=0.5, + ) + +ax.set_title( + "Bessel functions", + x=1, + weight="light", + family="Roboto Condensed", + transform=ax.transAxes, + ha="right", +) +ax.text( + 1, + 0.98, + r"$J_n(x) = \frac{1}{2\pi} \int_{-\pi}^\pi e^{i(x \sin \tau -n \tau)} \,d\tau$", + va="top", + transform=ax.transAxes, + size=12, + ha="right", + usetex=True, +) + +ax.set_yticks([-0.5, 0, 0.5, 1]) +ax.set_xticks([0, 10, 20]) +ax.axhline(0, color="0.5", linewidth=0.5) + +ax.spines["right"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["left"].set_position(("data", -1)) +ax.spines["bottom"].set_position(("data", -0.6)) + +plt.tight_layout() +plt.savefig("../../figures/ornaments/bessel-functions.pdf") +plt.show() diff --git a/code/ornaments/elegant-scatter.py b/code/ornaments/elegant-scatter.py new file mode 100644 index 0000000..297c209 --- /dev/null +++ b/code/ornaments/elegant-scatter.py @@ -0,0 +1,87 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +plt.rcParams["font.size"] = 10.0 +plt.rcParams["font.serif"] = ["Source Serif Pro"] +plt.rcParams["font.sans-serif"] = ["Source Sans Pro"] +plt.rcParams["font.monospace"] = ["Source Code Pro"] + + +fig = plt.figure(figsize=(5, 5)) +ax = plt.axes() + +ax.set_xlabel("Height (m)", weight="medium") +ax.set_ylabel("Weight (kg)", weight="medium") +ax.set_title( + """Distribution of height & weight\n""" """according to sex & age (fake data)""", + family="serif", +) + +n = 250 +np.random.seed(1) +X, Y = np.zeros(2 * n), np.zeros(2 * n) +S, C = np.zeros(2 * n), np.zeros(2 * n) + +X[:n] = np.random.normal(1.60, 0.1, n) +Y[:n] = np.random.normal(50, 10, n) +S[:n] = np.random.uniform(25, 50, n) +C[:n] = 0 + +X[n:] = np.random.normal(1.75, 0.1, n) +Y[n:] = np.random.normal(75, 10, n) +S[n:] = np.random.uniform(25, 50, n) +C[n:] = 1 + +cmap = plt.get_cmap("RdYlBu") +scatter = plt.scatter(X, Y, s=S, edgecolor="black", linewidth=0.75, zorder=-20) +scatter = plt.scatter(X, Y, s=S, edgecolor="None", facecolor="white", zorder=-10) +scatter = plt.scatter(X, Y, c=C, s=S, cmap=cmap, edgecolor="None", alpha=0.25) + +handles, labels = scatter.legend_elements( + num=3, + prop="sizes", + alpha=1, + markeredgewidth=0.5, + markeredgecolor="black", + markerfacecolor="None", +) +legend = plt.legend( + handles, + labels, + title=" Age", + loc=(0.6, 0.05), + handletextpad=0.1, + labelspacing=0.25, + facecolor="None", + edgecolor="None", +) +legend.get_children()[0].align = "left" +legend.get_title().set_fontweight("medium") +ax.add_artist(legend) + +handles, labels = scatter.legend_elements(markeredgewidth=0.0, markeredgecolor="black") +labels = ["Female", "Male"] +legend = plt.legend( + handles, + labels, + title=" Sex", + loc=(0.75, 0.05), + handletextpad=0.1, + labelspacing=0.25, + facecolor="None", + edgecolor="None", +) +legend.get_children()[0].align = "left" +legend.get_title().set_fontweight("medium") + + +plt.scatter(X, [19] * len(X), marker="|", color=cmap(C), linewidth=0.5, alpha=0.25) +plt.scatter([1.3] * len(X), Y, marker="_", color=cmap(C), linewidth=0.5, alpha=0.25) + +plt.savefig("../../figures/ornaments/elegant-scatter.pdf") +plt.show() diff --git a/code/ornaments/label-alternatives.py b/code/ornaments/label-alternatives.py new file mode 100644 index 0000000..b0552ca --- /dev/null +++ b/code/ornaments/label-alternatives.py @@ -0,0 +1,70 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(6, 6)) + +n = 5 +for i in range(n): + index = i + 1 + ax = plt.subplot( + n, + 1, + index, + facecolor="None", + xlim=[0, 10.5], + xticks=[], + ylim=[0, 10], + yticks=[], + ) + + for j in range(5): + ax.plot( + [0, 10], + [1 + j * 1.5, 1 + j * 1.5], + clip_on=False, + linewidth=5, + color=".9", + solid_capstyle="round", + ) + + x = np.random.uniform(1, 9) + ax.plot( + [0, x], + [1 + j * 1.5, 1 + j * 1.5], + clip_on=False, + linewidth=5, + color="C1", + solid_capstyle="round", + ) + + ax.text( + 10.5, + 0, + " %d " % (2000 + index), + font="Roboto Condensed", + weight="medium", + color="black", + ha="center", + va="center", + size="small", + bbox=dict( + facecolor="white", + edgecolor="black", + linewidth=0.75, + boxstyle="round,pad=0.25", + ), + ) + + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["left"].set_visible(False) + # ax.spines['bottom'].set_position(('data', -1.25)) + +plt.tight_layout() +plt.savefig("../../figures/ornaments/legend-regular.pdf") +plt.show() diff --git a/code/ornaments/latex-text-box.py b/code/ornaments/latex-text-box.py new file mode 100644 index 0000000..d05a38d --- /dev/null +++ b/code/ornaments/latex-text-box.py @@ -0,0 +1,134 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +import matplotlib.patches as patches +import matplotlib.patheffects as path_effects +from mpl_toolkits.axes_grid1 import make_axes_locatable + + +rows, cols = 2, 3 +fig = plt.figure(figsize=(cols * 2, rows * 2.45)) + +for row in range(rows): + for col in range(cols): + index = row * cols + col + 1 + + if index == cols * rows: + ax = plt.subplot(rows, cols, index, aspect=1, frameon=False) + ax.set_xticks([]), ax.set_yticks([]) + + plt.rcParams.update( + {"font.family": "sans-serif", "font.sans-serif": ["Helvetica"]} + ) + + ax.text( + 0, + 1.06, + r"\begin{minipage}{4.3cm}\small " + r"\textbf{Figure caption} " + r"can be directly inserted from within matplotlib using the full \LaTeX~engine. To fit the axes, you can use a minipage and adjust its width to the axes. Of course, the width can be also computed automatically but from time to time it is ok to do it by trials and errors (I did). Fun part is that we could also include some tikz rendering inside the figure. " + r"\end{minipage}\vspace{-5mm}", + va="top", + transform=ax.transAxes, + clip_on=False, + usetex=True, + ) + continue + + ax = plt.subplot(rows, cols, index, aspect=1, frameon=True) + ax.tick_params(which="both", direction="in") + ax.tick_params(which="both", right=True) + ax.set_axisbelow(True) + + ax.set_xlim(0, 10) + ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) + ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + ax.set_xticklabels([]) + + ax.set_ylim(0, 10) + ax.yaxis.set_major_locator(ticker.MultipleLocator(1.00)) + ax.yaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + ax.set_yticklabels([]) + + ax.grid(color=".9", linestyle="--") + + n = 50 + x0, y0 = np.random.uniform(2, 8, 2) + s = np.random.uniform(0.5, 2.0) + X = np.random.normal(x0, s, n) + Y = np.random.normal(y0, s, n) + S = np.random.uniform(100, 200, n) + ax.scatter(X, Y, alpha=0.5, facecolor="C0", edgecolor="None") + + Xm, Ym = X.mean(), Y.mean() + ax.axvline(Xm, linewidth=0.5, color="black") + text = ax.text( + Xm, + 9.5, + "%.1f" % Xm, + transform=ax.transData, + size=7, + rotation=90, + ha="center", + va="top", + ) + text.set_path_effects( + [ + path_effects.Stroke(linewidth=2, foreground="white"), + path_effects.Normal(), + ] + ) + + ax.axhline(Ym, linewidth=0.5, color="black") + text = ax.text( + 9.5, + Ym, + "%.1f" % Ym, + transform=ax.transData, + size=7, + rotation=0, + ha="right", + va="center", + ) + text.set_path_effects( + [ + path_effects.Stroke(linewidth=2, foreground="white"), + path_effects.Normal(), + ] + ) + + ax.scatter([Xm], [Ym], s=10, facecolor="black", edgecolor="None") + + divider = make_axes_locatable(ax) + cax = divider.append_axes("top", size="15%", pad=0) + cax.get_xaxis().set_visible(False) + cax.get_yaxis().set_visible(False) + cax.set_facecolor("black") + cax.text( + 0.05, + 0.45, + "SESSION %2d" % index, + size=10, + color="white", + ha="left", + va="center", + ) + cax.text( + 0.95, + 0.45, + "σ=%.1f" % s, + size=10, + color="white", + ha="right", + va="center", + weight="bold", + ) + +plt.tight_layout() +plt.savefig("../../figures/ornaments/latex-text-box.png", dpi=600) +plt.show() diff --git a/code/ornaments/legend-alternatives.py b/code/ornaments/legend-alternatives.py new file mode 100644 index 0000000..5b63b63 --- /dev/null +++ b/code/ornaments/legend-alternatives.py @@ -0,0 +1,161 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +def plot(ax): + ax.set_xlim([-np.pi, np.pi]) + ax.set_xticks([-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi]) + ax.set_xticklabels(["-π", "-π/2", "0", "+π/2", "+π"]) + ax.set_ylim([-1, 1]) + ax.set_yticks([-1, 0, 1]) + ax.set_yticklabels(["-1", "0", "+1"]) + + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["left"].set_position(("data", -3.25)) + ax.spines["bottom"].set_position(("data", -1.25)) + + (plot1,) = ax.plot(X, C, label="cosine", clip_on=False) + (plot2,) = ax.plot(X, S, label="sine", clip_on=False) + + return plot1, plot2 + + +n = 4 +fig = plt.figure(figsize=(6, n * 1.9)) + +X = np.linspace(-np.pi, np.pi, 400, endpoint=True) +C, S = np.cos(X), np.sin(X) + +ax = plt.subplot(n, 1, 1) +plot1, plot2 = plot(ax) +ax.text( + X[-1], + C[-1], + " — " + plot1.get_label(), + size="small", + color=plot1.get_color(), + ha="left", + va="center", +) +ax.text( + X[-1], + S[-1], + " — " + plot2.get_label(), + size="small", + color=plot2.get_color(), + ha="left", + va="center", +) + +ax = plt.subplot(n, 1, 2) +plot1, plot2 = plot(ax) +ax.text( + X[100], + C[100], + " " + plot1.get_label(), + family="Roboto Condensed", + size="small", + bbox=dict(facecolor="white", edgecolor="None", alpha=0.85), + color=plot1.get_color(), + ha="center", + va="center", + rotation=42.5, +) +ax.text( + X[200], + S[200], + " " + plot2.get_label(), + rotation=42.5, + family="Roboto Condensed", + size="small", + bbox=dict(facecolor="white", edgecolor="None", alpha=0.85), + color=plot2.get_color(), + ha="center", + va="center", +) + +ax = plt.subplot(n, 1, 3) +plot1, plot2 = plot(ax) + +ax.annotate( + "cosine", + (X[100], C[100]), + size="small", + color=plot1.get_color(), + xytext=(-50, +10), + textcoords="offset points", + arrowprops=dict( + arrowstyle="->", color=plot1.get_color(), connectionstyle="arc3,rad=-0.3" + ), +) +ax.annotate( + "sine", + (X[200], S[200]), + size="small", + color=plot2.get_color(), + xytext=(-50, +10), + textcoords="offset points", + arrowprops=dict( + arrowstyle="->", color=plot2.get_color(), connectionstyle="arc3,rad=-0.3" + ), +) + +ax = plt.subplot(n, 1, 4) +plot1, plot2 = plot(ax) +index = 10 +ax.scatter( + [X[index]], + [C[index]], + s=100, + marker="o", + zorder=10, + clip_on=False, + linewidth=1, + edgecolor=plot1.get_color(), + facecolor="white", +) +ax.text( + X[index], + 1.01 * C[index], + "A", + zorder=20, + color=plot1.get_color(), + ha="center", + va="center", + size="x-small", + clip_on=False, +) + +ax.scatter( + [X[index]], + [S[index]], + s=100, + marker="o", + zorder=10, + clip_on=False, + linewidth=1, + edgecolor=plot2.get_color(), + facecolor="white", +) +ax.text( + X[index], + 1.05 * S[index], + "B", + zorder=20, + color=plot2.get_color(), + ha="center", + va="center", + size="x-small", + clip_on=False, +) + + +plt.tight_layout() +plt.savefig("../../figures/ornaments/legend-alternatives.pdf") +plt.show() diff --git a/code/ornaments/legend-regular.py b/code/ornaments/legend-regular.py new file mode 100644 index 0000000..ff9b022 --- /dev/null +++ b/code/ornaments/legend-regular.py @@ -0,0 +1,33 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(6, 2.5)) +ax = plt.subplot( + xlim=[-np.pi, np.pi], + xticks=[-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi], + xticklabels=["-π", "-π/2", "0", "+π/2", "+π"], + ylim=[-1, 1], + yticks=[-1, 0, 1], + yticklabels=["-1", "0", "+1"], +) + +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) + +ax.plot(X, C, label="cosine", clip_on=False) +ax.plot(X, S, label="sine", clip_on=False) + +ax.spines["right"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["left"].set_position(("data", -3.25)) +ax.spines["bottom"].set_position(("data", -1.25)) +ax.legend(edgecolor="None") + +plt.tight_layout() +plt.savefig("../../figures/ornaments/legend-regular.pdf") +plt.show() diff --git a/code/ornaments/title-regular.py b/code/ornaments/title-regular.py new file mode 100644 index 0000000..ecad9d8 --- /dev/null +++ b/code/ornaments/title-regular.py @@ -0,0 +1,47 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +fig = plt.figure(figsize=(6, 3)) +ax = plt.subplot( + xlim=[-np.pi, np.pi], + xticks=[-np.pi, -np.pi / 2, np.pi / 2, np.pi], + xticklabels=["-π", "-π/2", "+π/2", "+π"], + ylim=[-1, 1], + yticks=[-1, 1], + yticklabels=["-1", "+1"], +) + +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) + +ax.plot(X, C, label="cosine", clip_on=False) +ax.plot(X, S, label="sine", clip_on=False) + +ax.spines["right"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.spines["left"].set_position(("data", -3.25)) +ax.spines["bottom"].set_position(("data", -1.25)) +ax.legend( + edgecolor="None", + ncol=2, + loc="upper right", + bbox_to_anchor=(1.01, 1.225), + borderaxespad=0, +) +ax.set_title("Trigonometric functions", x=1, y=1.2, ha="right") + +ax.set_xlabel("Angle", va="center", weight="bold") +ax.xaxis.set_label_coords(0.5, -0.25) + +ax.set_ylabel("Value", ha="center", weight="bold") +ax.yaxis.set_label_coords(-0.025, 0.5) + + +plt.tight_layout() +plt.savefig("../../figures/ornaments/title-regular.pdf") +plt.show() diff --git a/code/reference/axes-adjustment.py b/code/reference/axes-adjustment.py new file mode 100644 index 0000000..4128844 --- /dev/null +++ b/code/reference/axes-adjustment.py @@ -0,0 +1,213 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from matplotlib.collections import PatchCollection + + +fig = plt.figure(figsize=(4.25, 4.25 * 95 / 115)) +ax = fig.add_axes( + [0, 0, 1, 1], + frameon=False, + aspect=1, + xlim=(0 - 5, 100 + 10), + ylim=(-10, 80 + 5), + xticks=[], + yticks=[], +) + + +box = mpatches.FancyBboxPatch( + (0, 0), + 100, + 83, + mpatches.BoxStyle("Round", pad=0, rounding_size=2), + linewidth=1.0, + facecolor="0.9", + edgecolor="black", +) +ax.add_artist(box) + +box = mpatches.FancyBboxPatch( + (0, 0), + 100, + 75, + mpatches.BoxStyle("Round", pad=0, rounding_size=0), + linewidth=1.0, + facecolor="white", + edgecolor="black", +) +ax.add_artist(box) + + +box = mpatches.Rectangle( + (5, 5), 45, 30, zorder=10, linewidth=1.0, facecolor="white", edgecolor="black" +) +ax.add_artist(box) + +box = mpatches.Rectangle( + (5, 40), 45, 30, zorder=10, linewidth=1.0, facecolor="white", edgecolor="black" +) +ax.add_artist(box) + +box = mpatches.Rectangle( + (55, 5), 40, 65, zorder=10, linewidth=1.0, facecolor="white", edgecolor="black" +) +ax.add_artist(box) + +# Window button +X, Y = [5, 10, 15], [79, 79, 79] +plt.scatter(X, Y, s=75, zorder=10, edgecolor="black", facecolor="white", linewidth=1) + + +# Window size extension +X, Y = [0, 0], [0, -8] +plt.plot(X, Y, color="black", linestyle=":", linewidth=1, clip_on=False) + +X, Y = [100, 100], [0, -8] +plt.plot(X, Y, color="black", linestyle=":", linewidth=1, clip_on=False) + +X, Y = [100, 108], [0, 0] +plt.plot(X, Y, color="black", linestyle=":", linewidth=1, clip_on=False) + +X, Y = [100, 108], [75, 75] +plt.plot(X, Y, color="black", linestyle=":", linewidth=1, clip_on=False) + + +def ext_arrow(p0, p1, p2, p3): + p0, p1 = np.asarray(p0), np.asarray(p1) + p2, p3 = np.asarray(p2), np.asarray(p3) + ax.arrow( + *p0, + *(p1 - p0), + zorder=20, + linewidth=0, + length_includes_head=True, + width=0.4, + head_width=2, + head_length=2, + color="black" + ) + ax.arrow( + *p3, + *(p2 - p3), + zorder=20, + linewidth=0, + length_includes_head=True, + width=0.4, + head_width=2, + head_length=2, + color="black" + ) + plt.plot([p1[0], p2[0]], [p1[1], p2[1]], linewidth=0.9, color="black") + + +def int_arrow(p0, p1): + p0, p1 = np.asarray(p0), np.asarray(p1) + ax.arrow( + *((p0 + p1) / 2), + *((p1 - p0) / 2), + zorder=20, + linewidth=0, + length_includes_head=True, + width=0.4, + head_width=2, + head_length=2, + color="black" + ) + ax.arrow( + *((p0 + p1) / 2), + *(-(p1 - p0) / 2), + zorder=20, + linewidth=0, + length_includes_head=True, + width=0.4, + head_width=2, + head_length=2, + color="black" + ) + + +x = 0 +y = 10 +ext_arrow((x - 4, y), (x, y), (x + 5, y), (x + 9, y)) +ax.text(x + 9.5, y, "left", ha="left", va="center", size="x-small", zorder=20) + +x += 50 +ext_arrow((x - 4, y), (x, y), (x + 5, y), (x + 9, y)) +ax.text(x - 4.5, y, "wspace", ha="right", va="center", size="x-small", zorder=20) + +x += 45 +ext_arrow((x - 4, y), (x, y), (x + 5, y), (x + 9, y)) +ax.text(x - 4.5, y, "right", ha="right", va="center", size="x-small", zorder=20) + +y = 0 +x = 25 +ext_arrow((x, y - 4), (x, y), (x, y + 5), (x, y + 9)) +ax.text(x, y + 9.5, "bottom", ha="center", va="bottom", size="x-small", zorder=20) + +y += 35 +ext_arrow((x, y - 4), (x, y), (x, y + 5), (x, y + 9)) +ax.text(x, y - 4.5, "hspace", ha="center", va="top", size="x-small", zorder=20) + +y += 35 +ext_arrow((x, y - 4), (x, y), (x, y + 5), (x, y + 9)) +ax.text(x, y - 4.5, "top", ha="center", va="top", size="x-small", zorder=20) + +int_arrow((0, -5), (100, -5)) +ax.text( + 50, + -5, + "figure width", + backgroundcolor="white", + zorder=30, + ha="center", + va="center", + size="x-small", +) + +int_arrow((105, 0), (105, 75)) +ax.text( + 105, + 75 / 2, + "figure height", + backgroundcolor="white", + zorder=30, + rotation="vertical", + ha="center", + va="center", + size="x-small", +) + +int_arrow((55, 62.5), (95, 62.5)) +ax.text( + 75, + 62.5, + "axes width", + backgroundcolor="white", + zorder=30, + ha="center", + va="center", + size="x-small", +) + +int_arrow((62.5, 5), (62.5, 70)) +ax.text( + 62.5, + 35, + "axes height", + backgroundcolor="white", + zorder=30, + rotation="vertical", + ha="center", + va="center", + size="x-small", +) + + +plt.savefig("reference-axes-adjustment.pdf", dpi=600) +plt.show() diff --git a/code/reference/collection.py b/code/reference/collection.py new file mode 100644 index 0000000..df5016c --- /dev/null +++ b/code/reference/collection.py @@ -0,0 +1,337 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.path as mpath +from matplotlib.collections import LineCollection +from matplotlib.collections import PathCollection +from matplotlib.collections import CircleCollection +from matplotlib.collections import PolyCollection +from matplotlib.collections import EllipseCollection +from matplotlib.collections import RegularPolyCollection +from matplotlib.collections import StarPolygonCollection +from matplotlib.collections import AsteriskPolygonCollection + + +fig = plt.figure(figsize=(4.25, 8 * 0.4)) +ax = fig.add_axes( + [0, 0, 1, 1], + xlim=[0, 11], + ylim=[0.5, 8.5], + frameon=False, + xticks=[], + yticks=[], + aspect=1, +) +y = 8 + + +# Line collection +# ---------------------------------------------------------------------------- +n = 50 +segments = np.zeros((n, 2, 2)) +segments[:, 0, 0] = np.linspace(1, 10.25, n) - 0.2 +segments[:, 0, 1] = y - 0.2 +segments[:, 1, 0] = segments[:, 0, 0] + 0.2 +segments[:, 1, 1] = y + 0.15 +linewidths = np.linspace(0.5, 2.5, n) + +collection = LineCollection(segments, linewidths=linewidths, edgecolor="black") +ax.add_collection(collection) + +ax.text(1 - 0.25, y + 0.25, "Line collection", size="small", ha="left", va="baseline") +ax.text( + 10 + 0.25, + y + 0.25, + "LineCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Circle collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n), y +X, Y = offsets[:, 0], offsets[:, 1] +sizes = np.linspace(25, 100, n) +linewidths = np.linspace(1, 2, n) +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] + +collection = CircleCollection( + sizes, + # linewidths = linewidths, + facecolors=facecolors, + edgecolor="black", + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) + +ax.text( + X[0] - 0.25, y + 0.35, "Circle collection", size="small", ha="left", va="baseline" +) +ax.text( + X[-1] + 0.25, + y + 0.35, + "CircleCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Ellipse collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n), y +X, Y = offsets[:, 0], offsets[:, 1] +widths, heights = 15 * np.ones(n), 10 * np.ones(n) +angles = np.linspace(0, 45, n) +linewidths = np.linspace(1, 2, n) +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] +collection = EllipseCollection( + widths, + heights, + angles, + # linewidths = linewidths, + facecolors=facecolors, + edgecolor="black", + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text( + X[0] - 0.25, y + 0.35, "Ellipse collection", size="small", ha="left", va="baseline" +) +ax.text( + X[-1] + 0.25, + y + 0.35, + "EllipseCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Polygon collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n) - 0.2, y + 0.1 +X, Y = offsets[:, 0], offsets[:, 1] +verts = np.zeros((n, 4, 2)) +verts[:] = [0, 0], [1, 0], [1, 1], [0, 1] +sizes = np.linspace(0.25, 0.50, n) +verts *= sizes.reshape(n, 1, 1) +widths, heights = 15 * np.ones(n), 10 * np.ones(n) +numsides = 5 +rotation = np.pi / 4 + +offsets[:, 1] -= sizes / 2 - 0.25 +linewidths = np.linspace(1, 2, n) +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] +collection = PolyCollection( + verts, + # linewidths = linewidths, + sizes=None, + facecolors=facecolors, + edgecolor="black", + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text( + 1 - 0.25, y + 0.35, "Polygon collection", size="small", ha="left", va="baseline" +) +ax.text( + 10 + 0.25, + y + 0.35, + "PolyCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Path collection +# ---------------------------------------------------------------------------- +n = 10 +paths = [] +for i in range(n): + angle1 = np.random.randint(0, 180) + angle2 = angle1 + np.random.randint(180, 270) + path = mpath.Path.wedge(angle1, angle2) + verts, codes = path.vertices * 0.25, path.codes + path = mpath.Path(verts, codes) + paths.append(path) + +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n) + 0.2, y +X, Y = offsets[:, 0], offsets[:, 1] +sizes = np.ones(n) / 100 +offsets[:, 1] -= sizes / 2 - 0.125 +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] +collection = PathCollection( + paths, + sizes=None, + linewidths=1.0, + facecolors=facecolors, + edgecolor="black", + offsets=offsets - (0.2, -0.25), + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text(1 - 0.25, y + 0.35, "Path collection", size="small", ha="left", va="baseline") +ax.text( + 10 + 0.25, + y + 0.35, + "PathCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Regular collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n), y +X, Y = offsets[:, 0], offsets[:, 1] +widths, heights = 15 * np.ones(n), 10 * np.ones(n) +numsides = 5 +rotation = np.pi / 4 +sizes = np.linspace(100, 200, n) +linewidths = np.linspace(1, 2, n) +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] +collection = RegularPolyCollection( + numsides, + rotation, + # linewidths = linewidths, + sizes=sizes, + facecolors=facecolors, + edgecolor="black", + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text( + X[0] - 0.25, + y + 0.35, + "Regular polygon collection", + size="small", + ha="left", + va="baseline", +) +ax.text( + X[-1] + 0.25, + y + 0.35, + "RegularPolyCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Star collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n), y +X, Y = offsets[:, 0], offsets[:, 1] +widths, heights = 15 * np.ones(n), 10 * np.ones(n) +numsides = 5 +rotation = np.pi / 4 +sizes = np.linspace(100, 200, n) +linewidths = np.linspace(1, 2, n) +facecolors = ["%.1f" % c for c in np.linspace(0.25, 0.75, n)] +collection = StarPolygonCollection( + numsides, + rotation, + # linewidths = linewidths, + sizes=sizes, + facecolors=facecolors, + edgecolor="black", + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text( + X[0] - 0.25, y + 0.35, "Star collection", size="small", ha="left", va="baseline" +) +ax.text( + X[-1] + 0.25, + y + 0.35, + "StarPolygonCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Asterisk collection +# ---------------------------------------------------------------------------- +n = 10 +offsets = np.ones((n, 2)) +offsets[:, 0], offsets[:, 1] = np.linspace(1, 10, n), y +X, Y = offsets[:, 0], offsets[:, 1] +widths, heights = 15 * np.ones(n), 10 * np.ones(n) +numsides = 8 +rotation = np.pi / 4 +sizes = np.linspace(50, 150, n) +linewidths = np.linspace(0.5, 2.5, n) +facecolors = ["%.1f" % c for c in np.linspace(0.35, 0.65, n)] +collection = AsteriskPolygonCollection( + numsides, + rotation, + linewidths=linewidths, + sizes=sizes, + edgecolor=facecolors, + offsets=offsets, + transOffset=ax.transData, +) +ax.add_collection(collection) +ax.text( + X[0] - 0.25, y + 0.35, "Asterisk collection", size="small", ha="left", va="baseline" +) +ax.text( + X[-1] + 0.25, + y + 0.35, + "AsteriskPolygonCollection", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +plt.savefig("reference-collection.pdf", dpi=600) +plt.show() diff --git a/code/reference/colormap-diverging.py b/code/reference/colormap-diverging.py new file mode 100644 index 0000000..2b13115 --- /dev/null +++ b/code/reference/colormap-diverging.py @@ -0,0 +1,45 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +cmaps = ( + "PRGn", + "PiYG", + "RdYlGn", + "BrBG", + "RdGy", + "PuOr", + "RdBu", + "RdYlBu", + "Spectral", + "coolwarm_r", + "bwr_r", + "seismic_r", +) + +n = len(cmaps) + +fig = plt.figure(figsize=(4.25, n * 0.22), dpi=100) +ax = plt.subplot(1, 1, 1, frameon=False, xlim=[0, 10], xticks=[], yticks=[]) +fig.subplots_adjust(top=0.99, bottom=0.01, left=0.18, right=0.99) + +y, dy, pad = 0, 0.5, 0.1 +ticks, labels = [], [] +for cmap in cmaps[::-1]: + Z = np.linspace(0, 1, 512).reshape(1, 512) + plt.imshow(Z, extent=[0, 10, y, y + dy], cmap=plt.get_cmap(cmap)) + ticks.append(y + dy / 2) + labels.append(cmap) + y = y + dy + pad + +ax.set_ylim(-pad, y) +ax.set_yticks(ticks) +ax.set_yticklabels(labels) +ax.tick_params(axis="y", which="both", length=0, labelsize="small") + +plt.savefig("reference-colormap-diverging.pdf", dpi=600) +plt.show() diff --git a/code/reference/colormap-qualitative.py b/code/reference/colormap-qualitative.py new file mode 100644 index 0000000..eab2f34 --- /dev/null +++ b/code/reference/colormap-qualitative.py @@ -0,0 +1,45 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +cmaps = ( + "tab10", + "tab20", + "tab20b", + "tab20c", + "Pastel1", + "Pastel2", + "Paired", + "Set1", + "Set2", + "Set3", + "Accent", + "Dark2", +) + +n = len(cmaps) + +fig = plt.figure(figsize=(4.25, n * 0.22)) +ax = plt.subplot(1, 1, 1, frameon=False, xlim=[0, 10], xticks=[], yticks=[]) +fig.subplots_adjust(top=0.99, bottom=0.01, left=0.18, right=0.99) + +y, dy, pad = 0, 0.5, 0.1 +ticks, labels = [], [] +for cmap in cmaps[::-1]: + Z = np.linspace(0, 1, 512).reshape(1, 512) + plt.imshow(Z, extent=[0, 10, y, y + dy], cmap=plt.get_cmap(cmap)) + ticks.append(y + dy / 2) + labels.append(cmap) + y = y + dy + pad + +ax.set_ylim(-pad, y) +ax.set_yticks(ticks) +ax.set_yticklabels(labels) +ax.tick_params(axis="y", which="both", length=0, labelsize="small") + +plt.savefig("reference-colormap-qualitative.pdf", dpi=600) +plt.show() diff --git a/code/reference/colormap-sequential-1.py b/code/reference/colormap-sequential-1.py new file mode 100644 index 0000000..a7460c6 --- /dev/null +++ b/code/reference/colormap-sequential-1.py @@ -0,0 +1,51 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +cmaps = ( + "Greys", + "Reds", + "Oranges", + "YlOrBr", + "YlOrRd", + "OrRd", + "PuRd", + "RdPu", + "BuPu", + "Purples", + "YlGnBu", + "Blues", + "PuBu", + "GnBu", + "PuBuGn", + "BuGn", + "Greens", + "YlGn", +) + +n = len(cmaps) + +fig = plt.figure(figsize=(4.25, n * 0.22)) +ax = plt.subplot(1, 1, 1, frameon=False, xlim=[0, 10], xticks=[], yticks=[]) +fig.subplots_adjust(top=0.99, bottom=0.01, left=0.15, right=0.99) + +y, dy, pad = 0, 0.5, 0.1 +ticks, labels = [], [] +for cmap in cmaps[::-1]: + Z = np.linspace(0, 1, 512).reshape(1, 512) + plt.imshow(Z, extent=[0, 10, y, y + dy], cmap=plt.get_cmap(cmap)) + ticks.append(y + dy / 2) + labels.append(cmap) + y = y + dy + pad + +ax.set_ylim(-pad, y) +ax.set_yticks(ticks) +ax.set_yticklabels(labels) +ax.tick_params(axis="y", which="both", length=0, labelsize="small") + +plt.savefig("reference-colormap-sequential-1.pdf", dpi=600) +plt.show() diff --git a/code/reference/colormap-sequential-2.py b/code/reference/colormap-sequential-2.py new file mode 100644 index 0000000..5c679ae --- /dev/null +++ b/code/reference/colormap-sequential-2.py @@ -0,0 +1,46 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +cmaps = ( + "bone", + "gray", + "pink", + "afmhot", + "hot", + "gist_heat", + "copper", + "Wistia", + "autumn_r", + "summer_r", + "spring_r", + "cool", + "winter_r", +) + +n = len(cmaps) + +fig = plt.figure(figsize=(4.25, n * 0.22)) +ax = plt.subplot(1, 1, 1, frameon=False, xlim=[0, 10], xticks=[], yticks=[]) +fig.subplots_adjust(top=0.99, bottom=0.01, left=0.15, right=0.99) + +y, dy, pad = 0, 0.5, 0.1 +ticks, labels = [], [] +for cmap in cmaps[::-1]: + Z = np.linspace(0, 1, 512).reshape(1, 512) + plt.imshow(Z, extent=[0, 10, y, y + dy], cmap=plt.get_cmap(cmap)) + ticks.append(y + dy / 2) + labels.append(cmap) + y = y + dy + pad + +ax.set_ylim(-pad, y) +ax.set_yticks(ticks) +ax.set_yticklabels(labels) +ax.tick_params(axis="y", which="both", length=0, labelsize="small") + +plt.savefig("reference-colormap-sequential-2.pdf", dpi=600) +plt.show() diff --git a/code/reference/colormap-uniform.py b/code/reference/colormap-uniform.py new file mode 100644 index 0000000..4dd3fd8 --- /dev/null +++ b/code/reference/colormap-uniform.py @@ -0,0 +1,32 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +cmaps = "viridis", "plasma", "inferno", "magma" +n = len(cmaps) + +fig = plt.figure(figsize=(4.25, n * 0.23)) +ax = plt.subplot(1, 1, 1, frameon=False, xlim=[0, 10], xticks=[], yticks=[]) +fig.subplots_adjust(top=0.99, bottom=0.01, left=0.15, right=0.99) + +y, dy, pad = 0, 0.5, 0.1 +ticks, labels = [], [] +for cmap in cmaps: + Z = np.linspace(0, 1, 512).reshape(1, 512) + plt.imshow(Z, extent=[0, 10, y, y + dy], cmap=plt.get_cmap(cmap)) + ticks.append(y + dy / 2) + labels.append(cmap) + y = y + dy + pad + +ax.set_ylim(-pad, y) +ax.set_yticks(ticks) +ax.set_yticklabels(labels) +ax.tick_params(axis="y", which="both", length=0, labelsize="small") + +plt.savefig("reference-colormap-uniform.pdf", dpi=600) +plt.show() diff --git a/code/reference/colorspec.py b/code/reference/colorspec.py new file mode 100644 index 0000000..de1bf9e --- /dev/null +++ b/code/reference/colorspec.py @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Matplotlib cheat sheet +# Released under the BSD License +# ----------------------------------------------------------------------------- +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +fig = plt.figure(figsize=(5.5, 1.5)) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) # , aspect=1) +# ymin, ymax= 0, 7 +# xmin, xmax = 0, figsize[0]/figsize[1] +# ax.set_xlim(xmin, xmax), ax.set_xticks([]) +# ax.set_ylim(ymin, ymax), ax.set_yticks([]) +# ax = plt.subplot() + +palettes = { + "raw": ["b", "g", "r", "c", "m", "y", "k", "w"], + "rgba": [(1, 0, 0), (1, 0, 0, 0.75), (1, 0, 0, 0.50), (1, 0, 0, 0.25)], + "HexRGBA": ["#FF0000", "#FF0000BB", "#FF000088", "#FF000044"], + "cycle": ["C%d" % i for i in range(10)], + "grey": ["%1.1f" % (i / 10) for i in range(11)], + "name": ["DarkRed", "Firebrick", "Crimson", "IndianRed", "Salmon"], +} + + +ymin, ymax = 0, len(palettes) +xmin, xmax = 0, 22 +ax.set_xlim(xmin, xmax), ax.set_xticks([]) +ax.set_ylim(ymin, ymax), ax.set_yticks([]) + + +for y, (name, colors) in enumerate(palettes.items()): + C = mpl.colors.to_rgba_array(colors).reshape((1, len(colors), 4)) + ax.imshow(C, extent=[xmin, xmax, y + 0.025, y + 0.95]) + dx = (xmax - xmin) / len(colors) + for i in range(len(colors)): + color = "white" + if colors[i] in ["1.0", "w"]: + color = "black" + text = str(colors[i]).replace(" ", "") + ax.text( + (i + 0.5) * dx, + y + 0.5, + text, + color=color, + zorder=10, + family="Source Code Pro", + size=9, + ha="center", + va="center", + ) + +plt.savefig("../../figures/reference/colorspec.pdf") +plt.show() diff --git a/code/reference/font.py b/code/reference/font.py new file mode 100644 index 0000000..b074993 --- /dev/null +++ b/code/reference/font.py @@ -0,0 +1,171 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties + +fig = plt.figure(figsize=(4.25, 3.8)) +ax = fig.add_axes( + [0, 0, 1, 1], frameon=False, xticks=[], yticks=[], xlim=[0, 40], ylim=[0, 38] +) + +y = 1 + +# ----------------------------------------------------------------------------- +variants = { + "normal": "/Users/rougier/Library/Fonts/Delicious-Roman.otf", + "small-caps": "/Users/rougier/Library/Fonts/Delicious-SmallCaps.otf", +} + +text = "The quick brown fox jumps over the lazy dog" +for i, variant in enumerate(variants.keys()): + ax.text( + 1, + y, + text, + size=9, + va="center", + fontproperties=FontProperties(fname=variants[variant]), + ) + + ax.text( + 39, + y, + variant, + color="0.25", + va="center", + ha="right", + size="small", + family="Source Code Pro", + weight=400, + ) + y += 1.65 +y += 1 + +# ----------------------------------------------------------------------------- +styles = ["normal", "italic"] + +text = "The quick brown fox jumps over the lazy dog" +for i, style in enumerate(styles): + ax.text(1, y, text, size=9, va="center", style=style, family="Source Sans Pro") + + ax.text( + 39, + y, + style, + color="0.25", + va="center", + ha="right", + size="small", + family="Source Code Pro", + weight=400, + ) + y += 1.65 +y += 1 + + +# ----------------------------------------------------------------------------- +families = { + "Pacifico": "cursive", + "Source Sans Pro": "sans", + "Source Serif Pro": "serif", + "Source Code Pro": "monospace", +} + +text = "The quick brown fox jumps over the lazy dog" +for i, family in enumerate(families): + ax.text(1, y, text, va="center", size=9, family=family, weight="regular") + + ax.text( + 39, + y, + "%s" % (families[family]), + color="0.25", + va="center", + ha="right", + size="small", + family="Source Code Pro", + weight=400, + ) + y += 1.65 +y += 1 + + +# ----------------------------------------------------------------------------- +weights = { + "ultralight": 100, + "light": 200, + "normal": 400, + "regular": 400, + "book": 400, + "medium": 500, + "roman": 500, + "semibold": 600, + "demibold": 600, + "demi": 600, + "bold": 700, + "heavy": 800, + "extra bold": 800, + "black": 900, +} + +text = "The quick brown fox jumps over the lazy dog" +for i, weight in enumerate(["ultralight", "normal", "semibold", "bold", "black"]): + ax.text(1, y, text, size=9, va="center", family="Source Sans Pro", weight=weight) + + ax.text( + 39, + y, + "%s (%d)" % (weight, weights[weight]), + color="0.25", + va="center", + ha="right", + size="small", + family="Source Code Pro", + weight=400, + ) + y += 1.65 +y += 1 + +# ----------------------------------------------------------------------------- +sizes = { + "xx-small": 0.579, + "x-small": 0.694, + "small": 0.833, + "medium": 1.0, + "large": 1.200, + "x-large": 1.440, + "xx-large": 1.728, +} + +text = "The quick brown fox" +for i, size in enumerate(sizes.keys()): + ax.text( + 1, + y, + text, + size=size, + ha="left", + va="center", + family="Source Sans Pro", + weight="light", + ) + + ax.text( + 39, + y, + "%s (%.2f)" % (size, sizes[size]), + color="0.25", + va="center", + ha="right", + size="small", + family="Source Code Pro", + weight=400, + ) + y += 1.65 * max(sizes[size], sizes["small"]) + + +plt.savefig("reference-font.pdf", dpi=600) +plt.show() diff --git a/code/reference/hatch.py b/code/reference/hatch.py new file mode 100644 index 0000000..81f5927 --- /dev/null +++ b/code/reference/hatch.py @@ -0,0 +1,132 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import imageio +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +fig = plt.figure(figsize=(4.25, 4.5 * 0.55)) +ax = fig.add_axes( + [0, 0, 1, 1], xlim=[0, 11], ylim=[0.5, 4.5], frameon=False, xticks=[], yticks=[] +) # , aspect=1) +y = 3.75 + + +# Hatch pattern +# ---------------------------------------------------------------------------- +patterns = "/", "\\", "|", "-", "+", "x", "o", "O", ".", "*" +w, h = 0.75 * 10 / len(patterns), 0.5 +X = np.linspace(1, 10 - w, len(patterns)) + +for x, pattern in zip(X, patterns): + rect = Rectangle( + (x, y), + w, + h, + hatch=pattern * 2, + facecolor="0.85", + edgecolor="0.00", + linewidth=1.0, + ) + ax.add_patch(rect) + plt.text( + x + w / 2, + y - 0.125, + '"%s"' % pattern, + size="x-small", + ha="center", + va="top", + family="monospace", + ) + +plt.text(X[0] - 0.25, y + 0.65, "Hatch pattern", size="small", ha="left", va="baseline") +plt.text( + X[-1] + w + 0.25, + y + 0.65, + "hatch", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1.5 + + +# Hatch density +# ---------------------------------------------------------------------------- +patterns = "/", "//", "///", "////" +w, h = 0.75 * 10 / len(patterns), 0.5 +X = np.linspace(1, 10 - w, len(patterns)) + +for x, pattern in zip(X, patterns): + rect = Rectangle( + (x, y), w, h, hatch=pattern, facecolor="0.85", edgecolor="0.00", linewidth=1.0 + ) + ax.add_patch(rect) + plt.text( + x + w / 2, + y - 0.125, + '"%s"' % pattern, + size="x-small", + ha="center", + va="top", + family="monospace", + ) + +plt.text(X[0] - 0.25, y + 0.65, "Hatch density", size="small", ha="left", va="baseline") +plt.text( + X[-1] + w + 0.25, + y + 0.65, + "hatch", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1.5 + + +# Hatch linewidth (hack) +# ---------------------------------------------------------------------------- +# We cannot have different hatch linewidth in a single figure and the solution +# was to generate images and to show them here. Images were generated from the +# make-hatch-linewidth.py script. +widths = 1, 2, 3, 4, 5, 6 +w, h = 0.75 * 10 / len(widths), 0.5 +X = np.linspace(1, 10 - w, len(widths)) +for (x, width) in zip(X, widths): + image = imageio.imread("hatch-%d.png" % width) + ax.imshow(image, extent=[x, x + w, y, y + h]) + rect = Rectangle((x, y), w, h, facecolor="none", edgecolor="black", linewidth=1.0) + ax.add_patch(rect) + plt.text( + x + w / 2, + y - 0.125, + "%d" % width, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +plt.text( + X[0] - 0.25, y + 0.65, "Hatch linewidth", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + w + 0.25, + y + 0.65, + "rcParams['hatch.linewidth']", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1.5 + +plt.savefig("reference-hatch.pdf", dpi=600) +plt.show() diff --git a/code/reference/line.py b/code/reference/line.py new file mode 100644 index 0000000..a2d2dd5 --- /dev/null +++ b/code/reference/line.py @@ -0,0 +1,224 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +fig = plt.figure(figsize=(4.25, 6 * 0.55)) +ax = fig.add_axes( + [0, 0, 1, 1], xlim=[0, 11], ylim=[0.5, 6.5], frameon=False, xticks=[], yticks=[] +) +y = 6 + + +def split(n_segment): + width = 9 + segment_width = 0.75 * (width / n_segment) + segment_pad = (width - n_segment * segment_width) / (n_segment - 1) + X0 = 1 + np.arange(n_segment) * (segment_width + segment_pad) + X1 = X0 + segment_width + return X0, X1 + + +# Color +# ---------------------------------------------------------------------------- +X0, X1 = split(10) +for i, (x0, x1) in enumerate(zip(X0, X1)): + ax.plot([x0, x1], [y, y], color="C%d" % i, linewidth=10, solid_capstyle="butt") + plt.text( + (x0 + x1) / 2, + y - 0.2, + '"C%d"' % i, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +ax.text(X0[0] - 0.25, y + 0.2, "Line color", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "color / c", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) + +y -= 1 + +# Line width +# ---------------------------------------------------------------------------- +X0, X1 = split(5) +LW = np.arange(1, 6) +for x0, x1, lw in zip(X0, X1, LW): + ax.plot([x0, x1], [y, y], color="black", linewidth=lw, solid_capstyle="round") + plt.text((x0 + x1) / 2, y - 0.2, "%d" % lw, size="x-small", ha="center", va="top") +ax.text(X0[0] - 0.25, y + 0.2, "Line width", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "linewidth / lw", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Solid capstyle +# ---------------------------------------------------------------------------- +X0, X1 = split(3) +styles = "butt", "round", "projecting" +for x0, x1, style in zip(X0, X1, styles): + ax.plot([x0, x1], [y, y], color=".85", solid_capstyle="projecting", linewidth=7) + ax.plot([x0, x1], [y, y], color="black", solid_capstyle=style, linewidth=7) + ax.text( + (x0 + x1) / 2, + y - 0.2, + '"%s"' % style, + size="x-small", + ha="center", + va="top", + family="monospace", + ) + +ax.text(X0[0] - 0.25, y + 0.2, "Cap style", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "solid_capstyle", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Dash capstyle +# ---------------------------------------------------------------------------- +X0, X1 = split(3) +styles = "butt", "round", "projecting" +for x0, x1, style in zip(X0, X1, styles): + ax.plot( + [x0, x1], + [y, y], + color=".85", + dash_capstyle="projecting", + linewidth=7, + linestyle="--", + ) + ax.plot( + [x0, x1], + [y, y], + color="black", + linewidth=7, + linestyle="--", + dash_capstyle=style, + ) + ax.text( + (x0 + x1) / 2, + y - 0.2, + '"%s"' % style, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +ax.text(X0[0] - 0.25, y + 0.2, "Dash cap style", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "dash_capstyle", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Line style +# ---------------------------------------------------------------------------- +X0, X1 = split(5) +styles = "-", ":", "--", "-.", (0, (0.01, 2)) + +for x0, x1, style in zip(X0, X1, styles): + ax.plot( + [x0, x1], + [y, y], + color="black", + linestyle=style, + solid_capstyle="round", + dash_capstyle="round", + linewidth=3, + ) + if isinstance(style, str): + text = '"%s"' % style + else: + text = "%s" % str(style) + + ax.text( + (x0 + x1) / 2, + y - 0.2, + text, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +ax.text(X0[0] - 0.25, y + 0.2, "Line style", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "linestyle / ls", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Color +# ---------------------------------------------------------------------------- +X0, X1 = split(2) +for i, (x0, x1) in enumerate(zip(X0, X1)): + + X = np.linspace(x0, x1, 500) + Y = y + 0.1 * np.cos(5 * np.pi * (X - x0) / (x1 - x0)) + + ax.plot(X, Y, color="black", linewidth=0.75, antialiased=i, rasterized=1 - i) + plt.text( + (x0 + x1) / 2, + y - 0.2, + "%s" % bool(i), + size="x-small", + ha="center", + va="top", + family="monospace", + ) + +ax.text(X0[0] - 0.25, y + 0.2, "Antialias", size="small", ha="left", va="baseline") +ax.text( + X1[-1] + 0.25, + y + 0.2, + "antialiased", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) + +y -= 1 + +plt.savefig("reference-line.pdf", dpi=200) +plt.show() diff --git a/code/reference/marker.py b/code/reference/marker.py new file mode 100644 index 0000000..682dff8 --- /dev/null +++ b/code/reference/marker.py @@ -0,0 +1,288 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +fig = plt.figure(figsize=(4.25, 8 * 0.55)) +ax = fig.add_axes( + [0, 0, 1, 1], xlim=[0, 11], ylim=[0.5, 8.5], frameon=False, xticks=[], yticks=[] +) # , aspect=1) + +X = np.linspace(1, 10, 10) +Y = np.zeros(len(X)) +y = 8 + +# Marker edge color +# ---------------------------------------------------------------------------- +C = ["C%d" % i for i in range(10)] +plt.scatter(X, y + Y, s=200, facecolor="white", edgecolor=C, linewidth=1.5) +for x, c in zip(X, C): + plt.text( + x, + y - 0.25, + '"%s"' % c, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +plt.text( + X[0] - 0.25, y + 0.25, "Marker edge color", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + 0.25, + y + 0.25, + "mec / ec", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +# Marker face color +# ---------------------------------------------------------------------------- +C = ["C%d" % i for i in range(10)] +plt.scatter(X, y + Y, s=200, facecolor=C, edgecolor="None") +for x, c in zip(X, C): + plt.text( + x, + y - 0.25, + '"%s"' % c, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +plt.text( + X[0] - 0.25, y + 0.25, "Marker face color", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + 0.25, + y + 0.25, + "mfc / fc", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Marker edge width +# ---------------------------------------------------------------------------- +LW = 1 + np.arange(10) / 2 +plt.scatter(X, y + Y, s=100, facecolor="white", edgecolor="black", linewidth=LW) +for x, lw in zip(X, LW): + plt.text( + x, + y - 0.25, + "%.1f" % lw, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +plt.text( + X[0] - 0.25, y + 0.25, "Marker edge width", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + 0.25, + y + 0.25, + "mew / lw", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Marker edge width +# ---------------------------------------------------------------------------- +S = (1 + np.arange(10)) * 25 +plt.scatter(X, y + Y, s=S, facecolor="black", edgecolor="None") +for x, s in zip(X, S): + plt.text( + x, y - 0.25, "%d" % s, size="x-small", ha="center", va="top", family="monospace" + ) +plt.text(X[0] - 0.25, y + 0.25, "Marker size", size="small", ha="left", va="baseline") +plt.text( + X[-1] + 0.25, + y + 0.25, + "ms / s", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + + +X = np.linspace(1, 10, 12) + +# Filled markers +# ----------------------------------------------------------------------------- +M = [".", "o", "s", "P", "X", "*", "p", "D", "<", ">", "^", "v"] +for x, marker in zip(X, M): + plt.scatter(x, y, s=256, color="black", marker="s", fc=".9", ec="none") + plt.scatter( + x, + y, + s=100, + color="black", + marker=marker, + fc="white", + ec="black", + linewidth=0.75, + ) + plt.text( + x, + y - 0.25, + '"%s"' % marker, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +plt.text( + X[0] - 0.25, y + 0.25, "Filled markers", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + 0.25, + y + 0.25, + "marker", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Unfilled markers +# ----------------------------------------------------------------------------- +M = ["1", "2", "3", "4", "+", "x", "|", "_", 4, 5, 6, 7] +for x, marker in zip(X, M): + if isinstance(marker, str): + text = '"%s"' % marker + else: + text = "%s" % marker + + plt.scatter(x, y, s=256, color="black", marker="s", fc=".9", ec="none") + plt.scatter( + x, y, s=100, color="black", marker=marker, fc="none", ec="black", linewidth=0.75 + ) + plt.text( + x, y - 0.25, text, size="x-small", ha="center", va="top", family="monospace" + ) +plt.text( + X[0] - 0.25, y + 0.25, "Unfilled markers", size="small", ha="left", va="baseline" +) +plt.text( + X[-1] + 0.25, + y + 0.25, + "marker", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Unicode markers +# ----------------------------------------------------------------------------- +M = ["♠", "♣", "♥", "♦", "→", "←", "↑", "↓", "◐", "◑", "◒", "◓"] +for x, marker in zip(X, M): + ax.scatter(x, y, s=256, color="black", marker="s", fc=".9", ec="none") + ax.scatter( + x, + y, + s=100, + color="black", + marker="$" + marker + "$", + fc="black", + ec="none", + linewidth=0.5, + ) + ax.text( + x, + y - 0.25, + '"\$%s\$"' % marker, + size="x-small", + ha="center", + va="top", + family="monospace", + ) +ax.text( + X[0] - 0.25, y + 0.25, "Unicode markers", size="small", ha="left", va="baseline" +) +ax.text( + X[-1] + 0.25, + y + 0.25, + "marker", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) +y -= 1 + +# Spacing +# ----------------------------------------------------------------------------- +n_segment = 4 +width = 9 +segment_width = 0.75 * (width / n_segment) +segment_pad = (width - n_segment * segment_width) / (n_segment - 1) +X0 = 1 + np.arange(n_segment) * (segment_width + segment_pad) +marks = [10, [0, -1], (25, 5), [0, 25, -1]] + +for x0, mark in zip(X0, marks): + X = np.linspace(x0, x0 + segment_width, 50) + Y = y * np.ones(len(X)) + ax.plot( + X, + Y, + linewidth=1, + color="black", + marker=".", + mfc="white", + mec="black", + mew="1", + markevery=mark, + ) + + ax.text( + (X[0] + X[-1]) / 2, + y - 0.1, + "%s" % str(mark), + size="x-small", + ha="center", + va="top", + ) + + +ax.text(1 - 0.25, y + 0.25, "Marker spacing", size="small", ha="left", va="baseline") +ax.text( + X[-1] + 0.25, + y + 0.25, + "markevery", + color="blue", + size="small", + ha="right", + va="baseline", + family="monospace", +) + + +plt.savefig("reference-marker.pdf", dpi=600) +plt.show() diff --git a/code/reference/scale.py b/code/reference/scale.py new file mode 100644 index 0000000..dc12ad5 --- /dev/null +++ b/code/reference/scale.py @@ -0,0 +1,192 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import NullFormatter, SymmetricalLogLocator + + +X = np.linspace(-2, 2, 1001) +Y = np.cos(X * 5 * 2 * np.pi) + + +figure = plt.figure(figsize=(6, 6)) + +# Style +# ----------------------------------------------------------------------------- +# plt.rc('font', family="Roboto Condensed") +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + + +# Linear scale +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 1, ylim=(-3, 2)) +plt.plot(X, Y, color="C1", linewidth=1.5) +ax.set_yticks([]) +ax.text( + 0.01, + 0.95, + "Linear scale", + transform=ax.transAxes, + weight=600, + horizontalalignment="left", + verticalalignment="top", + size="medium", +) + +# Respective domains for scales +plt.plot([-2, 2], [-1.5, -1.5], color="black", linewidth=1, marker="|") +ax.text( + 0, + -1.5, + "SymLog scale domain", + transform=ax.transData, + size="x-small", + bbox=dict(facecolor="white", edgecolor="None", pad=1.0), + horizontalalignment="center", + verticalalignment="center", +) + +plt.plot([0, 2], [-2.0, -2.0], color="black", linewidth=1, marker="|") +ax.text( + 1, + -2, + "Log scale domain", + transform=ax.transData, + size="x-small", + bbox=dict(facecolor="white", edgecolor="None", pad=1.0), + horizontalalignment="center", + verticalalignment="center", +) + +plt.plot([0, 1], [-2.5, -2.5], color="black", linewidth=1, marker="|") +ax.text( + 0.5, + -2.5, + "Logit scale domain", + transform=ax.transData, + size="x-small", + bbox=dict(facecolor="white", edgecolor="None", pad=1.0), + horizontalalignment="center", + verticalalignment="center", +) + + +# Log scale (X > 0) +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 2, ylim=(-2, 2)) +ax.set_xscale("log", basex=10) +plt.plot(X, Y, color="C1", linewidth=1.5) +ax.set_yticks([]) +ax.text( + 0.01, + 0.95, + "Log scale (x > 0)", + transform=ax.transAxes, + weight=600, + horizontalalignment="left", + verticalalignment="top", + size="medium", +) + + +# SymLog scale +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 3, ylim=(-2, 2)) +t = 0.05 +ax.set_xscale("symlog", basex=10, linthreshx=t, linscalex=1) +plt.plot(X, Y, color="C1", linewidth=1.5) +ax.set_yticks([]) +ax.xaxis.set_minor_locator( + SymmetricalLogLocator(base=10, linthresh=t, subs=np.arange(2, 10)) +) + +ax.text( + 0.01, + 0.95, + "Symlog scale", + transform=ax.transAxes, + weight=600, + horizontalalignment="left", + verticalalignment="top", + size="medium", +) +ax.fill_betweenx([-2, 2], [-t, -t], [+t, +t], facecolor="black", alpha=0.1) +ax.text( + 0.50, + 0.05, + "Linear", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.text( + 0.15, + 0.05, + "Logarithmic", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.text( + 0.85, + 0.05, + "Logarithmic", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) + + +# Logit (0 < X < 1) +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 4, ylim=(-2, 2)) +ax.set_xscale("logit") +plt.plot(X, Y, color="C1", linewidth=1.5) +ax.set_yticks([]) +ax.xaxis.set_minor_formatter(NullFormatter()) +ax.text( + 0.01, + 0.95, + "Logit scale (0 < x < 1)", + transform=ax.transAxes, + weight=600, + horizontalalignment="left", + verticalalignment="top", + size="medium", +) +ax.fill_betweenx([-2, 2], [0.2, 0.2], [0.8, 0.8], facecolor="black", alpha=0.1) +ax.text( + 0.50, + 0.05, + "Quasi linear", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.text( + 0.2, + 0.05, + "Logarithmic", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.text( + 0.8, + 0.05, + "Logarithmic", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) + +# Show +plt.tight_layout() +plt.savefig("reference-scale.pdf") +plt.show() diff --git a/code/reference/text-alignment.py b/code/reference/text-alignment.py new file mode 100644 index 0000000..1d19c08 --- /dev/null +++ b/code/reference/text-alignment.py @@ -0,0 +1,91 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +dpi = 100 +fig = plt.figure(figsize=(4.25, 1.5), dpi=dpi) +ax = fig.add_axes( + [0, 0, 1, 1], frameon=False, xlim=(0, 4.25), ylim=(0, 1.5), xticks=[], yticks=[] +) + +fontsize = 48 +renderer = fig.canvas.get_renderer() +horizontalalignment = "left" +verticalalignment = "center" +position = (0.25, 1.5 / 2) +color = "0.25" + +# Compute vertical and horizontal alignment offsets +text = ax.text(0, 0, "Matplotlib", fontsize=fontsize) +yoffset = {} +for alignment in ["top", "center", "baseline", "bottom"]: + text.set_verticalalignment(alignment) + y = text.get_window_extent(renderer).y0 / dpi + yoffset[alignment] = y + +xoffset = {} +for alignment in ["left", "center", "right"]: + text.set_horizontalalignment(alignment) + x = text.get_window_extent(renderer).x0 / dpi + xoffset[alignment] = x + +# Actual positioning of the text +text.set_horizontalalignment(horizontalalignment) +text.set_verticalalignment(verticalalignment) +text.set_position(position) + + +for name, y in yoffset.items(): + y = position[1] - y + yoffset[verticalalignment] + plt.plot([0.1, 3.75], [y, y], linewidth=0.5, color=color) + plt.text(3.75, y, " " + name, color=color, ha="left", va="center", size="x-small") + +for name, x in xoffset.items(): + x = position[0] - x + xoffset[horizontalalignment] + plt.plot([x, x], [0.25, 1.25], linewidth=0.5, color=color) + plt.text(x, 0.24, name, color=color, ha="center", va="top", size="x-small") + +P = [] +for x in xoffset.values(): + x = position[0] - x + xoffset[horizontalalignment] + for y in yoffset.values(): + y = position[1] - y + yoffset[verticalalignment] + P.append((x, y)) +P = np.array(P) + +ax.scatter( + P[:, 0], + P[:, 1], + s=10, + zorder=10, + facecolor="white", + edgecolor=color, + linewidth=0.75, +) + +epsilon = 0.05 +plt.text( + P[3, 0] + epsilon, + P[3, 1] - epsilon, + "(0,0)", + color=color, + ha="left", + va="top", + size="x-small", +) +plt.text( + P[8, 0] - epsilon, + P[8, 1] + epsilon, + "(1,1)", + color=color, + ha="right", + va="bottom", + size="x-small", +) + +plt.savefig("reference-text-alignment.pdf", dpi=600) +plt.show() diff --git a/code/reference/tick-formatter.py b/code/reference/tick-formatter.py new file mode 100644 index 0000000..3afe31b --- /dev/null +++ b/code/reference/tick-formatter.py @@ -0,0 +1,109 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +# Setup a plot such that only the bottom spine is shown +def setup(ax): + ax.spines["right"].set_color("none") + ax.spines["left"].set_color("none") + ax.yaxis.set_major_locator(ticker.NullLocator()) + ax.spines["top"].set_color("none") + ax.xaxis.set_ticks_position("bottom") + ax.tick_params(which="major", width=1.00, length=5) + ax.tick_params(which="minor", width=0.75, length=2.5, labelsize=10) + ax.set_xlim(0, 5) + ax.set_ylim(0, 1) + ax.patch.set_alpha(0.0) + + +fig = plt.figure(figsize=(8, 6)) +n = 7 + +# Null formatter +ax = fig.add_subplot(n, 1, 1) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +ax.xaxis.set_major_formatter(ticker.NullFormatter()) +ax.xaxis.set_minor_formatter(ticker.NullFormatter()) +ax.text(0.0, 0.1, "NullFormatter()", fontsize=16, transform=ax.transAxes) + +# Fixed formatter +ax = fig.add_subplot(n, 1, 2) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.0)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +majors = ["", "0", "1", "2", "3", "4", "5"] +ax.xaxis.set_major_formatter(ticker.FixedFormatter(majors)) +minors = [""] + [ + "%.2f" % (x - int(x)) if (x - int(x)) else "" for x in np.arange(0, 5, 0.25) +] +ax.xaxis.set_minor_formatter(ticker.FixedFormatter(minors)) +ax.text( + 0.0, 0.1, "FixedFormatter(['', '0', '1', ...])", fontsize=15, transform=ax.transAxes +) + + +# FuncFormatter can be used as a decorator +@ticker.FuncFormatter +def major_formatter(x, pos): + return "[%.2f]" % x + + +ax = fig.add_subplot(n, 1, 3) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +ax.xaxis.set_major_formatter(major_formatter) +ax.text( + 0.0, + 0.1, + 'FuncFormatter(lambda x, pos: "[%.2f]" % x)', + fontsize=15, + transform=ax.transAxes, +) + + +# FormatStr formatter +ax = fig.add_subplot(n, 1, 4) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter(">%d<")) +ax.text(0.0, 0.1, "FormatStrFormatter('>%d<')", fontsize=15, transform=ax.transAxes) + +# Scalar formatter +ax = fig.add_subplot(n, 1, 5) +setup(ax) +ax.xaxis.set_major_locator(ticker.AutoLocator()) +ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) +ax.xaxis.set_major_formatter(ticker.ScalarFormatter(useMathText=True)) +ax.text(0.0, 0.1, "ScalarFormatter()", fontsize=15, transform=ax.transAxes) + +# StrMethod formatter +ax = fig.add_subplot(n, 1, 6) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +ax.xaxis.set_major_formatter(ticker.StrMethodFormatter("{x}")) +ax.text(0.0, 0.1, "StrMethodFormatter('{x}')", fontsize=15, transform=ax.transAxes) + +# Percent formatter +ax = fig.add_subplot(n, 1, 7) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) +ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5)) +ax.text(0.0, 0.1, "PercentFormatter(xmax=5)", fontsize=15, transform=ax.transAxes) + +# Push the top of the top axes outside the figure because we only show the +# bottom spine. +fig.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05) + +plt.savefig("reference-tick-formatter.pdf", dpi=600) +plt.show() diff --git a/code/reference/tick-locator.py b/code/reference/tick-locator.py new file mode 100644 index 0000000..44fff8f --- /dev/null +++ b/code/reference/tick-locator.py @@ -0,0 +1,97 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +# Setup a plot such that only the bottom spine is shown +def setup(ax): + ax.spines["right"].set_color("none") + ax.spines["left"].set_color("none") + ax.yaxis.set_major_locator(ticker.NullLocator()) + ax.spines["top"].set_color("none") + ax.xaxis.set_ticks_position("bottom") + ax.tick_params(which="major", width=1.00) + ax.tick_params(which="major", length=5) + ax.tick_params(which="minor", width=0.75) + ax.tick_params(which="minor", length=2.5) + ax.set_xlim(0, 5) + ax.set_ylim(0, 1) + ax.patch.set_alpha(0.0) + + +plt.figure(figsize=(8, 6)) +n = 8 + +# Null Locator +ax = plt.subplot(n, 1, 1) +setup(ax) +ax.xaxis.set_major_locator(ticker.NullLocator()) +ax.xaxis.set_minor_locator(ticker.NullLocator()) +ax.text(0.0, 0.1, "NullLocator()", fontsize=14, transform=ax.transAxes) + +# Multiple Locator +ax = plt.subplot(n, 1, 2) +setup(ax) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.5)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.1)) +ax.text(0.0, 0.1, "MultipleLocator(0.5)", fontsize=14, transform=ax.transAxes) + +# Fixed Locator +ax = plt.subplot(n, 1, 3) +setup(ax) +majors = [0, 1, 5] +ax.xaxis.set_major_locator(ticker.FixedLocator(majors)) +minors = np.linspace(0, 1, 11)[1:-1] +ax.xaxis.set_minor_locator(ticker.FixedLocator(minors)) +ax.text(0.0, 0.1, "FixedLocator([0, 1, 5])", fontsize=14, transform=ax.transAxes) + +# Linear Locator +ax = plt.subplot(n, 1, 4) +setup(ax) +ax.xaxis.set_major_locator(ticker.LinearLocator(3)) +ax.xaxis.set_minor_locator(ticker.LinearLocator(31)) +ax.text(0.0, 0.1, "LinearLocator(numticks=3)", fontsize=14, transform=ax.transAxes) + +# Index Locator +ax = plt.subplot(n, 1, 5) +setup(ax) +ax.plot(range(0, 5), [0] * 5, color="white") +ax.xaxis.set_major_locator(ticker.IndexLocator(base=0.5, offset=0.25)) +ax.text( + 0.0, 0.1, "IndexLocator(base=0.5, offset=0.25)", fontsize=14, transform=ax.transAxes +) + +# Auto Locator +ax = plt.subplot(n, 1, 6) +setup(ax) +ax.xaxis.set_major_locator(ticker.AutoLocator()) +ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) +ax.text(0.0, 0.1, "AutoLocator()", fontsize=14, transform=ax.transAxes) + +# MaxN Locator +ax = plt.subplot(n, 1, 7) +setup(ax) +ax.xaxis.set_major_locator(ticker.MaxNLocator(4)) +ax.xaxis.set_minor_locator(ticker.MaxNLocator(40)) +ax.text(0.0, 0.1, "MaxNLocator(n=4)", fontsize=14, transform=ax.transAxes) + +# Log Locator +ax = plt.subplot(n, 1, 8) +setup(ax) +ax.set_xlim(10 ** 3, 10 ** 10) +ax.set_xscale("log") +ax.xaxis.set_major_locator(ticker.LogLocator(base=10.0, numticks=15)) +ax.text( + 0.0, 0.1, "LogLocator(base=10, numticks=15)", fontsize=15, transform=ax.transAxes +) + +# Push the top of the top axes outside the figure because we only show the +# bottom spine. +plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05) + +plt.savefig("reference-tick-locator.pdf", dpi=600) +plt.show() diff --git a/code/rules/graphics.py b/code/rules/graphics.py new file mode 100644 index 0000000..1cc732d --- /dev/null +++ b/code/rules/graphics.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright INRIA +# Contributors: Wahiba Taouali (Wahiba.Taouali@inria.fr) +# Nicolas P. Rougier (Nicolas.Rougier@inria.fr) +# +# This software is governed by the CeCILL license under French law and abiding +# by the rules of distribution of free software. You can use, modify and/ or +# redistribute the software under the terms of the CeCILL license as circulated +# by CEA, CNRS and INRIA at the following URL +# http://www.cecill.info/index.en.html. +# +# As a counterpart to the access to the source code and rights to copy, modify +# and redistribute granted by the license, users are provided only with a +# limited warranty and the software's author, the holder of the economic +# rights, and the successive licensors have only limited liability. +# +# In this respect, the user's attention is drawn to the risks associated with +# loading, using, modifying and/or developing or reproducing the software by +# the user in light of its specific status of free software, that may mean that +# it is complicated to manipulate, and that also therefore means that it is +# reserved for developers and experienced professionals having in-depth +# computer knowledge. Users are therefore encouraged to load and test the +# software's suitability as regards their requirements in conditions enabling +# the security of their systems and/or data to be ensured and, more generally, +# to use and operate it in the same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# ----------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import ImageGrid +from mpl_toolkits.axes_grid1.inset_locator import mark_inset +from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes + +from projections import * + +# ----------------------------------------------------------------------------- +def polar_frame(ax, title=None, legend=False, zoom=False, labels=True): + """ Draw a polar frame """ + + for rho in [0, 2, 5, 10, 20, 40, 60, 80, 90]: + lw, color, alpha = 1, "0.00", 0.25 + if rho == 90 and not zoom: + color, lw, alpha = "0.00", 2, 1 + + n = 500 + R = np.ones(n) * rho / 90.0 + T = np.linspace(-np.pi / 2, np.pi / 2, n) + X, Y = polar_to_cartesian(R, T) + ax.plot(X, Y, color=color, lw=lw, alpha=alpha) + + if not zoom and rho in [0, 10, 20, 40, 80] and labels: + ax.text( + X[-1] * 1.0 - 0.075, + Y[-1], + u"%d°" % rho, + color="k", # size=15, + horizontalalignment="center", + verticalalignment="center", + ) + + for theta in [-90, -60, -30, 0, +30, +60, +90]: + lw, color, alpha = 1, "0.00", 0.25 + if theta in [-90, +90] and not zoom: + color, lw, alpha = "0.00", 2, 1 + angle = theta / 90.0 * np.pi / 2 + + n = 500 + R = np.linspace(0, 1, n) + T = np.ones(n) * angle + X, Y = polar_to_cartesian(R, T) + ax.plot(X, Y, color=color, lw=lw, alpha=alpha) + + if not zoom and theta in [-90, -60, -30, +30, +60, +90] and labels: + ax.text( + X[-1] * 1.05, + Y[-1] * 1.05, + u"%d°" % theta, + color="k", # size=15, + horizontalalignment="left", + verticalalignment="center", + ) + d = 0.01 + ax.set_xlim(0.0 - d, 1.0 + d) + ax.set_ylim(-1.0 - d, 1.0 + d) + ax.set_xticks([]) + ax.set_yticks([]) + + if legend: + ax.set_frame_on(True) + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.spines["top"].set_color("none") + ax.xaxis.set_ticks_position("bottom") + ax.spines["bottom"].set_position(("data", -1.2)) + ax.set_xticks([]) + ax.text( + 0.0, + -1.1, + "$\longleftarrow$ Foveal", + verticalalignment="top", + horizontalalignment="left", + size=12, + ) + ax.text( + 1.0, + -1.1, + "Peripheral $\longrightarrow$", + verticalalignment="top", + horizontalalignment="right", + size=12, + ) + else: + ax.set_frame_on(False) + if title: + ax.title(title) + + +# ----------------------------------------------------------------------------- +def logpolar_frame(ax, title=None, legend=False, labels=True): + """ Draw a log polar frame """ + + for rho in [2, 5, 10, 20, 40, 60, 80, 90]: + lw, color, alpha = 1, "0.00", 0.25 + if rho == 90: + color, lw, alpha = "0.00", 2, 1 + + n = 500 + R = np.ones(n) * rho / 90.0 + T = np.linspace(-np.pi / 2, np.pi / 2, n) + X, Y = polar_to_logpolar(R, T) + X, Y = X * 2, 2 * Y - 1 + ax.plot(X, Y, color=color, lw=lw, alpha=alpha) + if labels and rho in [2, 5, 10, 20, 40, 80]: + ax.text( + X[-1], + Y[-1] + 0.05, + u"%d°" % rho, + color="k", # size=15, + horizontalalignment="right", + verticalalignment="bottom", + ) + + for theta in [-90, -60, -30, 0, +30, +60, +90]: + lw, color, alpha = 1, "0.00", 0.25 + if theta in [-90, +90]: + color, lw, alpha = "0.00", 2, 1 + angle = theta / 90.0 * np.pi / 2 + + n = 500 + R = np.linspace(0, 1, n) + T = np.ones(n) * angle + X, Y = polar_to_logpolar(R, T) + X, Y = X * 2, 2 * Y - 1 + ax.plot(X, Y, color=color, lw=lw, alpha=alpha) + if labels: + ax.text( + X[-1] * 1.0 + 0.05, + Y[-1] * 1.0, + u"%d°" % theta, + color="k", # size=15, + horizontalalignment="left", + verticalalignment="center", + ) + + d = 0.01 + ax.set_xlim(0.0 - d, 2.0 + d) + ax.set_ylim(-1.0 - d, 1.0 + d) + ax.set_xticks([]) + ax.set_yticks([]) + if legend: + ax.set_frame_on(True) + ax.spines["left"].set_color("none") + ax.spines["right"].set_color("none") + ax.spines["top"].set_color("none") + ax.xaxis.set_ticks_position("bottom") + ax.spines["bottom"].set_position(("data", -1.2)) + ax.set_xticks([0, 2]) + ax.set_xticklabels(["0", "4.8 (mm)"]) + ax.text( + 0.0, + -1.1, + "$\longleftarrow$ Rostral", + verticalalignment="top", + horizontalalignment="left", + size=12, + ) + ax.text( + 2, + -1.1, + "Caudal $\longrightarrow$", + verticalalignment="top", + horizontalalignment="right", + size=12, + ) + else: + ax.set_frame_on(False) + if title: + ax.title(title) + + +# ----------------------------------------------------------------------------- +def polar_imshow(axis, Z, *args, **kwargs): + kwargs["interpolation"] = kwargs.get("interpolation", "nearest") + kwargs["cmap"] = kwargs.get("cmap", plt.cm.gray_r) + # kwargs['vmin'] = kwargs.get('vmin', Z.min()) + # kwargs['vmax'] = kwargs.get('vmax', Z.max()) + kwargs["vmin"] = kwargs.get("vmin", 0) + kwargs["vmax"] = kwargs.get("vmax", 1) + kwargs["origin"] = kwargs.get("origin", "lower") + axis.imshow(Z, extent=[0, 1, -1, 1], *args, **kwargs) + + +# ----------------------------------------------------------------------------- +def logpolar_imshow(axis, Z, *args, **kwargs): + kwargs["interpolation"] = kwargs.get("interpolation", "nearest") + kwargs["cmap"] = kwargs.get("cmap", plt.cm.gray_r) + # kwargs['vmin'] = kwargs.get('vmin', Z.min()) + # kwargs['vmax'] = kwargs.get('vmax', Z.max()) + kwargs["vmin"] = kwargs.get("vmin", 0) + kwargs["vmax"] = kwargs.get("vmax", 1) + kwargs["origin"] = kwargs.get("origin", "lower") + im = axis.imshow(Z, extent=[0, 2, -1, 1], *args, **kwargs) + # axins = inset_axes(axis, width='25%', height='5%', loc=3) + # vmin, vmax = Z.min(), Z.max() + # plt.colorbar(im, cax=axins, orientation='horizontal', ticks=[vmin,vmax], format = '%.2f') + # axins.xaxis.set_ticks_position('bottom') diff --git a/code/rules/helper.py b/code/rules/helper.py new file mode 100644 index 0000000..777a294 --- /dev/null +++ b/code/rules/helper.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright INRIA +# Contributors: Wahiba Taouali (Wahiba.Taouali@inria.fr) +# Nicolas P. Rougier (Nicolas.Rougier@inria.fr) +# +# This software is governed by the CeCILL license under French law and abiding +# by the rules of distribution of free software. You can use, modify and/ or +# redistribute the software under the terms of the CeCILL license as circulated +# by CEA, CNRS and INRIA at the following URL +# http://www.cecill.info/index.en.html. +# +# As a counterpart to the access to the source code and rights to copy, modify +# and redistribute granted by the license, users are provided only with a +# limited warranty and the software's author, the holder of the economic +# rights, and the successive licensors have only limited liability. +# +# In this respect, the user's attention is drawn to the risks associated with +# loading, using, modifying and/or developing or reproducing the software by +# the user in light of its specific status of free software, that may mean that +# it is complicated to manipulate, and that also therefore means that it is +# reserved for developers and experienced professionals having in-depth +# computer knowledge. Users are therefore encouraged to load and test the +# software's suitability as regards their requirements in conditions enabling +# the security of their systems and/or data to be ensured and, more generally, +# to use and operate it in the same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# ----------------------------------------------------------------------------- +import os +import numpy as np + +from parameters import * + + +def disc(shape=(1024, 1024), center=(512, 512), radius=512): + """ Generate a numpy array containing a disc. """ + + def distance(x, y): + return (x - center[0]) ** 2 + (y - center[1]) ** 2 + + D = np.fromfunction(distance, shape) + return np.where(D < radius * radius, 1.0, 0.0) + + +def gaussian(shape=(25, 25), width=0.5, center=0.0): + """ Generate a gaussian of the form g(x) = height*exp(-(x-center)**2/width**2). """ + if type(shape) in [float, int]: + shape = (shape,) + if type(width) in [float, int]: + width = (width,) * len(shape) + if type(center) in [float, int]: + center = (center,) * len(shape) + grid = [] + for size in shape: + grid.append(slice(0, size)) + C = np.mgrid[tuple(grid)] + R = np.zeros(shape) + for i, size in enumerate(shape): + if shape[i] > 1: + R += (((C[i] / float(size - 1)) * 2 - 1 - center[i]) / width[i]) ** 2 + return np.exp(-R / 2) + + +def stimulus(position, size, intensity): + """ + Parameters + ---------- + + position : (rho,theta) (degrees) + size : float (degrees) + intensity: float + """ + + x, y = cartesian(position[0] / 90.0, np.pi * position[1] / 180.0) + Y, X = np.mgrid[0 : shape[0], 0 : shape[1]] + X = X / float(shape[1]) + Y = 2 * Y / float(shape[0]) - 1 + R = (X - x) ** 2 + (Y - y) ** 2 + return np.exp(-0.5 * R / (size / 90.0)) + + +def best_fft_shape(shape): + """ + This function returns the best shape for computing a fft + + From fftw.org: + FFTW is best at handling sizes of the form 2^a*3^b*5^c*7^d*11^e*13^f, + where e+f is either 0 or 1, + + From http://www.netlib.org/fftpack/doc + "the method is most efficient when n is a product of small primes." + -> What is small ? + """ + + # fftpack (not sure of the base) + base = [13, 11, 7, 5, 3, 2] + # fftw + # base = [13,11,7,5,3,2] + + def factorize(n): + if n == 0: + raise (RuntimeError, "Length n must be positive integer") + elif n == 1: + return [ + 1, + ] + factors = [] + for b in base: + while n % b == 0: + n /= b + factors.append(b) + if n == 1: + return factors + return [] + + def is_optimal(n): + factors = factorize(n) + # fftpack + return len(factors) > 0 + # fftw + # return len(factors) > 0 and factors[:2] not in [[13,13],[13,11],[11,11]] + + shape = np.atleast_1d(np.array(shape)) + for i in range(shape.size): + while not is_optimal(shape[i]): + shape[i] += 1 + return shape.astype(int) diff --git a/code/rules/parameters.py b/code/rules/parameters.py new file mode 100644 index 0000000..f64346f --- /dev/null +++ b/code/rules/parameters.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright INRIA +# Contributors: Wahiba Taouali (Wahiba.Taouali@inria.fr) +# Nicolas P. Rougier (Nicolas.Rougier@inria.fr) +# +# This software is governed by the CeCILL license under French law and abiding +# by the rules of distribution of free software. You can use, modify and/ or +# redistribute the software under the terms of the CeCILL license as circulated +# by CEA, CNRS and INRIA at the following URL +# http://www.cecill.info/index.en.html. +# +# As a counterpart to the access to the source code and rights to copy, modify +# and redistribute granted by the license, users are provided only with a +# limited warranty and the software's author, the holder of the economic +# rights, and the successive licensors have only limited liability. +# +# In this respect, the user's attention is drawn to the risks associated with +# loading, using, modifying and/or developing or reproducing the software by +# the user in light of its specific status of free software, that may mean that +# it is complicated to manipulate, and that also therefore means that it is +# reserved for developers and experienced professionals having in-depth +# computer knowledge. Users are therefore encouraged to load and test the +# software's suitability as regards their requirements in conditions enabling +# the security of their systems and/or data to be ensured and, more generally, +# to use and operate it in the same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# ----------------------------------------------------------------------------- +import numpy as np + +second = 1.0 +millisecond = 0.001 +dt = 5 * millisecond +duration = 10 * second +noise = 0.01 + +retina_shape = np.array([4096, 2048]).astype(int) +projection_shape = np.array([512, 512]).astype(int) +n = 128 +colliculus_shape = np.array([n, n]).astype(int) + +# Default stimulus +stimulus_size = 1.5 # in degrees +stimulus_intensity = 1.5 + +# DNF parameters (linear) +sigma_e = 0.10 +A_e = 1.30 +sigma_i = 1.00 +A_i = 0.65 +alpha = 12.5 +tau = 10 * millisecond +scale = 40.0 * 40.0 / (n * n) diff --git a/code/rules/projections.py b/code/rules/projections.py new file mode 100644 index 0000000..6ae510e --- /dev/null +++ b/code/rules/projections.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright INRIA +# Contributors: Wahiba Taouali (Wahiba.Taouali@inria.fr) +# Nicolas P. Rougier (Nicolas.Rougier@inria.fr) +# +# This software is governed by the CeCILL license under French law and abiding +# by the rules of distribution of free software. You can use, modify and/ or +# redistribute the software under the terms of the CeCILL license as circulated +# by CEA, CNRS and INRIA at the following URL +# http://www.cecill.info/index.en.html. +# +# As a counterpart to the access to the source code and rights to copy, modify +# and redistribute granted by the license, users are provided only with a +# limited warranty and the software's author, the holder of the economic +# rights, and the successive licensors have only limited liability. +# +# In this respect, the user's attention is drawn to the risks associated with +# loading, using, modifying and/or developing or reproducing the software by +# the user in light of its specific status of free software, that may mean that +# it is complicated to manipulate, and that also therefore means that it is +# reserved for developers and experienced professionals having in-depth +# computer knowledge. Users are therefore encouraged to load and test the +# software's suitability as regards their requirements in conditions enabling +# the security of their systems and/or data to be ensured and, more generally, +# to use and operate it in the same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# ----------------------------------------------------------------------------- +import os +import numpy as np +from parameters import * + + +def cartesian_to_polar(x, y): + """ Cartesian to polar coordinates. """ + + rho = np.sqrt(x ** 2 + y ** 2) + theta = np.arctan2(y, x) + return rho, theta + + +def polar_to_cartesian(rho, theta): + """ Polar to cartesian coordinates. """ + + x = rho * np.cos(theta) + y = rho * np.sin(theta) + return x, y + + +def polar_to_logpolar(rho, theta): + """ Polar to logpolar coordinates. """ + + # Shift in the SC mapping function in deg + A = 3.0 + # Collicular magnification along u axe in mm/rad + Bx = 1.4 + # Collicular magnification along v axe in mm/rad + By = 1.8 + xmin, xmax = 0.0, 4.80743279742 + ymin, ymax = -2.76745559565, 2.76745559565 + rho = rho * 90.0 + x = Bx * np.log(np.sqrt(rho * rho + 2 * A * rho * np.cos(theta) + A * A) / A) + y = By * np.arctan(rho * np.sin(theta) / (rho * np.cos(theta) + A)) + x = (x - xmin) / (xmax - xmin) + y = (y - ymin) / (ymax - ymin) + return x, y + + +def retina_projection(Rs=retina_shape, Ps=projection_shape): + """ + Compute the projection indices from retina to colliculus + + Parameters + ---------- + + Rs : (int,int) + Half-retina shape + + Ps : (int,int) + Retina projection shape (might be different from colliculus) + """ + + filename = "retina (%d,%d) - colliculus (%d,%d).npy" % (Rs[0], Rs[1], Ps[0], Ps[1]) + if os.path.exists(filename): + return np.load(filename) + + s = 4 + rho = (np.logspace(start=0, stop=1, num=s * Rs[1], base=10) - 1) / 9.0 + theta = np.linspace(start=-np.pi / 2, stop=np.pi / 2, num=s * Rs[0]) + + rho = rho.reshape((s * Rs[1], 1)) + rho = np.repeat(rho, s * Rs[0], axis=1) + + theta = theta.reshape((1, s * Rs[0])) + theta = np.repeat(theta, s * Rs[1], axis=0) + + y, x = polar_to_cartesian(rho, theta) + + xmin, xmax = x.min(), x.max() + x = (x - xmin) / (xmax - xmin) + + ymin, ymax = y.min(), y.max() + y = (y - ymin) / (ymax - ymin) + + P = np.zeros((Ps[0], Ps[1], 2), dtype=int) + xi = np.rint(x * (Rs[0] - 1)).astype(int) + yi = np.rint((0.0 + 1.0 * y) * (Rs[1] - 1)).astype(int) + + yc, xc = polar_to_logpolar(rho, theta) + xmin, xmax = xc.min(), xc.max() + xc = (xc - xmin) / (xmax - xmin) + ymin, ymax = yc.min(), yc.max() + yc = (yc - ymin) / (ymax - ymin) + xc = np.rint(xc * (Ps[0] - 1)).astype(int) + yc = np.rint((0.0 + yc * 1.0) * (Ps[1] - 1)).astype(int) + + P[xc, yc, 0] = xi + P[xc, yc, 1] = yi + np.save(filename, P) + return P diff --git a/code/rules/rule-1.py b/code/rules/rule-1.py new file mode 100644 index 0000000..04f2171 --- /dev/null +++ b/code/rules/rule-1.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2014, Matplotlib Development Team. All Rights Reserved. +# Distributed under the (new) BSD License. See LICENSE.txt for more info. +# +# Author: Nicolas P. Rougier +# Source: New York Times graphics, 2007 +# -> http://www.nytimes.com/imagepages/2007/07/29/health/29cancer.graph.web.html +# ----------------------------------------------------------------------------- +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.patches as patches + +# ---------- +# Data to be represented +diseases = [ + "Kidney Cancer", + "Bladder Cancer", + "Esophageal Cancer", + "Ovarian Cancer", + "Liver Cancer", + "Non-Hodgkin's\nlymphoma", + "Leukemia", + "Prostate Cancer", + "Pancreatic Cancer", + "Breast Cancer", + "Colorectal Cancer", + "Lung Cancer", +] +men_deaths = [ + 10000, + 12000, + 13000, + 0, + 14000, + 12000, + 16000, + 25000, + 20000, + 500, + 25000, + 80000, +] +men_cases = [ + 30000, + 50000, + 13000, + 0, + 16000, + 30000, + 25000, + 220000, + 22000, + 600, + 55000, + 115000, +] +women_deaths = [ + 6000, + 5500, + 5000, + 20000, + 9000, + 12000, + 13000, + 0, + 19000, + 40000, + 30000, + 70000, +] +women_cases = [ + 20000, + 18000, + 5000, + 25000, + 9000, + 29000, + 24000, + 0, + 21000, + 160000, + 55000, + 97000, +] + +# ---------- +# Choose some nice colors +matplotlib.rc("axes", facecolor="white") +matplotlib.rc("figure.subplot", wspace=0.65) +matplotlib.rc("grid", color="white") +matplotlib.rc("grid", linewidth=1) + +# Make figure background the same colors as axes +fig = plt.figure(figsize=(12, 7), facecolor="white") + + +# ---WOMEN data --- +axes_left = plt.subplot(121) + +# Keep only top and right spines +axes_left.spines["left"].set_color("none") +axes_left.spines["right"].set_zorder(10) +axes_left.spines["bottom"].set_color("none") +axes_left.xaxis.set_ticks_position("top") +axes_left.yaxis.set_ticks_position("right") +axes_left.spines["top"].set_position(("data", len(diseases) + 0.25)) +axes_left.spines["top"].set_color("w") + +# Set axes limits +plt.xlim(200000, 0) +plt.ylim(0, len(diseases)) + +# Set ticks labels +plt.xticks([150000, 100000, 50000, 0], ["150,000", "100,000", "50,000", "WOMEN"]) +axes_left.get_xticklabels()[-1].set_weight("bold") +axes_left.get_xticklines()[-1].set_markeredgewidth(0) +for label in axes_left.get_xticklabels(): + label.set_fontsize(10) +plt.yticks([]) + + +# Plot data +for i in range(len(women_deaths)): + H, h = 0.8, 0.55 + # Death + value = women_cases[i] + p = patches.Rectangle( + (0, i + (1 - H) / 2.0), + value, + H, + fill=True, + transform=axes_left.transData, + lw=0, + facecolor="red", + alpha=0.1, + ) + axes_left.add_patch(p) + # New cases + value = women_deaths[i] + p = patches.Rectangle( + (0, i + (1 - h) / 2.0), + value, + h, + fill=True, + transform=axes_left.transData, + lw=0, + facecolor="red", + alpha=0.5, + ) + axes_left.add_patch(p) + +# Add a grid +axes_left.grid() + +plt.text(165000, 8.2, "Leading Causes\nOf Cancer Deaths", fontsize=18, va="top") +plt.text( + 165000, + 7, + """In 2007, there were more\n""" + """than 1.4 million new cases\n""" + """of cancer in the United States.""", + va="top", + fontsize=10, +) + +# --- MEN data --- +axes_right = plt.subplot(122, sharey=axes_left) + +# Keep only top and left spines +axes_right.spines["right"].set_color("none") +axes_right.spines["left"].set_zorder(10) +axes_right.spines["bottom"].set_color("none") +axes_right.xaxis.set_ticks_position("top") +axes_right.yaxis.set_ticks_position("left") +axes_right.spines["top"].set_position(("data", len(diseases) + 0.25)) +axes_right.spines["top"].set_color("w") + + +# Set axes limits +plt.xlim(0, 200000) +plt.ylim(0, len(diseases)) + +# Set ticks labels +plt.xticks( + [0, 50000, 100000, 150000, 200000], + ["MEN", "50,000", "100,000", "150,000", "200,000"], +) +axes_right.get_xticklabels()[0].set_weight("bold") +for label in axes_right.get_xticklabels(): + label.set_fontsize(10) +axes_right.get_xticklines()[1].set_markeredgewidth(0) +plt.yticks([]) + +# Plot data +for i in range(len(men_deaths)): + H, h = 0.8, 0.55 + # Death + value = men_cases[i] + p = patches.Rectangle( + (0, i + (1 - H) / 2.0), + value, + H, + fill=True, + transform=axes_right.transData, + lw=0, + facecolor="blue", + alpha=0.1, + ) + axes_right.add_patch(p) + # New cases + value = men_deaths[i] + p = patches.Rectangle( + (0, i + (1 - h) / 2.0), + value, + h, + fill=True, + transform=axes_right.transData, + lw=0, + facecolor="blue", + alpha=0.5, + ) + axes_right.add_patch(p) + +# Add a grid +axes_right.grid() + +# Y axis labels +# We want them to be exactly in the middle of the two y spines +# and it requires some computations +for i in range(len(diseases)): + x1, y1 = axes_left.transData.transform_point((0, i + 0.5)) + x2, y2 = axes_right.transData.transform_point((0, i + 0.5)) + x, y = fig.transFigure.inverted().transform_point(((x1 + x2) / 2, y1)) + plt.text( + x, + y, + diseases[i], + transform=fig.transFigure, + fontsize=10, + horizontalalignment="center", + verticalalignment="center", + ) + + +# Devil hides in the details... +arrowprops = dict(arrowstyle="-", connectionstyle="angle,angleA=0,angleB=90,rad=0") +x = women_cases[-1] +axes_left.annotate( + "NEW CASES", + xy=(0.9 * x, 11.5), + xycoords="data", + horizontalalignment="right", + fontsize=10, + xytext=(-40, -3), + textcoords="offset points", + arrowprops=arrowprops, +) + +x = women_deaths[-1] +axes_left.annotate( + "DEATHS", + xy=(0.85 * x, 11.5), + xycoords="data", + horizontalalignment="right", + fontsize=10, + xytext=(-50, -25), + textcoords="offset points", + arrowprops=arrowprops, +) + +x = men_cases[-1] +axes_right.annotate( + "NEW CASES", + xy=(0.9 * x, 11.5), + xycoords="data", + horizontalalignment="left", + fontsize=10, + xytext=(+40, -3), + textcoords="offset points", + arrowprops=arrowprops, +) + +x = men_deaths[-1] +axes_right.annotate( + "DEATHS", + xy=(0.9 * x, 11.5), + xycoords="data", + horizontalalignment="left", + fontsize=10, + xytext=(+50, -25), + textcoords="offset points", + arrowprops=arrowprops, +) + + +# Done +plt.savefig("../../figures/rules/rule-1.pdf") +plt.show() diff --git a/code/rules/rule-2.py b/code/rules/rule-2.py new file mode 100644 index 0000000..68c79e6 --- /dev/null +++ b/code/rules/rule-2.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright INRIA +# Contributors: Wahiba Taouali (Wahiba.Taouali@inria.fr) +# Nicolas P. Rougier (Nicolas.Rougier@inria.fr) +# +# This software is governed by the CeCILL license under French law and abiding +# by the rules of distribution of free software. You can use, modify and/ or +# redistribute the software under the terms of the CeCILL license as circulated +# by CEA, CNRS and INRIA at the following URL +# http://www.cecill.info/index.en.html. +# +# As a counterpart to the access to the source code and rights to copy, modify +# and redistribute granted by the license, users are provided only with a +# limited warranty and the software's author, the holder of the economic +# rights, and the successive licensors have only limited liability. +# +# In this respect, the user's attention is drawn to the risks associated with +# loading, using, modifying and/or developing or reproducing the software by +# the user in light of its specific status of free software, that may mean that +# it is complicated to manipulate, and that also therefore means that it is +# reserved for developers and experienced professionals having in-depth +# computer knowledge. Users are therefore encouraged to load and test the +# software's suitability as regards their requirements in conditions enabling +# the security of their systems and/or data to be ensured and, more generally, +# to use and operate it in the same conditions as regards security. +# +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. +# ----------------------------------------------------------------------------- +import os +import numpy as np + +from helper import * +from graphics import * +from projections import * + + +if __name__ == "__main__": + import matplotlib.pyplot as plt + from matplotlib.patches import Polygon + from mpl_toolkits.axes_grid1 import ImageGrid + from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes + from mpl_toolkits.axes_grid1.inset_locator import mark_inset + + P = retina_projection() + + # Checkerboard pattern for retina + grid = 2 * 32 + even = int(grid / 2) * [0, 1] + odd = int(grid / 2) * [1, 0] + R = np.row_stack(int(grid / 2) * (even, odd)) + R = R.repeat(grid, axis=0).repeat(grid, axis=1) + + # Mask with a disc + R = R * disc( + (retina_shape[0], retina_shape[0]), + (retina_shape[0] // 2, retina_shape[0] // 2), + retina_shape[0] // 2, + ) + + # Take half-retina + R = R[:, retina_shape[1] :] + + # Project to colliculus + SC = R[P[..., 0], P[..., 1]] + + fig = plt.figure(figsize=(10, 8), facecolor="w") + ax1, ax2 = ImageGrid(fig, 111, nrows_ncols=(1, 2), axes_pad=0.5) + polar_frame(ax1, legend=True) + polar_imshow(ax1, R, vmin=0, vmax=5) + logpolar_frame(ax2, legend=True) + logpolar_imshow(ax2, SC, vmin=0, vmax=5) + + plt.savefig("../../figures/rules/rule-2.pdf") + plt.show() diff --git a/code/rules/rule-3.py b/code/rules/rule-3.py new file mode 100644 index 0000000..3b2baad --- /dev/null +++ b/code/rules/rule-3.py @@ -0,0 +1,111 @@ +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes +from mpl_toolkits.axes_grid1.inset_locator import mark_inset + + +def simulate(): + d = 0.005 + x = np.random.uniform(0, d) + y = d - x + x, y = np.random.uniform(0, d, 2) + + dt = 0.05 + t = 35.0 + alpha = 0.25 + n = int(t / dt) + X = np.zeros(n) + Y = np.zeros(n) + C = np.random.randint(0, 2, n) + + for i in range(n): + # Asynchronous + if 0: + if C[i]: + x += (alpha + (x - y)) * (1 - x) * dt + x = max(x, 0.0) + y += (alpha + (y - x)) * (1 - y) * dt + y = max(y, 0.0) + else: + y += (alpha + (y - x)) * (1 - y) * dt + y = max(y, 0.0) + x += (alpha + (x - y)) * (1 - x) * dt + x = max(x, 0.0) + # Synchronous + else: + dx = (alpha + (x - y)) * (1 - x) * dt + dy = (alpha + (y - x)) * (1 - y) * dt + x = max(x + dx, 0.0) + y = max(y + dy, 0.0) + X[i] = x + Y[i] = y + return X, Y + + +np.random.seed(11) +S = [] +n = 250 +for i in range(n): + S.append(simulate()) + + +plt.figure(figsize=(20, 10)) +ax = plt.subplot(121, aspect=1) +axins = zoomed_inset_axes(ax, 25, loc=3) +for i in range(n): + X, Y = S[i] + if X[-1] > 0.9 and Y[-1] > 0.9: + c = "r" + lw = 1.0 + axins.scatter(X[0], Y[0], c="r", edgecolor="w", zorder=10) + else: + c = "b" + lw = 1.0 + ax.plot(X, Y, c=c, alpha=0.25, lw=lw) + axins.plot(X, Y, c=c, alpha=0.25, lw=lw) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +ax.set_xlabel("x position") +ax.set_ylabel("y position") +ax.set_title("%d trajectories of a dual particle system (x,y)" % n) +axins.set_xlim(0.01, 0.02) +axins.set_xticks([]) +axins.set_ylim(0.01, 0.02) +axins.set_yticks([]) + +ax = plt.subplot(122, aspect=1) +axins = zoomed_inset_axes(ax, 50, loc=3) +axins.set_facecolor((1, 1, 0.9)) +n = 9 +for i in range(n): + X, Y = S[i] + ls = "-" + if i == 2: + ls = "--" + if X[-1] > 0.9 and Y[-1] > 0.9: + c = "r" + lw = 2.0 + axins.scatter(X[0], Y[0], s=150, c="r", edgecolor="w", zorder=10, lw=2) + else: + c = "b" + lw = 2.0 + ax.plot(X, Y, c=c, alpha=0.75, lw=lw, ls=ls) + axins.plot(X, Y, c=c, alpha=0.75, lw=lw, ls=ls) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +ax.set_xticks([0, 1]) +ax.set_yticks([0, 1]) +ax.set_xticklabels(["0", "1"], fontsize=16) +ax.set_yticklabels(["0", "1"], fontsize=16) +ax.set_xlabel("x position", fontsize=20) +ax.set_ylabel("y position", fontsize=20) +# ax.set_title('%d trajectories of a dual particle system (x,y)' % n) +axins.set_xlim(0.01, 0.02) +axins.set_xticks([]) +axins.set_ylim(0.01, 0.02) +axins.set_yticks([]) + +plt.savefig("../../figures/rules/rule-3.pdf") +plt.show() diff --git a/code/rules/rule-5-left.py b/code/rules/rule-5-left.py new file mode 100644 index 0000000..63e1624 --- /dev/null +++ b/code/rules/rule-5-left.py @@ -0,0 +1,11 @@ +from pylab import * + +fig = plt.figure(figsize=(8, 4)) + +n = 256 +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) +plot(X, C), plot(X, S) + +savefig("../../figures/rules/rule-5-left.pdf") +show() diff --git a/code/rules/rule-5-right.py b/code/rules/rule-5-right.py new file mode 100644 index 0000000..12299e3 --- /dev/null +++ b/code/rules/rule-5-right.py @@ -0,0 +1,63 @@ +from pylab import * + +figure(figsize=(8, 5), dpi=80) +subplot(111) + +X = np.linspace(-np.pi, np.pi, 256, endpoint=True) +C, S = np.cos(X), np.sin(X) + +plot(X, C, color="blue", linewidth=2.5, linestyle="-", label="cosine") +plot(X, S, color="red", linewidth=2.5, linestyle="-", label="sine") + +ax = gca() +ax.spines["right"].set_color("none") +ax.spines["top"].set_color("none") +ax.xaxis.set_ticks_position("bottom") +ax.spines["bottom"].set_position(("data", 0)) +ax.yaxis.set_ticks_position("left") +ax.spines["left"].set_position(("data", 0)) + +xlim(X.min() * 1.1, X.max() * 1.1) +xticks( + [-np.pi, -np.pi / 2, 0, np.pi / 2, np.pi], + [r"$-\pi$", r"$-\pi/2$", r"$0$", r"$+\pi/2$", r"$+\pi$"], +) + +ylim(C.min() * 1.1, C.max() * 1.1) +yticks([-1, +1], [r"$-1$", r"$+1$"]) + + +legend(loc="upper left") + +t = 2 * np.pi / 3 +plot([t, t], [0, np.cos(t)], color="blue", linewidth=1.5, linestyle="--") +scatter([t,], [np.cos(t),], 50, color="blue") +annotate( + r"$sin(\frac{2\pi}{3})=\frac{\sqrt{3}}{2}$", + xy=(t, np.sin(t)), + xycoords="data", + xytext=(+10, +30), + textcoords="offset points", + fontsize=16, + arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"), +) + +plot([t, t], [0, np.sin(t)], color="red", linewidth=1.5, linestyle="--") +scatter([t,], [np.sin(t),], 50, color="red") +annotate( + r"$cos(\frac{2\pi}{3})=-\frac{1}{2}$", + xy=(t, np.cos(t)), + xycoords="data", + xytext=(-90, -50), + textcoords="offset points", + fontsize=16, + arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"), +) + +for label in ax.get_xticklabels() + ax.get_yticklabels(): + label.set_fontsize(16) + label.set_bbox(dict(facecolor="white", edgecolor="None", alpha=0.65)) + + +savefig("../../figures/rules/rule-5-right.pdf") +show() diff --git a/code/rules/rule-5.tex b/code/rules/rule-5.tex new file mode 100644 index 0000000..8a73908 --- /dev/null +++ b/code/rules/rule-5.tex @@ -0,0 +1,6 @@ +\documentclass[varwidth]{standalone} +\usepackage{graphicx} +\begin{document} +\includegraphics[height=3cm]{../../figures/rules/rule-5-left.pdf} +\includegraphics[height=3cm]{../../figures/rules/rule-5-right.pdf} +\end{document} diff --git a/code/rules/rule-6.py b/code/rules/rule-6.py new file mode 100644 index 0000000..5e0786b --- /dev/null +++ b/code/rules/rule-6.py @@ -0,0 +1,71 @@ +import numpy as np +import matplotlib + +import matplotlib.pylab as plt +import matplotlib.patheffects as PathEffects +from matplotlib.ticker import MultipleLocator +from mpl_toolkits.axes_grid1.inset_locator import inset_axes +import matplotlib.gridspec as gridspec + + +def make(ax1, ax2, cmap, title, y, color="k"): + # ----------------- + ax1.set_xlim(0, 1) + ax1.set_ylim(0, 1) + ax1.set_xticks([]) + ax1.set_yticks([0, 0.5, 1]) + ax1.get_yaxis().tick_left() + + ax1.axhline(y, lw=1, c=color, xmin=0, xmax=1) + ax1.text(0.025, y + 0.015, "Slice y=%.2f" % y, fontsize=10, color=color) + ax1.imshow(Z, cmap=cmap, origin="upper", extent=[0, 1, 0, 1]) + ax1.set_xticks([]), ax1.set_yticks([]) + ax1.set_title(title) + + ax2.set_xlim(0, 1) + ax2.set_ylim(-0.1, +1.1) + ax2.set_xticks([0, 0.5, 1]) + ax2.get_xaxis().tick_bottom() + ax2.set_yticks([0, 1]) + ax2.get_yaxis().tick_left() + ax2.plot(T / np.pi, Z[int(1024 * (1 - y))], c="k", lw=0.5) + ax2.axis("off") + ax2.text(0.025, 1.25, "Slice detail") + + +if __name__ == "__main__": + fg = 0.0, 0.0, 0.0 + bg = 1.0, 1.0, 1.0 + matplotlib.rcParams["xtick.direction"] = "out" + matplotlib.rcParams["ytick.direction"] = "out" + matplotlib.rcParams["font.size"] = 12.0 + matplotlib.rc("axes", facecolor=bg) + matplotlib.rc("axes", edgecolor=fg) + matplotlib.rc("xtick", color=fg) + matplotlib.rc("ytick", color=fg) + matplotlib.rc("figure", facecolor=bg) + matplotlib.rc("savefig", facecolor=bg) + + plt.figure(figsize=(18, 6)) + + G = gridspec.GridSpec(2, 3, width_ratios=[1, 1, 1], height_ratios=[15, 1]) + + T = np.linspace(0, np.pi, 2 * 512) + X, Y = np.meshgrid(T, T) + Z = np.power(Y / 2, 5) * np.sin(np.exp(np.pi / 2 * X)) + Z = (Z - Z.min()) / (Z.max() - Z.min()) + + ax1 = plt.subplot(G[0, 0], aspect=1) + ax2 = plt.subplot(G[1, 0]) + make(ax1, ax2, plt.cm.rainbow, "Rainbow colormap (qualitative)", y=0.3) + + ax1 = plt.subplot(G[0, 1], aspect=1) + ax2 = plt.subplot(G[1, 1]) + make(ax1, ax2, plt.cm.seismic, "Seismic colormap (diverging)", y=0.2) + + ax1 = plt.subplot(G[0, 2], aspect=1) + ax2 = plt.subplot(G[1, 2]) + make(ax1, ax2, plt.cm.Purples, "Purples colormap (sequential)", 0.1, "white") + + plt.savefig("../../figures/rules/rule-6.pdf") + plt.show() diff --git a/code/rules/rule-7.py b/code/rules/rule-7.py new file mode 100644 index 0000000..d6a4cce --- /dev/null +++ b/code/rules/rule-7.py @@ -0,0 +1,71 @@ +import numpy as np +import matplotlib.pyplot as plt + +n = 20 +Z = np.linspace(0, 1, n * n).reshape(n, n) +Z = (Z - Z.min()) / (Z.max() - Z.min()) +Z += np.random.uniform(0, 1, (n, n)) + +plt.figure(figsize=(20, 8)) + +ax = plt.subplot(1, 2, 1, aspect=1) +values = [30.0, 20.0, 15.0, 10.0] +x, y = 0.0, 0.5 +for value in values: + r1 = 0.5 * (value / values[0]) + r2 = 0.5 * ((np.sqrt(value / np.pi)) / (np.sqrt(values[0] / np.pi))) + ax.add_artist(plt.Circle((x + r2, y), r1, color="r")) + ax.add_artist(plt.Circle((x + r2, 1.5 + y), r2, color="k")) + fontsize = 2 * value + # plt.text(x+r2, y, "%d" % value, color="white", family = "Times New Roman", + # fontsize=fontsize, ha="center", va="center") + # plt.text(x+r2, 1.5+y, "%d" % value, color="white", family = "Times New Roman", + # fontsize=fontsize, ha="center", va="center") + x += 2 * r2 + 0.05 +plt.axhline(1.25, c="k") +plt.text( + 0.0, + 1.25 + 0.05, + "Relative size using disc area", + ha="left", + va="bottom", + color=".25", +) +plt.text( + 0.0, + 1.25 - 0.05, + "Relative size using disc radius", + ha="left", + va="top", + color=".25", +) +plt.xlim(-0.05, 3.5) +plt.ylim(-0.05, 2.6) +plt.axis("off") + + +ax = plt.subplot(1, 2, 2, aspect=1) + +plt.axhline(5, c="k") +plt.text( + 0.0, 5 + 0.15, "Relative size using full range", ha="left", va="bottom", color=".25" +) +plt.text( + 0.0, 5 - 0.15, "Relative size using partial range", ha="left", va="top", color=".25" +) + + +n = 10 +np.random.seed(123) +X = 0.25 + np.arange(n) +Y = 2 + np.random.uniform(0.75, 0.85, n) + +plt.bar(X, 5 * (Y - 2.2), 0.9, color="k", ec="None", bottom=6) +plt.bar(X, 20 * (Y - 2.75), 0.9, color="r", ec="None", bottom=1) + +plt.xlim(-0.05, 10.5) +plt.ylim(-0.05, 10.5) +plt.axis("off") + +plt.savefig("../../figures/rules/rule-7.pdf") +plt.show() diff --git a/code/rules/rule-8.py b/code/rules/rule-8.py new file mode 100644 index 0000000..b4d8720 --- /dev/null +++ b/code/rules/rule-8.py @@ -0,0 +1,55 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Data +# ----------------------------------------------------------------------------- +p, n = 7, 32 +X = np.linspace(0, 2, n) +Y = np.random.uniform(-0.75, 0.5, (p, n)) + +# ----------------------------------------------------------------------------- +fig = plt.figure(figsize=(20, 8)) +ax = plt.subplot(1, 2, 1, aspect=1) +ax.patch.set_facecolor((1, 1, 0.75)) +for i in range(p): + plt.plot(X, Y[i], label="Series %d " % (1 + i), lw=2) +plt.xlim(0, 2) +plt.ylim(-1, 1) +plt.yticks(np.linspace(-1, 1, 18)) +plt.xticks(np.linspace(0, 2, 18)) +plt.legend() +plt.grid() + +# ----------------------------------------------------------------------------- +ax = plt.subplot(1, 2, 2, aspect=1) +Yy = p - (np.arange(p) + 0.5) +Xx = [p,] * p +rects = plt.barh( + Yy, Xx, align="center", height=0.75, color=".95", ec="None", zorder=-20 +) +plt.xlim(0, p), plt.ylim(0, p) + +for i in range(p): + label = "Series %d" % (1 + i) + plt.text(-0.1, Yy[i], label, ha="right", fontsize=16) + plt.axvline(0, (Yy[i] - 0.4) / p, (Yy[i] + 0.4) / p, c="k", lw=3) + plt.axvline( + 0.25 * p, (Yy[i] - 0.375) / p, (Yy[i] + 0.375) / p, c=".5", lw=0.5, zorder=-15 + ) + plt.axvline( + 0.50 * p, (Yy[i] - 0.375) / p, (Yy[i] + 0.375) / p, c=".5", lw=0.5, zorder=-15 + ) + plt.axvline( + 0.75 * p, (Yy[i] - 0.375) / p, (Yy[i] + 0.375) / p, c=".5", lw=0.5, zorder=-15 + ) + plt.plot(X * p / 2, i + 0.5 + 2 * Y[i] / p, c="k", lw=2) + for j in range(p): + if i != j: + plt.plot(X * p / 2, i + 0.5 + 2 * Y[j] / p, c=".5", lw=0.5, zorder=-10) +plt.text(0.25 * p, 0, "0.5", va="top", ha="center", fontsize=10) +plt.text(0.50 * p, 0, "1.0", va="top", ha="center", fontsize=10) +plt.text(0.75 * p, 0, "1.5", va="top", ha="center", fontsize=10) +plt.axis("off") + +plt.savefig("../../figures/rules/rule-8.pdf") +plt.show() diff --git a/code/rules/rule-9.py b/code/rules/rule-9.py new file mode 100644 index 0000000..0bbfc62 --- /dev/null +++ b/code/rules/rule-9.py @@ -0,0 +1,60 @@ +import numpy as np + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +plt.xkcd() + +X = np.linspace(0, 2.32, 100) +Y = X * X - 5 * np.exp(-5 * (X - 2) * (X - 2)) + +fig = plt.figure(figsize=(12, 5), dpi=72, facecolor="white") +axes = plt.subplot(111) + +plt.plot(X, Y, color="k", linewidth=2, linestyle="-", zorder=+10) + +axes.set_xlim(X.min(), X.max()) +axes.set_ylim(1.01 * Y.min(), 1.01 * Y.max()) + +axes.spines["right"].set_color("none") +axes.spines["top"].set_color("none") +axes.xaxis.set_ticks_position("bottom") +axes.spines["bottom"].set_position(("data", 0)) +axes.yaxis.set_ticks_position("left") +axes.spines["left"].set_position(("data", X.min())) + +axes.set_xticks([]) +axes.set_yticks([]) +axes.set_xlim(1.05 * X.min(), 1.10 * X.max()) +axes.set_ylim(1.15 * Y.min(), 1.05 * Y.max()) + +t = [10, 40, 82, 88, 93, 99] +plt.scatter(X[t], Y[t], s=50, zorder=+12, c="k") + +plt.text(X[t[0]] - 0.1, Y[t[0]] + 0.1, "Industrial\nRobot", ha="left", va="bottom") +plt.text(X[t[1]] - 0.15, Y[t[1]] + 0.1, "Humanoid\nRobot", ha="left", va="bottom") +plt.text(X[t[2]] - 0.25, Y[t[2]], "Zombie", ha="left", va="center") +plt.text(X[t[3]] + 0.05, Y[t[3]], "Prosthetic\nHand", ha="left", va="center") +plt.text(X[t[4]] + 0.05, Y[t[4]], "Bunraku\nPuppet", ha="left", va="center") +plt.text(X[t[5]] + 0.05, Y[t[5]], "Human", ha="left", va="center") +plt.text(X[t[2]] - 0.05, 1.5, "Uncanny\nValley", ha="center", va="center", fontsize=24) + +plt.ylabel("- Comfort Level +", y=0.5, fontsize=20) +plt.text(0.05, -0.1, "Human Likeness ->", ha="left", va="top", color="r", fontsize=20) + +X = np.linspace(0, 1.1 * 2.32, 100) +axes.fill_between(X, 0, -10, color="0.85", zorder=-1) +axes.fill_between(X, 0, +10, color=(1.0, 1.0, 0.9), zorder=-1) + +# X = np.linspace(1.652,2.135,100) +X = np.linspace(1.5, 2.25, 100) +Y = X * X - 5 * np.exp(-5 * (X - 2) * (X - 2)) +axes.fill_between(X, Y, +10, color=(1, 1, 1), zorder=-1) + +axes.axvline(x=1.5, ymin=0, ymax=1, color=".5", ls="--") +axes.axvline(x=2.25, ymin=0, ymax=1, color=".5", ls="--") + +plt.savefig("../../figures/rules/rule-9.pdf") +plt.show() diff --git a/code/scales-projections/geo-projections.py b/code/scales-projections/geo-projections.py new file mode 100644 index 0000000..50a88ef --- /dev/null +++ b/code/scales-projections/geo-projections.py @@ -0,0 +1,51 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +import cartopy.crs + +mpl.rcParams["font.serif"] = "Roboto Slab" +mpl.rcParams["font.size"] = 10 +mpl.rcParams["font.weight"] = 400 +mpl.rcParams["font.family"] = "serif" + +dpi = 100 +inch = 2.54 +fig_width = 2 * 10.85 / inch +fig_height = fig_width * 1.25 + +fig = plt.figure(figsize=(fig_width, fig_height), dpi=dpi) + +projections = [ + (cartopy.crs.Mercator(), "Mercator",), + (cartopy.crs.PlateCarree(), "Plate Carree"), + (cartopy.crs.LambertCylindrical(), "Lambert Cylindrical"), + (cartopy.crs.Orthographic(), "Orthographic"), + (cartopy.crs.Mollweide(), "Mollweide"), + (cartopy.crs.EqualEarth(), "Equal Earth"), + (cartopy.crs.LambertConformal(), "Lambert Conformal"), + (cartopy.crs.EquidistantConic(), "Equidistant Conic"), + (cartopy.crs.AlbersEqualArea(), "Albers Equal Area"), + ( + cartopy.crs.NearsidePerspective( + central_latitude=50.72, central_longitude=-3.53, satellite_height=10000000.0 + ), + "Nearside Perspective", + ), + (cartopy.crs.NorthPolarStereo(), "North Polar Stereo"), + (cartopy.crs.SouthPolarStereo(), "South Polar Stereo"), +] + + +for i, (projection, name) in enumerate(projections): + ax = plt.subplot(4, 3, i + 1, projection=projection, frameon="False") + ax.coastlines(resolution="110m", linewidth=0.5) + ax.stock_img() + ax.set_title(name, weight=400) + +plt.savefig("../../figures/scales-projections/geo-projections.png", dpi=300) +plt.show() diff --git a/code/scales-projections/polar-patterns.py b/code/scales-projections/polar-patterns.py new file mode 100644 index 0000000..5741d80 --- /dev/null +++ b/code/scales-projections/polar-patterns.py @@ -0,0 +1,131 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +def shotgun_pattern(T, a, b, c=1, d=1, e=10000): + """ + Equations taken from [1] or [2] + some modifications by Matthieu LEROY + in order to squish the pattern and make a log-based pattern. + + [1] https://math.stackexchange.com/questions/1808380/non-symmetrical-lemniscate-curve-parameterization + [2] https://www.jneurosci.org/content/22/18/8201 + + Parameters + ---------- + T: array-like of floats + Angle between -pi and pi. + a: float + Amplitude. + b: float + Asymetric parameter. + c: float + Squish parameter?? + d: float + Not sure what it does. + e: float + Not sure what it does. + + Returns + ------- + Shotgun pattern. + """ + x = a * (np.cos(T) + b) * np.cos(T) / (c + np.sin(T) ** 2) + y = d * x * np.sin(T) + res = np.sqrt(x ** 2 + y ** 2) # to polar coordinates + return np.exp(res) / e # to get log-based plots after + + +def plot(ax, title, alpha=1, shotgun=False): + T = np.linspace(-np.pi, np.pi, 5000) + if shotgun: + R = shotgun_pattern(T, 4, 0.2, 0.1, 4) + shotgun_pattern( + T - np.pi / 2, 3.5, 0, c=0.15, d=1 + ) + else: + R = alpha + (1 - alpha) * np.cos(T) + R = np.log(1 + np.abs(50 * R)) / np.log(10) + R = 1000 * (R / R.max()) + + ax.set_theta_offset(np.pi / 2) + ax.set_thetalim(0, 2 * np.pi) + ax.set_rorigin(0) + ax.set_rlabel_position(np.pi / 2) + + ax.fill(T, R, zorder=20, color="C1", clip_on=True, alpha=0.25) + ax.plot( + T, + R, + zorder=30, + alpha=0.75, + color="C1", + linewidth=1.0, + linestyle=":", + clip_on=False, + ) + ax.plot(T, R, zorder=40, color="C1", linewidth=1.5, clip_on=True) + ax.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2]) + ax.xaxis.set_tick_params("major", pad=-2.5) + ax.set_xticklabels( + ["0°", "", "180°", ""], + family="Roboto", + size="small", + horizontalalignment="center", + verticalalignment="center", + ) + ax.set_yticks([200, 400, 600, 800, 1010]) + + for y, label in zip([390, 590, 790], ["-20 dB", "-15 dB", "-10 dB"]): + ax.text( + 0, + y, + label, + zorder=10, + family="Roboto Condensed", + size="small", + horizontalalignment="center", + verticalalignment="center", + bbox=dict(facecolor="white", edgecolor="None", pad=1.0), + ) + ax.set_yticklabels([]) + + ax.set_ylim(200, 1010) + ax.set_title(title, family="Roboto", weight="bold", size="large", y=-0.2) + + +fig = plt.figure(figsize=(8, 7)) +fig.suptitle( + "Microphone polar patterns", family="Roboto", weight="bold", size="xx-large" +) + +# Omnidirectional +ax = plt.subplot(2, 3, 1, projection="polar") +plot(ax, "Omnidirectional", 1.00) + +# Subcardioid +ax = plt.subplot(2, 3, 2, projection="polar") +plot(ax, "Subcardioid", 0.75) + +# Cardioid +ax = plt.subplot(2, 3, 3, projection="polar") +plot(ax, "Cardioid", 0.50) + +# Supercardioid +ax = plt.subplot(2, 3, 4, projection="polar") +plot(ax, "Supercardioid", 0.25) + +# Bidirectional +ax = plt.subplot(2, 3, 5, projection="polar") +plot(ax, "Bidirectional", 0.00) + +# Shotgun (not quite right yet) +ax = plt.subplot(2, 3, 6, projection="polar") +plot(ax, "Shotgun", shotgun=True) + +plt.tight_layout() +plt.savefig("../../figures/scales-projections/polar-patterns.pdf") +plt.show() diff --git a/code/scales-projections/projection-3d-frame.py b/code/scales-projections/projection-3d-frame.py new file mode 100644 index 0000000..b8e6770 --- /dev/null +++ b/code/scales-projections/projection-3d-frame.py @@ -0,0 +1,145 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +from mpl_toolkits.mplot3d import Axes3D, proj3d, art3d +import matplotlib.patheffects as path_effects + + +# Style +# ----------------------------------------------------------------------------- +# plt.rc('font', family="Roboto Condensed") +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + +fig = plt.figure(figsize=(6, 6)) +ax = plt.subplot(1, 1, 1, projection="3d") +ax.set_axis_off() +ax.set_xlim(0, 10) +ax.set_ylim(10, 0) +ax.set_zlim(0, 10) + +for d in np.arange(0, 11): + color, linewidth, zorder = "0.75", 0.5, -100 + if d in [0, 5, 10]: + color, linewidth, zorder = "0.5", 0.75, -50 + ax.plot([0, 0], [d, d], [0, 10], linewidth=linewidth, color=color, zorder=zorder) + ax.plot([0, 0], [0, 10], [d, d], linewidth=linewidth, color=color, zorder=zorder) + ax.plot([0, 10], [0, 0], [d, d], linewidth=linewidth, color=color, zorder=zorder) + ax.plot([d, d], [0, 10], [0, 0], linewidth=linewidth, color=color, zorder=zorder) + ax.plot([0, 10], [d, d], [0, 0], linewidth=linewidth, color=color, zorder=zorder) + ax.plot([d, d], [0, 0], [0, 10], linewidth=linewidth, color=color, zorder=zorder) + +size, color = "small", "0.75" +for d in np.arange(1, 11): + ax.text(d, 10.75, 0, "%d" % d, color=color, size=size, ha="center", va="center") + ax.text(10.5, d, 0, "%d" % d, color=color, size=size, ha="center", va="center") + ax.text(0, 10.75, d, "%d" % d, color=color, size=size, ha="center", va="center") + ax.text(10.5, 0, d, "%d" % d, color=color, size=size, ha="center", va="center") + ax.text(d, 0, 10.5, "%d" % d, color=color, size=size, ha="center", va="center") + + ax.text(0, d, 10.5, "%d" % d, color=color, size=size, ha="center", va="center") + + +plt.plot([0, 10], [0, 0], [0, 0], linewidth=1.5, color="red") +ax.text(10.5, 0, 0, "X", color="black", ha="center", va="center") + +plt.plot([0, 0], [0, 10], [0, 0], linewidth=1.5, color="green") +ax.text(0, 10.5, 0, "Y", color="black", ha="center", va="center") + +plt.plot([0, 0], [0, 0], [0, 10], linewidth=1.5, color="blue") +ax.text(0, 0, 10.5, "Z", color="black", ha="center", va="center") + + +text = ax.text(0, 0, 0, "O", color="black", size="large", ha="center", va="center") +text.set_path_effects( + [path_effects.Stroke(linewidth=3, foreground="white"), path_effects.Normal()] +) + + +rect = mpatches.Rectangle((0, 0), 7, 5, facecolor="k", alpha=0.03, zorder=50) +ax.add_patch(rect) +art3d.pathpatch_2d_to_3d(rect, z=6, zdir="z") + +rect = mpatches.Rectangle((0, 0), 7, 6, facecolor="k", alpha=0.06, zorder=50) +ax.add_patch(rect) +art3d.pathpatch_2d_to_3d(rect, z=5, zdir="y") + +rect = mpatches.Rectangle((0, 0), 5, 6, facecolor="k", alpha=0.09, zorder=50) +ax.add_patch(rect) +art3d.pathpatch_2d_to_3d(rect, z=7, zdir="x") + + +class Arrow3D(mpatches.FancyArrowPatch): + def __init__(self, xs, ys, zs, *args, **kwargs): + mpatches.FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs) + self._verts3d = xs, ys, zs + + def draw(self, renderer): + xs3d, ys3d, zs3d = self._verts3d + xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M) + self.set_positions((xs[0], ys[0]), (xs[1], ys[1])) + mpatches.FancyArrowPatch.draw(self, renderer) + + +ax.add_artist( + Arrow3D( + [7, 0], + [5, 5], + [6, 6], + mutation_scale=10, + lw=1.5, + arrowstyle="-|>", + color="black", + ) +) +ax.add_artist( + Arrow3D( + [7, 7], + [5, 0], + [6, 6], + mutation_scale=10, + lw=1.5, + arrowstyle="-|>", + color="black", + ) +) +ax.add_artist( + Arrow3D( + [7, 7], + [5, 5], + [6, 0], + mutation_scale=10, + lw=1.5, + arrowstyle="-|>", + color="black", + ) +) + +ax.scatter([7], [5], [6], s=50, facecolor="black", edgecolor="black", linewidth=1.5) + +ax.text( + 7.5, 5, 6, "(7.0, 5.0, 6.0)", color="black", size="small", ha="left", va="center" +) + +ax.plot([7, 0], [5, 0], [6, 6], color="blue", linewidth=1.5, linestyle="--", marker="o") +ax.text(0.5, 0.5, 6.5, "6", color="black", size="small", ha="left", va="bottom") + +ax.plot([7, 7], [5, 0], [6, 0], color="red", linewidth=1.5, linestyle="--", marker="o") +ax.text(0, 5.5, 0, "7", color="black", size="small", ha="right", va="bottom") + +ax.plot( + [7, 0], [5, 5], [6, 0], color="green", linewidth=1.5, linestyle="--", marker="o" +) +ax.text(7.5, 0, 0, "5", color="black", size="small", ha="left", va="bottom") + + +plt.tight_layout() +plt.savefig("../../figures/scales-projections/projection-3d-frame.pdf") +plt.show() diff --git a/code/scales-projections/projection-polar-config.py b/code/scales-projections/projection-polar-config.py new file mode 100644 index 0000000..46853fc --- /dev/null +++ b/code/scales-projections/projection-polar-config.py @@ -0,0 +1,44 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + + +def polar(ax, r0, rmin, rmax, rticks, tmin, tmax, tticks): + ax.set_yticks(np.linspace(rmin, rmax, rticks)) + ax.set_yticklabels([]) + ax.set_rorigin(r0) + ax.set_rmin(rmin) + ax.set_rmax(rmax) + + ax.set_xticks(np.linspace(np.pi * tmin / 180, np.pi * tmax / 180, tticks)) + ax.set_xticklabels([]) + ax.set_thetamin(tmin) + ax.set_thetamax(tmax) + + text = r"""$r_{0}=%.2f,r_{min}=%.2f,r_{max}=%.2f$""" % (r0, rmin, rmax) + text += "\n" + text += r"""$t_{min}=%.2f,t_{max}=%.2f$""" % (tmin, tmax) + + plt.text( + 0.5, -0.15, text, size="small", ha="center", va="bottom", transform=ax.transAxes + ) + + +fig = plt.figure(figsize=(8, 4)) + +ax = fig.add_subplot(1, 3, 1, aspect=1, projection="polar") +polar(ax, 0.00, 0.00, 1.00, 4, 0, 360, 16) + +ax = fig.add_subplot(1, 3, 2, aspect=1, projection="polar") +polar(ax, 0.00, 0.25, 1.00, 8, 0, 360, 32) + +ax = fig.add_subplot(1, 3, 3, aspect=1, projection="polar") +polar(ax, 0.00, 0.50, 1.00, 10, 0, 90, 20) + +plt.tight_layout() +plt.savefig("../../figures/scales-projections/projection-polar-config.pdf", dpi=600) +plt.show() diff --git a/code/scales-projections/projection-polar-histogram.py b/code/scales-projections/projection-polar-histogram.py new file mode 100644 index 0000000..baa395d --- /dev/null +++ b/code/scales-projections/projection-polar-histogram.py @@ -0,0 +1,137 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patheffects as path_effects + +# Setup +fig = plt.figure(figsize=(6, 6)) +ax = plt.subplot(1, 1, 1, projection="polar", frameon=True) +ax.set_thetalim(0, 2 * np.pi) +ax.set_rlim(0, 1000) +ax.set_xticks([]) +ax.set_xticklabels([]) +ax.set_yticks(np.linspace(100, 1000, 10)) +ax.set_yticklabels([]) +ax.tick_params("both", grid_alpha=0.50, grid_zorder=-10, grid_linewidth=0.5) + + +# Theta ticks +radius = ax.get_rmax() +length = 0.025 * radius +for i in range(360): + angle = np.pi * i / 180 + plt.plot( + [angle, angle], + [radius, radius - length], + linewidth=0.50, + color="0.75", + clip_on=False, + ) +for i in range(0, 360, 5): + angle = np.pi * i / 180 + plt.plot( + [angle, angle], + [radius, radius - 2 * length], + linewidth=0.75, + color="0.75", + clip_on=False, + ) +for i in range(0, 360, 15): + angle = np.pi * i / 180 + plt.plot([angle, angle], [radius, 100], linewidth=0.5, color="0.75") + plt.plot( + [angle, angle], + [radius + length, radius], + zorder=500, + linewidth=1.0, + color="0.00", + clip_on=False, + ) + plt.text( + angle, + radius + 4 * length, + "%d°" % i, + zorder=500, + rotation=i - 90, + rotation_mode="anchor", + va="top", + ha="center", + size="small", + family="Roboto", + color="black", + ) +for i in range(0, 360, 90): + angle = np.pi * i / 180 + plt.plot([angle, angle], [radius, 0], zorder=500, linewidth=1.00, color="0.0") + + +# Radius ticks +def polar_to_cartesian(theta, radius): + x = radius * np.cos(theta) + y = radius * np.sin(theta) + return np.array([x, y]) + + +def cartesian_to_polar(x, y): + radius = np.sqrt(x ** 2 + y ** 2) + theta = np.arctan2(y, x) + return np.array([theta, radius]) + + +for i in range(0, 1000, 10): + P0 = 0, i + P1 = cartesian_to_polar(*(polar_to_cartesian(*P0) + [0, 0.75 * length])) + plt.plot([P0[0], P1[0]], [P0[1], P1[1]], linewidth=0.50, color="0.75") + +for i in range(100, 1000, 100): + P0 = 0, i + P1 = cartesian_to_polar(*(polar_to_cartesian(*P0) + [0, +1.0 * length])) + plt.plot([P0[0], P1[0]], [P0[1], P1[1]], zorder=500, linewidth=0.75, color="0.0") + P1 = cartesian_to_polar(*(polar_to_cartesian(*P0) + [0, -1.0 * length])) + text = ax.text( + P1[0], + P1[1], + "%d" % i, + zorder=500, + va="top", + ha="center", + size="x-small", + family="Roboto", + color="black", + ) + text.set_path_effects( + [path_effects.Stroke(linewidth=2, foreground="white"), path_effects.Normal()] + ) + +# Circular bands +n = 1000 +T = np.linspace(0, 2 * np.pi, n) +color = "0.95" +ax.fill_between(T, 0, 100, color=color, zorder=-50) +ax.fill_between(T, 200, 300, color=color, zorder=-50) +ax.fill_between(T, 400, 500, color=color, zorder=-50) +ax.fill_between(T, 600, 700, color=color, zorder=-50) +ax.fill_between(T, 800, 900, color=color, zorder=-50) +plt.scatter([0], [0], 20, facecolor="white", edgecolor="black", zorder=1000) + + +# Actual plot +np.random.seed(123) +n = 145 +T = 2 * np.pi / n + np.linspace(0, 2 * np.pi, n) +T[1::2] = T[0:-1:2] +R = np.random.uniform(400, 800, n) +R[-1] = R[0] +R[1:-1:2] = R[2::2] +ax.fill(T, R, color="C1", zorder=150, alpha=0.1) +ax.plot(T, R, color="white", zorder=200, linewidth=3.5) +ax.plot(T, R, color="C1", zorder=250, linewidth=1.5) + + +plt.tight_layout() +plt.savefig("../../figures/scales-projections/projection-polar-histogram.pdf") +plt.show() diff --git a/code/scales-projections/scales-check.py b/code/scales-projections/scales-check.py new file mode 100644 index 0000000..ae67b72 --- /dev/null +++ b/code/scales-projections/scales-check.py @@ -0,0 +1,54 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import NullFormatter, SymmetricalLogLocator + + +fig = plt.figure(figsize=(6, 6)) +ax = plt.subplot(1, 1, 1, aspect=1, xlim=[0, 100], ylim=[0, 100]) +P0, P1, P2, P3 = (0.1, 0.1), (1, 1), (10, 10), (100, 100) +transform = ax.transData.transform +print("Linear scale") +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P0[0], P1[0], abs((transform(P1) - transform(P0))[0]) + ) +) +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P1[0], P2[0], abs((transform(P2) - transform(P1))[0]) + ) +) +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P2[0], P3[0], abs((transform(P3) - transform(P2))[0]) + ) +) +print() + +fig = plt.figure(figsize=(6, 6)) +ax = plt.subplot(1, 1, 1, aspect=1, xlim=[0.1, 100], ylim=[0.1, 100]) +ax.set_xscale("log") +transform = ax.transData.transform + +P0, P1, P2, P3 = (0.1, 0.1), (1, 1), (10, 10), (100, 100) +print("Log scale") +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P0[0], P1[0], abs((transform(P1) - transform(P0))[0]) + ) +) +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P1[0], P2[0], abs((transform(P2) - transform(P1))[0]) + ) +) +print( + " -> distance({0:5.1f},{1:5.1f}) = {2:.2f}".format( + P2[0], P3[0], abs((transform(P3) - transform(P2))[0]) + ) +) diff --git a/code/scales-projections/scales-comparison.py b/code/scales-projections/scales-comparison.py new file mode 100644 index 0000000..cb1e66e --- /dev/null +++ b/code/scales-projections/scales-comparison.py @@ -0,0 +1,92 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + + +# Style +# ----------------------------------------------------------------------------- +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + + +fig = plt.figure(figsize=(6, 2)) + +# Linear axis +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 1, xlim=[0, 1], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "linear scale (default)", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) +ax.tick_params(axis="both", which="major") + + +# Log axis +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 2, xlim=[0.1, 1.0], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "log scale ('log')", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_xscale("log") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) + +# Logit axis +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 3, xlim=[0.1, 0.9], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "logit scale ('logit')", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_xscale("logit") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) + +# Show +# ----------------------------------------------------------------------------- +plt.tight_layout() +plt.savefig("../../figures/scales-projections/scales-comparison.pdf") +plt.show() diff --git a/code/scales-projections/scales-custom.py b/code/scales-projections/scales-custom.py new file mode 100644 index 0000000..eba2ded --- /dev/null +++ b/code/scales-projections/scales-custom.py @@ -0,0 +1,100 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + + +# Style +# ----------------------------------------------------------------------------- +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + + +fig = plt.figure(figsize=(6, 2)) + +# Linear axis +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 1, xlim=[0.0, 1.0], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "linear scale", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) +ax.tick_params(axis="both", which="major") + + +def forward(x): + return x ** 2 + + +def inverse(x): + return x ** (1 / 2) + + +# x**2 scale +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 2, xlim=[0.1, 1.0], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "$x^2$ scale", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_xscale("function", functions=(forward, inverse)) +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) + +# sqrt(x) +# ----------------------------------------------------------------------------- +ax = plt.subplot(3, 1, 3, xlim=[0.1, 1.0], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.text( + 0.5, + 0.1, + "$\sqrt{x}$ scale", + transform=ax.transAxes, + horizontalalignment="center", + verticalalignment="bottom", +) +ax.set_xscale("function", functions=(inverse, forward)) +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.10)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.02)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) + +# Show +# ----------------------------------------------------------------------------- +plt.tight_layout() +plt.savefig("../../figures/scales-projections/scales-custom.pdf") +plt.show() diff --git a/code/scales-projections/scales-log-log.py b/code/scales-projections/scales-log-log.py new file mode 100644 index 0000000..1097bf7 --- /dev/null +++ b/code/scales-projections/scales-log-log.py @@ -0,0 +1,88 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.ticker import NullFormatter, MultipleLocator + +X = np.linspace(0.001, 90, 5000) +figure = plt.figure(figsize=(6, 6)) + +# Style +# ----------------------------------------------------------------------------- +plt.rc("font", family="Roboto") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + + +# X-linear Y-linear +# ----------------------------------------------------------------------------- +ax1 = plt.subplot(2, 2, 1, xlim=(0.0, 10), ylim=(0.0, 10)) +ax1.plot(X, 10 ** X, color="C0") +ax1.plot(X, X, color="C1") +ax1.plot(X, np.log10(X), color="C2") +ax1.set_ylabel("Linear") +ax1.xaxis.set_major_locator(MultipleLocator(2.0)) +ax1.xaxis.set_minor_locator(MultipleLocator(0.4)) +ax1.yaxis.set_major_locator(MultipleLocator(2.0)) +ax1.yaxis.set_minor_locator(MultipleLocator(0.4)) +ax1.grid(True, "minor", color="0.85", linewidth=0.50, zorder=-20) +ax1.grid(True, "major", color="0.65", linewidth=0.75, zorder=-10) +ax1.tick_params(which="both", labelbottom=False, bottom=False) + +ax1.text(1.25, 8.50, "$f(x) = 10^x$", color="C0") +ax1.text(5.75, 5.00, "$f(x) = x$", color="C1") +ax1.text(5.50, 1.50, "$f(x) = log_{10}(x)$", color="C2") +ax1.set_title("X linear - Y linear") + + +# X-log Y-linear +# ----------------------------------------------------------------------------- +ax2 = plt.subplot(2, 2, 2, xlim=(0.001, 100), ylim=(0.0, 10), sharey=ax1) +ax2.set_xscale("log") +ax2.tick_params(which="both", labelbottom=False, bottom=False) +ax2.tick_params(which="both", labelleft=False, left=False) +ax2.plot(X, 10 ** X, color="C0") +ax2.plot(X, X, color="C1") +ax2.plot(X, np.log10(X), color="C2") +ax2.grid(True, "minor", color="0.85", linewidth=0.50, zorder=-20) +ax2.grid(True, "major", color="0.65", linewidth=0.75, zorder=-10) +ax2.set_title("X logarithmic - Y linear") + + +# X-linear Y-log +# ----------------------------------------------------------------------------- +ax3 = plt.subplot(2, 2, 3, xlim=(0.0, 10), ylim=(0.001, 100), sharex=ax1) +ax3.set_yscale("log") +ax3.plot(X, 10 ** X, color="C0") +ax3.plot(X, X, color="C1") +ax3.plot(X, np.log10(X), color="C2") +ax3.set_ylabel("Logarithmic") +ax3.set_xlabel("Linear") +ax3.grid(True, "minor", color="0.85", linewidth=0.50, zorder=-20) +ax3.grid(True, "major", color="0.65", linewidth=0.75, zorder=-10) +ax3.set_title("X linear - Y logarithmic") + +# X-log Y-log +# ----------------------------------------------------------------------------- +ax4 = plt.subplot(2, 2, 4, xlim=(0.001, 100), ylim=(0.001, 100), sharex=ax2, sharey=ax3) +ax4.set_xscale("log") +ax4.set_yscale("log") +ax4.tick_params(which="both", labelleft=False, left=False) +ax4.plot(X, 10 ** X, color="C0") +ax4.plot(X, X, color="C1") +ax4.plot(X, np.log10(X), color="C2") +ax4.set_xlabel("Logarithmic") +ax4.grid(True, "minor", color="0.85", linewidth=0.50, zorder=-20) +ax4.grid(True, "major", color="0.65", linewidth=0.75, zorder=-10) +ax4.set_title("X logarithmic - Y logarithmic") + + +# Show +# ----------------------------------------------------------------------------- +plt.tight_layout() +plt.savefig("../../figures/scales-projections/scales-log-log.pdf") +plt.show() diff --git a/code/scales-projections/text-polar.py b/code/scales-projections/text-polar.py new file mode 100644 index 0000000..13610a3 --- /dev/null +++ b/code/scales-projections/text-polar.py @@ -0,0 +1,111 @@ +# ----------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ----------------------------------------------------------------------------- +# Illustrate the use of polar projection and rotated text (with transformation) +# ----------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch +from matplotlib.patches import Ellipse + +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1) + + +size = 0.1 +vals = np.ones(12) +np.random.seed(123) + +# A nice set of colors for seasons +cmap20c = plt.get_cmap("tab20c") +cmap20b = plt.get_cmap("tab20b") +colors = [ + cmap20c(0), + cmap20c(1), + cmap20c(2), # Winter + cmap20c(10), + cmap20c(9), + cmap20c(8), # Spring + cmap20c(4), + cmap20c(5), + cmap20c(6), # Summer + cmap20b(15), + cmap20b(14), + cmap20b(13), +] # Autumn + +# Simple pie +ax.pie(np.ones(12), radius=1, colors=colors, wedgeprops=dict(width=size, edgecolor="w")) + + +# Rotated and transformed label +def label(text, angle, radius=1, scale=0.005): + path = TextPath((0, 0), text, size=10) + V = path.vertices + xmin, xmax = V[:, 0].min(), V[:, 0].max() + ymin, ymax = V[:, 1].min(), V[:, 1].max() + V -= (xmin + xmax) / 2, (ymin + ymax) / 2 + V *= scale + for i in range(len(V)): + a = angle - V[i, 0] + V[i, 0] = (radius + V[i, 1]) * np.cos(a) + V[i, 1] = (radius + V[i, 1]) * np.sin(a) + patch = PathPatch(path, facecolor="k", linewidth=0) + ax.add_artist(patch) + + +# This could be made through a list but it is easier to red this way +label("JANUARY", 0.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("FEBRUARY", 1.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("MARCH", 2.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("WINTER", 1.5 * 2 * np.pi / 12, 1 + size, 0.0125) + +label("APRIL", 3.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("MAY", 4.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("JUNE", 5.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("SPRING", 4.5 * 2 * np.pi / 12, 1 + size, 0.0125) + +label("JULY", 6.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("AUGUST", 7.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("SEPTEMBER", 8.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("SUMMER", 7.5 * 2 * np.pi / 12, 1 + size, 0.0125) + +label("OCTOBER", 9.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("NOVEMBER", 10.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("DECEMBER", 11.5 * 2 * np.pi / 12, 1 - 0.5 * size) +label("AUTUMN", 10.5 * 2 * np.pi / 12, 1 + size, 0.0125) + + +# Add a polar projection on top of the previous one +ax = fig.add_axes([0.15, 0.15, 0.7, 0.7], projection="polar") + +# Some ellipses that will enforce polar projection +for i in range(250): + p = np.random.uniform(0, 2 * np.pi), np.random.uniform(0.05, 0.95) + w = h = 0.01 + 0.05 * np.random.uniform(1, 2) + color = colors[int(np.floor((p[0] / (2 * np.pi)) * 12))] + ellipse = Ellipse( + p, + width=2 * w, + height=h, + zorder=10, + facecolor=color, + edgecolor="none", + alpha=0.5, + ) + ax.add_artist(ellipse) +ax.set_xlim(0, 2 * np.pi) +ax.set_xticks(np.linspace(0, 2 * np.pi, 12, endpoint=False)) +ax.set_xticklabels([]) +ax.set_ylim(0, 1) +ax.set_yticks(np.linspace(0, 1, 6)) +ax.set_yticklabels([]) +ax.set_rorigin(-0.25) + + +# Save results and show it +plt.savefig("../../figures/scales-projections/text-polar.pdf") +plt.show() diff --git a/code/showcases/contour-dropshadow.py b/code/showcases/contour-dropshadow.py new file mode 100644 index 0000000..00f22f0 --- /dev/null +++ b/code/showcases/contour-dropshadow.py @@ -0,0 +1,96 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Contour with drop shadows +# ---------------------------------------------------------------------------- +import scipy +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from matplotlib.backends.backend_agg import FigureCanvas +from matplotlib.patheffects import Stroke, Normal +from scipy.ndimage import gaussian_filter + +# Some data +n = 64 +X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), +) +Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + +def f(x, y): + return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2) - y ** 2) + + +x = np.linspace(-3, 3, 100) +y = np.linspace(-3, 3, 100) +X, Y = np.meshgrid(x, y) +Z = 0.5 * f(X, Y) + +n = 11 # Number of levels +dz = (Z.max() - Z.min()) / n +levels = np.linspace(Z.min(), Z.max(), n, endpoint=True) +cmap = plt.get_cmap("viridis") + + +def drop_shadow(Z, l0, l1, sigma=5, alpha=0.5): + """Compute the drop shadow for a contour between l0 and l1. + + This works by first: + 1. render the contour in black using an offline image + 2. blue the contour using a Guassian filter + 3. set the alpha channel accordingly + """ + fig = Figure(figsize=(5, 5)) + canvas = FigureCanvas(fig) + ax = fig.add_axes([0, 0, 1, 1], frameon=False) + ax.contourf( + Z, + vmin=Z.min(), + vmax=Z.max(), + levels=[l0, l1], + origin="lower", + colors="black", + extent=[-1, 1, -1, 1], + ) + ax.set_xlim(-1, 1), ax.set_ylim(-1, 1) + canvas.draw() + A = np.array(canvas.renderer.buffer_rgba())[:, :, 0] + del fig + A = gaussian_filter(A, sigma) + A = (A - A.min()) / (A.max() - A.min()) + I = np.zeros((A.shape[0], A.shape[1], 4)) + I[:, :, 3] = (1 - A) * alpha + return I + + +fig = plt.figure(figsize=(8, 8), dpi=100) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) + +zorder = -100 +for i in range(len(levels) - 1): + l0, l1 = levels[i], levels[i + 1] + I = drop_shadow(Z, l0, l1) + ax.imshow(I, extent=[-3, 3, -3, 3], origin="upper", zorder=zorder) + zorder += 1 + ax.contourf( + Z, + vmin=Z.min(), + vmax=Z.max(), + levels=[l0, l1], + origin="lower", + cmap=cmap, + extent=[-3, 3, -3, 3], + zorder=zorder, + ) + zorder += 1 +ax.set_xlim(-2.9, 2.9) +ax.set_ylim(-2.9, 2.9) + +plt.savefig("../../figures/showcases/contour-dropshadow.png", dpi=600) +# plt.savefig("../figures/contour-dropshadow.pdf", dpi=300) +plt.show() diff --git a/code/showcases/domain-coloring.py b/code/showcases/domain-coloring.py new file mode 100644 index 0000000..782f87a --- /dev/null +++ b/code/showcases/domain-coloring.py @@ -0,0 +1,43 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patheffects import Stroke, Normal + + +def normalize(Z): + zmin, zmax = Z.min(), Z.max() + return (Z - zmin) / (zmax - zmin) + + +T = np.linspace(-2.5, 2.5, 2048) +X, Y = np.meshgrid(T, T) +Z = X + 1j * Y +Z = Z + 1 / Z +A = normalize(np.angle(Z)) +N = normalize(np.abs(Z)) * 2 * np.pi * 200 + + +fig = plt.figure(figsize=(8, 8)) +e = 0.001 +ax = fig.add_axes([e, e, 1 - 2 * e, 1 - 2 * e], frameon=True, facecolor="black") + +ax.imshow( + A, + interpolation="bicubic", + cmap="Spectral", + rasterized=True, + alpha=1 - (N < 1.5 * np.pi) * 0.25 * abs(np.cos(N % (np.pi / 2))) ** 2, +) +ax.contour(np.abs(Z.real - np.round(Z.real)), 1, colors="black", linewidths=0.25) +ax.contour(np.abs(Z.imag - np.round(Z.imag)), 1, colors="black", linewidths=0.25) + + +ax.set_xticks([]) +ax.set_yticks([]) +plt.savefig("../../figures/showcases/domain-coloring.png", dpi=600) +plt.savefig("../../figures/showcases/domain-coloring.pdf") +plt.show() diff --git a/code/showcases/escher-movie.py b/code/showcases/escher-movie.py new file mode 100644 index 0000000..c33f3a6 --- /dev/null +++ b/code/showcases/escher-movie.py @@ -0,0 +1,72 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon + + +def line(A, B, thickness=0.005, n=100): + T = (B - A) / np.linalg.norm(B - A) + O = np.array([-T[1], T[0]]) + P0 = A + thickness * O + P1 = B + thickness * O + P2 = B - thickness * O + P3 = A - thickness * O + P = np.concatenate( + [ + np.linspace(P0, P1, n), + np.linspace(P1, P2, n // 10), + np.linspace(P2, P3, n), + np.linspace(P3, P0, n // 10), + ] + ) + X, Y = P[:, 0], P[:, 1] + return np.dstack([np.exp(X) * np.cos(Y), np.exp(X) * np.sin(Y)]).squeeze() + + +n = 250 +X = np.exp(-5 * np.linspace(-1, 1, n, endpoint=True) ** 2) +A = X * 7 + (1 - X) * 13 +B = X * 3 + (1 - X) * 11 + +for k, (a, b) in enumerate(zip(A, B)): + y = 2 * np.pi / (a + b * b / a) + x = b * y / a + v1, v2 = np.array([x, y]), np.array([-y, x]) + + fig = plt.figure(figsize=(8, 8)) + ax = plt.subplot(1, 1, 1, aspect=1) + + for i, z in enumerate(np.linspace(0, 1, 3 * 10, endpoint=False)): + zorder = 0 + color = "0.5" + thickness = 0.001 + if i % 10 == 0: + thickness = 0.0025 + color = "0.0" + zorder = 10 + P0, P1 = (2 + z) * b * v2, (2 + z) * b * v2 + 4 * a * v1 + L = line(P0, P1, thickness=thickness, n=500) + ax.add_patch(Polygon(L, facecolor=color, zorder=zorder)) + + for i, z in enumerate(np.linspace(0, 1, 7 * 10, endpoint=False)): + zorder = 0 + color = "0.5" + thickness = 0.001 + if i % 10 == 0: + thickness = 0.0025 + color = "0.0" + zorder = 10 + + P0, P1 = (1 + z) * a * v1, (1 + z) * a * v1 + 4 * b * v2 + L = line(P0, P1, thickness=thickness, n=500) + ax.add_patch(Polygon(L, facecolor=color, zorder=zorder)) + + ax.set_xlim(-np.pi, np.pi), ax.set_xticks([]) + ax.set_ylim(-np.pi, np.pi), ax.set_yticks([]) + plt.tight_layout() + # plt.show() + plt.savefig("escher-frame-%03d.png" % k, dpi=100) + print("Frame escher-frame-%03d.png" % k) + del fig + +# Encode with (slow): +# ffmpeg -i escher-frame-%03d.png -framerate 30 -vcodec libvpx-vp9 -b:v 1M output.webm diff --git a/code/showcases/escher.py b/code/showcases/escher.py new file mode 100644 index 0000000..f4966e6 --- /dev/null +++ b/code/showcases/escher.py @@ -0,0 +1,68 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon + + +def line(A, B, thickness=0.005, n=100): + T = (B - A) / np.linalg.norm(B - A) + O = np.array([-T[1], T[0]]) + P0 = A + thickness * O + P1 = B + thickness * O + P2 = B - thickness * O + P3 = A - thickness * O + P = np.concatenate( + [ + np.linspace(P0, P1, n), + np.linspace(P1, P2, n // 10), + np.linspace(P2, P3, n), + np.linspace(P3, P0, n // 10), + ] + ) + X, Y = P[:, 0], P[:, 1] + return np.dstack([np.exp(X) * np.cos(Y), np.exp(X) * np.sin(Y)]).squeeze() + + +a, b = 11, 9 +y = 2 * np.pi / (a + b * b / a) +x = b * y / a +v1, v2 = np.array([x, y]), np.array([-y, x]) + + +fig = plt.figure(figsize=(8, 8)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1, frameon=False) + +for i, z in enumerate(np.linspace(0, 1, b * 10, endpoint=False)): + zorder = 0 + color = "0.5" + thickness = 0.001 + if i % 10 == 0: + thickness = 0.0025 + color = "0.0" + zorder = 10 + P0, P1 = (2 + z) * b * v2, (2 + z) * b * v2 + 4 * a * v1 + L = line(P0, P1, thickness=thickness, n=500) + ax.add_patch(Polygon(L, facecolor=color, zorder=zorder)) + +for i, z in enumerate(np.linspace(0, 1, a * 10, endpoint=False)): + zorder = 0 + color = "0.5" + thickness = 0.001 + if i % 10 == 0: + thickness = 0.0025 + color = "0.0" + zorder = 10 + + P0, P1 = (1 + z) * a * v1, (1 + z) * a * v1 + 4 * b * v2 + L = line(P0, P1, thickness=thickness, n=500) + ax.add_patch(Polygon(L, facecolor=color, zorder=zorder)) + + +ax.set_xlim(-np.pi, np.pi), ax.set_xticks([]) +ax.set_ylim(-np.pi, np.pi), ax.set_yticks([]) +plt.savefig("../../figures/showcases/escher.pdf") +plt.show() diff --git a/code/showcases/mandelbrot.py b/code/showcases/mandelbrot.py new file mode 100644 index 0000000..d24d4a9 --- /dev/null +++ b/code/showcases/mandelbrot.py @@ -0,0 +1,52 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +from matplotlib import colors +import matplotlib.pyplot as plt + + +def mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon=2.0): + X = np.linspace(xmin, xmax, xn, dtype=np.float32) + Y = np.linspace(ymin, ymax, yn, dtype=np.float32) + C = X + Y[:, None] * 1j + N = np.zeros(C.shape, dtype=int) + Z = np.zeros(C.shape, np.complex64) + for n in range(maxiter): + I = np.less(abs(Z), horizon) + N[I] = n + Z[I] = Z[I] ** 2 + C[I] + N[N == maxiter - 1] = 0 + return Z, N + + +xmin, xmax, xn = -2.75, +1.25, 4000 +ymin, ymax, yn = -1.5, +1.5, 3000 +maxiter = 200 +horizon = 2.0 ** 40 +log_horizon = np.log(np.log(horizon)) / np.log(2) +Z, N = mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon) + +# Normalized recount as explained in: +# https://linas.org/art-gallery/escape/smooth.html +# https://www.ibm.com/developerworks/community/blogs/jfp/entry/My_Christmas_Gift +with np.errstate(invalid="ignore"): + M = np.nan_to_num(N + 1 - np.log(np.log(abs(Z))) / np.log(2) + log_horizon) + +width = 10 +height = 10 * yn / xn +fig = plt.figure(figsize=(width, height), dpi=100) +ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1) + +# Shaded rendering +light = colors.LightSource(azdeg=315, altdeg=10) +M = light.shade( + M, cmap=plt.cm.hot, vert_exag=1.5, norm=colors.PowerNorm(0.3), blend_mode="hsv" +) +plt.imshow(M, extent=[xmin, xmax, ymin, ymax], interpolation="bicubic") +ax.set_xticks([]) +ax.set_yticks([]) +plt.savefig("mandelbrot.png", dpi=600) +plt.show() diff --git a/code/showcases/mosaic.py b/code/showcases/mosaic.py new file mode 100644 index 0000000..d1149cc --- /dev/null +++ b/code/showcases/mosaic.py @@ -0,0 +1,127 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from scipy.spatial import Voronoi +from math import sqrt, ceil, floor, pi, cos, sin +from matplotlib.collections import PolyCollection + + +def blue_noise(shape, radius, k=30, seed=None): + """ + Generate blue noise over a two-dimensional rectangle of size (width,height) + + Parameters + ---------- + + shape : tuple + Two-dimensional domain (width x height) + radius : float + Minimum distance between samples + k : int, optional + Limit of samples to choose before rejection (typically k = 30) + seed : int, optional + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + + References + ---------- + + .. [1] Fast Poisson Disk Sampling in Arbitrary Dimensions, Robert Bridson, + Siggraph, 2007. :DOI:`10.1145/1278780.1278807` + """ + + def sqdist(a, b): + """ Squared Euclidean distance """ + dx, dy = a[0] - b[0], a[1] - b[1] + return dx * dx + dy * dy + + def grid_coords(p): + """ Return index of cell grid corresponding to p """ + return int(floor(p[0] / cellsize)), int(floor(p[1] / cellsize)) + + def fits(p, radius): + """ Check whether p can be added to the queue """ + + radius2 = radius * radius + gx, gy = grid_coords(p) + for x in range(max(gx - 2, 0), min(gx + 3, grid_width)): + for y in range(max(gy - 2, 0), min(gy + 3, grid_height)): + g = grid[x + y * grid_width] + if g is None: + continue + if sqdist(p, g) <= radius2: + return False + return True + + # When given a seed, we use a private random generator in order to not + # disturb the default global random generator + if seed is not None: + from numpy.random.mtrand import RandomState + + rng = RandomState(seed=seed) + else: + rng = np.random + + width, height = shape + cellsize = radius / sqrt(2) + grid_width = int(ceil(width / cellsize)) + grid_height = int(ceil(height / cellsize)) + grid = [None] * (grid_width * grid_height) + + p = rng.uniform(0, shape, 2) + queue = [p] + grid_x, grid_y = grid_coords(p) + grid[grid_x + grid_y * grid_width] = p + + while queue: + qi = rng.randint(len(queue)) + qx, qy = queue[qi] + queue[qi] = queue[-1] + queue.pop() + for _ in range(k): + theta = rng.uniform(0, 2 * pi) + r = radius * np.sqrt(rng.uniform(1, 4)) + p = qx + r * cos(theta), qy + r * sin(theta) + if not (0 <= p[0] < width and 0 <= p[1] < height) or not fits(p, radius): + continue + queue.append(p) + gx, gy = grid_coords(p) + grid[gx + gy * grid_width] = p + + return np.array([p for p in grid if p is not None]) + + +I = plt.imread("../data/poppy.png") +P = blue_noise((1, 1), radius=0.005) +P = np.append(P, [[+999, +999], [-999, +999], [+999, -999], [-999, -999]], axis=0) +voronoi = Voronoi(P) + + +polys, colors = [], [] +for i, region in enumerate(voronoi.regions): + if -1 in region: + continue + poly = [list(voronoi.vertices[i]) for i in region] + if len(poly) <= 0: + continue + polys.append(poly) + index = np.where(voronoi.point_region == i)[0][0] + row, col = int((1 - P[index, 1]) * I.shape[1]), int(P[index, 0] * I.shape[0]) + colors.append(I[row, col]) +colors = np.array(colors) +collection = PolyCollection( + polys, linewidth=0.5, facecolor=colors, edgecolors=colors * 0.5 +) + +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[0, 1], ylim=[0, 1], aspect=1) +ax.axis("off") + +ax.add_collection(collection) + +plt.savefig("../../figures/showcases/mosaic.pdf") +plt.show() diff --git a/code/showcases/recursive-voronoi.py b/code/showcases/recursive-voronoi.py new file mode 100644 index 0000000..786c064 --- /dev/null +++ b/code/showcases/recursive-voronoi.py @@ -0,0 +1,284 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import scipy.spatial +import matplotlib.pyplot as plt +import matplotlib.path as mpath +from shapely.geometry import Polygon +from matplotlib.collections import PolyCollection +from math import sqrt, ceil, floor, pi, cos, sin + + +def blue_noise(shape, radius, k=30, seed=None): + """ + Generate blue noise over a two-dimensional rectangle of size (width,height) + + Parameters + ---------- + + shape : tuple + Two-dimensional domain (width x height) + radius : float + Minimum distance between samples + k : int, optional + Limit of samples to choose before rejection (typically k = 30) + seed : int, optional + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + + References + ---------- + + .. [1] Fast Poisson Disk Sampling in Arbitrary Dimensions, Robert Bridson, + Siggraph, 2007. :DOI:`10.1145/1278780.1278807` + """ + + def sqdist(a, b): + """ Squared Euclidean distance """ + dx, dy = a[0] - b[0], a[1] - b[1] + return dx * dx + dy * dy + + def grid_coords(p): + """ Return index of cell grid corresponding to p """ + return int(floor(p[0] / cellsize)), int(floor(p[1] / cellsize)) + + def fits(p, radius): + """ Check whether p can be added to the queue """ + + radius2 = radius * radius + gx, gy = grid_coords(p) + for x in range(max(gx - 2, 0), min(gx + 3, grid_width)): + for y in range(max(gy - 2, 0), min(gy + 3, grid_height)): + g = grid[x + y * grid_width] + if g is None: + continue + if sqdist(p, g) <= radius2: + return False + return True + + # When given a seed, we use a private random generator in order to not + # disturb the default global random generator + if seed is not None: + from numpy.random.mtrand import RandomState + + rng = RandomState(seed=seed) + else: + rng = np.random + + width, height = shape + cellsize = radius / sqrt(2) + grid_width = int(ceil(width / cellsize)) + grid_height = int(ceil(height / cellsize)) + grid = [None] * (grid_width * grid_height) + + p = rng.uniform(0, shape, 2) + queue = [p] + grid_x, grid_y = grid_coords(p) + grid[grid_x + grid_y * grid_width] = p + + while queue: + qi = rng.randint(len(queue)) + qx, qy = queue[qi] + queue[qi] = queue[-1] + queue.pop() + for _ in range(k): + theta = rng.uniform(0, 2 * pi) + r = radius * np.sqrt(rng.uniform(1, 4)) + p = qx + r * cos(theta), qy + r * sin(theta) + if not (0 <= p[0] < width and 0 <= p[1] < height) or not fits(p, radius): + continue + queue.append(p) + gx, gy = grid_coords(p) + grid[gx + gy * grid_width] = p + + return np.array([p for p in grid if p is not None]) + + +def bounded_voronoi(points): + """ + Reconstruct infinite voronoi regions in a 2D diagram to finite regions. + + Parameters + ---------- + vor : Voronoi + Input diagram + + Returns + ------- + regions : list of tuples + Indices of vertices in each revised Voronoi regions. + vertices : list of tuples + Coordinates for revised Voronoi vertices. Same as coordinates + of input vertices, with 'points at infinity' appended to the + end. + """ + + vor = scipy.spatial.Voronoi(points) + new_regions = [] + new_vertices = vor.vertices.tolist() + center = vor.points.mean(axis=0) + radius = vor.points.ptp().max() * 2 + + # Construct a map containing all ridges for a given point + all_ridges = {} + for (p1, p2), (v1, v2) in zip(vor.ridge_points, vor.ridge_vertices): + all_ridges.setdefault(p1, []).append((p2, v1, v2)) + all_ridges.setdefault(p2, []).append((p1, v1, v2)) + + # Reconstruct infinite regions + for p1, region in enumerate(vor.point_region): + vertices = vor.regions[region] + + if all(v >= 0 for v in vertices): + # finite region + new_regions.append(vertices) + continue + + # reconstruct a non-finite region + ridges = all_ridges[p1] + new_region = [v for v in vertices if v >= 0] + + for p2, v1, v2 in ridges: + if v2 < 0: + v1, v2 = v2, v1 + if v1 >= 0: + # finite ridge: already in the region + continue + + # Compute the missing endpoint of an infinite ridge + t = vor.points[p2] - vor.points[p1] # tangent + t /= np.linalg.norm(t) + n = np.array([-t[1], t[0]]) # normal + + midpoint = vor.points[[p1, p2]].mean(axis=0) + direction = np.sign(np.dot(midpoint - center, n)) * n + far_point = vor.vertices[v2] + direction * radius + + new_region.append(len(new_vertices)) + new_vertices.append(far_point.tolist()) + + # sort region counterclockwise + vs = np.asarray([new_vertices[v] for v in new_region]) + c = vs.mean(axis=0) + angles = np.arctan2(vs[:, 1] - c[1], vs[:, 0] - c[0]) + new_region = np.array(new_region)[np.argsort(angles)] + + # finish + new_regions.append(new_region.tolist()) + return new_regions, np.asarray(new_vertices) + + +def poly_random_points_safe(V, n=10): + def random_point_inside_triangle(A, B, C): + r1 = np.sqrt(np.random.uniform(0, 1)) + r2 = np.random.uniform(0, 1) + return (1 - r1) * A + r1 * (1 - r2) * B + r1 * r2 * C + + def triangle_area(A, B, C): + return 0.5 * np.abs( + (B[0] - A[0]) * (C[1] - A[1]) - (C[0] - A[0]) * (B[1] - A[1]) + ) + + C = V.mean(axis=0) + T = [(C, V[i], V[i + 1]) for i in range(len(V) - 1)] + A = np.array([triangle_area(*t) for t in T]) + A /= A.sum() + + points = [C] + for i in np.random.choice(len(A), size=n - 1, p=A): + points.append(random_point_inside_triangle(*T[i])) + return points + + +def poly_random_points(V, n=10): + path = mpath.Path(V) + xmin, xmax = V[:, 0].min(), V[:, 0].max() + ymin, ymax = V[:, 1].min(), V[:, 1].max() + xscale, yscale = xmax - xmin, ymax - ymin + if xscale > yscale: + xscale, yscale = 1, yscale / xscale + else: + xscale, yscale = xscale / yscale, 1 + radius = 0.85 * np.sqrt(2 * xscale * yscale / (n * np.pi)) + points = blue_noise((xscale, yscale), radius) + points = [xmin, ymin] + points * [xmax - xmin, ymax - ymin] + inside = path.contains_points(points) + + P = points[inside] + if len(P) < 5: + return poly_random_points_safe(V, n) + np.random.shuffle(P) + return P[:n] + + +def voronoi(V, npoints, level, maxlevel, color=None): + if level == maxlevel: + return [] + + linewidths = [1.50, 1.00, 0.75, 0.50, 0.25, 0.10] + edgecolors = [ + (0, 0, 0, 1.00), + (0, 0, 0, 0.50), + (0, 0, 0, 0.25), + (0, 0, 0, 0.10), + (0, 0, 0, 0.10), + (0, 0, 0, 0.10), + ] + + if level == 1: + color = np.random.uniform(0, 1, 4) + color[3] = 0.5 + + points = poly_random_points(V, npoints - level) + regions, vertices = bounded_voronoi(points) + clip = Polygon(V) + cells = [] + for region in regions: + polygon = Polygon(vertices[region]).intersection(clip) + polygon = np.array([point for point in polygon.exterior.coords]) + linewidth = linewidths[level] + edgecolor = edgecolors[level] + facecolor = "none" + if level > 1: + alpha = color[3] + (1 / (level + 1)) * 0.25 * np.random.uniform(-1, 0.5) + color = color[0], color[1], color[2], min(max(alpha, 0.1), 1) + if level == maxlevel - 1: + facecolor = color + zorder = -level + cells.append((polygon, linewidth, edgecolor, facecolor, zorder)) + cells.extend(voronoi(polygon, npoints, level + 1, maxlevel, color)) + return cells + + +np.random.seed(12345) +T = np.linspace(0, 2 * np.pi, 100, endpoint=False) +R = 100 +X, Y = R * np.cos(T), R * np.sin(T) +V = np.c_[X, Y] + +V = 100 * np.array([[-1, -1], [-1, 1], [1, 1], [1, -1]]) + +fig = plt.figure(figsize=(8, 8)) +d = R - 1 +ax = fig.add_axes([0, 0, 1, 1], aspect=1, xlim=[-d, d], ylim=[-d, d]) +ax.axis("off") + +cells = voronoi(V, 11, level=0, maxlevel=5) +zorder = [cell[-1] for cell in cells] +cells = [cells[i] for i in np.argsort(zorder)] +polygons = [cell[0] for cell in cells] +linewidths = [cell[1] for cell in cells] +edgecolors = [cell[2] for cell in cells] +facecolors = [cell[3] for cell in cells] + +collection = PolyCollection( + polygons, linewidth=linewidths, edgecolor=edgecolors, facecolor=facecolors +) +ax.add_collection(collection) + +plt.savefig("../../figures/showcases/recursive-voronoi.pdf") +plt.savefig("../../figures/showcases/recursive-voronoi.png", dpi=600) +plt.show() diff --git a/code/showcases/text-shadow.py b/code/showcases/text-shadow.py new file mode 100644 index 0000000..cf316fe --- /dev/null +++ b/code/showcases/text-shadow.py @@ -0,0 +1,87 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch +from matplotlib.collections import PolyCollection +from matplotlib.font_manager import FontProperties + +red = np.array([233, 77, 85, 255]) / 255 +darkred = np.array([130, 60, 71, 255]) / 255 +prop = FontProperties(family="Source Sans Pro", weight=100) + +fig = plt.figure(figsize=(14.8 / 2.54, 21 / 2.54)) + +ax = fig.add_axes([0, 0, 1, 1], aspect=1, xlim=[-10, 10], ylim=[-14.2, 14.2]) +ax.axis("off") + +# Text path +path = TextPath((0, 0), "MATPLOTLIB", size=2.5, prop=prop) + +# Text centering +V = path.vertices +xmin, xmax = V[:, 0].min(), V[:, 0].max() +ymin, ymax = V[:, 1].min(), V[:, 1].max() +V -= (xmin + xmax) / 2 + 1, (ymin + ymax) / 2 + + +# Compute shadow by iterating over text path segments +polys = [] +for (point, code) in path.iter_segments(curves=False): + if code == path.MOVETO: + points = [point] + elif code == path.LINETO: + points.append(point) + elif code == path.CLOSEPOLY: + points.append(points[0]) + points = np.array(points) + for i in range(len(points) - 1): + p0, p1 = points[i], points[i + 1] + polys.append([p0, p1, p1 + (+20, -20), p0 + (+20, -20)]) + +# Display shadow +collection = PolyCollection( + polys, closed=True, linewidth=0.0, facecolor=darkred, zorder=-10 +) +ax.add_collection(collection) + +# Display text +patch = PathPatch(path, facecolor="white", edgecolor="none", zorder=10) +ax.add_artist(patch) + +# Transparent gradient to fade out shadow +I = np.zeros((200, 1, 4)) + red +ax.imshow(I, extent=[-11, 11, -15, 15], zorder=-20, clip_on=False) +I[:, 0, 3] = np.linspace(0, 1, len(I)) +ax.imshow(I, extent=[-11, 11, -15, 15], zorder=0, clip_on=False) + + +ax.text( + 6.5, + -1.75, + "a versatile scientific visualization library ", + color="white", + ha="right", + va="baseline", + size=10, + family="Pacifico", + zorder=30, +) + +# Save and show result +plt.savefig("../../figures/showcases/text-shadow.pdf") +plt.savefig("../../figures/showcases/text-shadow.png", dpi=600) +plt.show() + + +# Bad alternative (slow and memory hungry) +# ---------------------------------------- +# ax.text(0, 0, "MATPLOTLIB", color="white", ha="center", va="center", +# size=48, family="Source Sans Pro", weight=100, zorder=10) +# for x in np.linspace(0,7,500): +# ax.text(x, -x, "MATPLOTLIB", color="darkred", ha="center", va="center", +# size=48, family="Source Sans Pro", weight=100, zorder=-10) diff --git a/code/showcases/text-spiral.py b/code/showcases/text-spiral.py new file mode 100644 index 0000000..ed30b00 --- /dev/null +++ b/code/showcases/text-spiral.py @@ -0,0 +1,60 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import PathPatch +from matplotlib.textpath import TextPath +from matplotlib.collections import PolyCollection +from matplotlib.font_manager import FontProperties + +n = 100 +A = np.linspace(np.pi, n * 2 * np.pi, 10_000) +R = 5 + np.linspace(np.pi, n * 2 * np.pi, 10_000) +T = np.stack([R * np.cos(A), R * np.sin(A)], axis=1) +dx = np.cos(A) - R * np.sin(A) +dy = np.sin(A) + R * np.cos(A) +O = np.stack([-dy, dx], axis=1) +O = O / (np.linalg.norm(O, axis=1)).reshape(len(O), 1) + +L = np.zeros(len(T)) +np.cumsum(np.sqrt(((T[1:] - T[:-1]) ** 2).sum(axis=1)), out=L[1:]) + +import mpmath + +mpmath.mp.dps = 15000 +text = str(mpmath.pi) + +path = TextPath((0, 0), text, size=6, prop=FontProperties(family="Source Serif Pro")) +Vx, Vy = path.vertices[:, 0], path.vertices[:, 1] +X = np.interp(Vx, L, T[:, 0]) + Vy * np.interp(Vx, L, O[:, 0]) +Y = np.interp(Vx, L, T[:, 1]) + Vy * np.interp(Vx, L, O[:, 1]) +Vx[...], Vy[...] = X, Y + +fig = plt.figure(figsize=(8, 8)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1) +patch = PathPatch(path, facecolor="k", linewidth=0) +ax.add_artist(patch) + +# from matplotlib.patheffects import Stroke, Normal +# text = fig.text(.05, .060, "Scientific Visualization — Python & Matplotlib", +# va="bottom", size="16", color="white", +# transform=ax.transAxes, +# weight="bold", family="Roboto Condensed") +# text.set_path_effects([Stroke(linewidth=2, foreground="black"), Normal()]) +# text = fig.text(.05, .055, "github.com/rougier/scientific-visualization-book", +# size = 10.2, transform=ax.transAxes, +# va="top", color="blue", family="Roboto Mono", weight="bold") +# text.set_path_effects([Stroke(linewidth=5, foreground="white"), Normal()]) + +plt.rcParams["text.usetex"] = True +ax.text(-3, 0, "$\pi$", ha="center", va="center", size=500, color="white", alpha=0.6) + +ax.set_xlim(-200, 200), ax.set_xticks([]) +ax.set_ylim(-200, 200), ax.set_yticks([]) + +# plt.savefig("/Users/rougier/Desktop/spiral-pi.png", dpi=150) +plt.savefig("../../figures/showcases/text-spiral.pdf") +plt.show() diff --git a/code/showcases/waterfall-3d.py b/code/showcases/waterfall-3d.py new file mode 100644 index 0000000..cf9a4d6 --- /dev/null +++ b/code/showcases/waterfall-3d.py @@ -0,0 +1,153 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +from shapely.geometry import box, Polygon +from scipy.ndimage import gaussian_filter1d +from matplotlib.collections import PolyCollection +from matplotlib.transforms import Affine2D +from matplotlib.text import TextPath +from matplotlib.patches import PathPatch +from mpl_toolkits.mplot3d import Axes3D, art3d +from matplotlib.font_manager import FontProperties + + +def text3d(ax, xyz, s, zdir="z", size=0.1, angle=0, **kwargs): + x, y, z = xyz + if zdir == "y": + xy, z = (x, z), y + elif zdir == "x": + xy, z = (y, z), x + else: + xy, z = (x, y), z + path = TextPath((0, 0), s, size=size, prop=FontProperties(family="Roboto")) + V = path.vertices + V[:, 0] -= (V[:, 0].max() - V[:, 0].min()) / 2 + trans = Affine2D().rotate(angle).translate(xy[0], xy[1]) + path = PathPatch(trans.transform_path(path), **kwargs) + ax.add_patch(path) + art3d.pathpatch_2d_to_3d(path, z=z, zdir=zdir) + + +# Some nice but random curves +def random_curve(n=100): + Y = np.random.uniform(0, 1, n) + Y = gaussian_filter1d(Y, 1) + X = np.linspace(-1, 1, len(Y)) + Y *= np.exp(-2 * (X * X)) + return Y + + +def cmap_plot(Y, ymin=0, ymax=1, n=50, cmap="magma", y0=0): + X = np.linspace(0.3, 0.7, len(Y)) + Y = gaussian_filter1d(Y, 2) + + verts = [] + colors = [] + P = Polygon([(X[0], 0), *zip(X, Y), (X[-1], 0)]) + + dy = (ymax - ymin) / n + cmap = plt.cm.get_cmap(cmap) + cnorm = matplotlib.colors.Normalize(vmin=ymin, vmax=ymax) + + for y in np.arange(Y.min(), Y.max(), dy): + B = box(0, y, 10, y + dy) + I = P.intersection(B) + if hasattr(I, "geoms"): + for p in I.geoms: + V = np.array(p.exterior.coords) + V[:, 1] += y0 + verts.append(V) + colors.append(cmap(cnorm(y))) + else: + if I.exterior.coords: + V = np.array(I.exterior.coords) + V[:, 1] += y0 + verts.append(V) + colors.append(cmap(cnorm(y))) + + return verts, colors + + +fig = plt.figure(figsize=(10, 10)) +fig.patch.set_facecolor("black") +ax = fig.gca(projection="3d", proj_type="ortho") +ax.patch.set_facecolor("black") + +# Make panes transparent +ax.xaxis.pane.fill = False # Left pane +ax.yaxis.pane.fill = False # Right pane +ax.zaxis.pane.fill = False # Right pane + +# Remove grid lines +ax.grid(False) + +# Remove tick labels +ax.set_xticklabels([]) +ax.set_yticklabels([]) +ax.set_zticklabels([]) + +# Transparent spines +ax.w_xaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) +ax.w_yaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) +ax.w_zaxis.line.set_color((1.0, 1.0, 1.0, 0.0)) + +# Transparent panes +ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) +ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) +ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) + +# No ticks +ax.set_xticks([]) +ax.set_yticks([]) +ax.set_zticks([]) + + +np.random.seed(1) + +# text3d(ax, (1.05, 0.5, 0), "Series A", zdir="x", +# size=0.05, facecolor="white", edgecolor="None", alpha=.25) + +# text3d(ax, (1.05, 0.5, 0.6), "Series B", zdir="x", +# size=0.05, facecolor="white", edgecolor="None", alpha=.25) + +# text3d(ax, (1.05, 0.5, 1.2), "Series C", zdir="x", +# size=0.05, facecolor="white", edgecolor="None", alpha=.25) + + +for zs in np.linspace(0, 1, 50): + Y = 0.1 * random_curve() + verts, colors = cmap_plot(Y, ymin=0, ymax=0.075, n=50, cmap="magma", y0=-0.2) + collection = PolyCollection( + verts, antialiased=False, edgecolors="None", facecolor=colors + ) + ax.add_collection3d(collection, zdir="x", zs=zs) + + Y = 0.1 * random_curve() + verts, colors = cmap_plot(Y, ymin=0, ymax=0.075, n=50, cmap="magma", y0=0.4) + collection = PolyCollection( + verts, antialiased=False, edgecolors="None", facecolor=colors + ) + ax.add_collection3d(collection, zdir="x", zs=zs) + + Y = 0.1 * random_curve() + verts, colors = cmap_plot(Y, ymin=0, ymax=0.075, n=50, cmap="magma", y0=1.0) + collection = PolyCollection( + verts, antialiased=False, edgecolors="None", facecolor=colors + ) + ax.add_collection3d(collection, zdir="x", zs=zs) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +ax.set_zlim(0, 1) + +ax.view_init(elev=40, azim=-40) + +plt.tight_layout() +plt.savefig("../../figures/showcases/waterfall-3d.pdf") +# plt.savefig("./waterfall-3d.png", dpi=300) +plt.show() diff --git a/code/showcases/windmap.py b/code/showcases/windmap.py new file mode 100644 index 0000000..649a301 --- /dev/null +++ b/code/showcases/windmap.py @@ -0,0 +1,242 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation, writers +from matplotlib.collections import LineCollection + + +class Streamlines(object): + """ + Copyright (c) 2011 Raymond Speth. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + See: http://web.mit.edu/speth/Public/streamlines.py + """ + + def __init__( + self, X, Y, U, V, res=0.125, spacing=2, maxLen=2500, detectLoops=False + ): + """ + Compute a set of streamlines covering the given velocity field. + + X and Y - 1D or 2D (e.g. generated by np.meshgrid) arrays of the + grid points. The mesh spacing is assumed to be uniform + in each dimension. + U and V - 2D arrays of the velocity field. + res - Sets the distance between successive points in each + streamline (same units as X and Y) + spacing - Sets the minimum density of streamlines, in grid points. + maxLen - The maximum length of an individual streamline segment. + detectLoops - Determines whether an attempt is made to stop extending + a given streamline before reaching maxLen points if + it forms a closed loop or reaches a velocity node. + + Plots are generated with the 'plot' or 'plotArrows' methods. + """ + + self.spacing = spacing + self.detectLoops = detectLoops + self.maxLen = maxLen + self.res = res + + xa = np.asanyarray(X) + ya = np.asanyarray(Y) + self.x = xa if xa.ndim == 1 else xa[0] + self.y = ya if ya.ndim == 1 else ya[:, 0] + self.u = U + self.v = V + self.dx = (self.x[-1] - self.x[0]) / (self.x.size - 1) # assume a regular grid + self.dy = (self.y[-1] - self.y[0]) / (self.y.size - 1) # assume a regular grid + self.dr = self.res * np.sqrt(self.dx * self.dy) + + # marker for which regions have contours + self.used = np.zeros(self.u.shape, dtype=bool) + self.used[0] = True + self.used[-1] = True + self.used[:, 0] = True + self.used[:, -1] = True + + # Don't try to compute streamlines in regions where there is no velocity data + for i in range(self.x.size): + for j in range(self.y.size): + if self.u[j, i] == 0.0 and self.v[j, i] == 0.0: + self.used[j, i] = True + + # Make the streamlines + self.streamlines = [] + while not self.used.all(): + nz = np.transpose(np.logical_not(self.used).nonzero()) + # Make a streamline starting at the first unrepresented grid point + self.streamlines.append( + self._makeStreamline(self.x[nz[0][1]], self.y[nz[0][0]]) + ) + + def _interp(self, x, y): + """ Compute the velocity at point (x,y) """ + i = (x - self.x[0]) / self.dx + ai = i % 1 + + j = (y - self.y[0]) / self.dy + aj = j % 1 + + i, j = int(i), int(j) + + # Bilinear interpolation + u = ( + self.u[j, i] * (1 - ai) * (1 - aj) + + self.u[j, i + 1] * ai * (1 - aj) + + self.u[j + 1, i] * (1 - ai) * aj + + self.u[j + 1, i + 1] * ai * aj + ) + + v = ( + self.v[j, i] * (1 - ai) * (1 - aj) + + self.v[j, i + 1] * ai * (1 - aj) + + self.v[j + 1, i] * (1 - ai) * aj + + self.v[j + 1, i + 1] * ai * aj + ) + + self.used[j : j + self.spacing, i : i + self.spacing] = True + + return u, v + + def _makeStreamline(self, x0, y0): + """ + Compute a streamline extending in both directions from the given point. + """ + + sx, sy = self._makeHalfStreamline(x0, y0, 1) # forwards + rx, ry = self._makeHalfStreamline(x0, y0, -1) # backwards + + rx.reverse() + ry.reverse() + + return rx + [x0] + sx, ry + [y0] + sy + + def _makeHalfStreamline(self, x0, y0, sign): + """ + Compute a streamline extending in one direction from the given point. + """ + + xmin = self.x[0] + xmax = self.x[-1] + ymin = self.y[0] + ymax = self.y[-1] + + sx = [] + sy = [] + + x = x0 + y = y0 + i = 0 + while xmin < x < xmax and ymin < y < ymax: + u, v = self._interp(x, y) + theta = np.arctan2(v, u) + + x += sign * self.dr * np.cos(theta) + y += sign * self.dr * np.sin(theta) + sx.append(x) + sy.append(y) + + i += 1 + + if self.detectLoops and i % 10 == 0 and self._detectLoop(sx, sy): + break + + if i > self.maxLen / 2: + break + + return sx, sy + + def _detectLoop(self, xVals, yVals): + """ Detect closed loops and nodes in a streamline. """ + x = xVals[-1] + y = yVals[-1] + D = np.array( + [np.hypot(x - xj, y - yj) for xj, yj in zip(xVals[:-1], yVals[:-1])] + ) + return (D < 0.9 * self.dr).any() + + +# See https://stackoverflow.com/questions/11578760/ +# matplotlib-control-capstyle-of-line-collection-large-number-of-lines +# ----------------------------------------------------------------------------- +import types +from matplotlib.backend_bases import GraphicsContextBase, RendererBase + + +class GC(GraphicsContextBase): + def __init__(self): + super().__init__() + self._capstyle = "round" + + +def custom_new_gc(self): + return GC() + + +RendererBase.new_gc = types.MethodType(custom_new_gc, RendererBase) + + +# ----------------------------------------------------------------------------- +Y, X = np.mgrid[-3:3:100j, -3:3:100j] +U, V = -1 - X ** 2 + Y, 1 + X - X * Y ** 2 +speed = np.sqrt(U * U + V * V) + +fig = plt.figure(figsize=(8, 8)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1, frameon=False) + +lengths = [] +colors = [] +lines = [] + +cmap = plt.get_cmap("Blues_r") +s = Streamlines(X, Y, U, V) +for streamline in s.streamlines: + x, y = streamline + points = np.array([x, y]).T.reshape(-1, 1, 2) + segments = np.concatenate([points[:-1], points[1:]], axis=1) + n = len(segments) + + D = np.sqrt(((points[1:] - points[:-1]) ** 2).sum(axis=-1)) + L = D.cumsum().reshape(n, 1) + np.random.uniform(0, 1) + C = np.zeros((n, 3)) + C[:] = (L * 1.5) % 1 + + C = cmap(((L * 1.5) % 1).ravel()) + + linewidths = np.zeros(n) + linewidths[:] = 2 - ((L.reshape(n) * 1.5) % 1) + line = LineCollection(segments, color=C, linewidth=linewidths) + + lengths.append(L) + colors.append(C) + lines.append(line) + + ax.add_collection(line) + +ax.set_xlim(-3, +3), ax.set_xticks([]) +ax.set_ylim(-3, +3), ax.set_yticks([]) +plt.savefig("../../figures/showcases/windmap.png", dpi=600) +plt.show() diff --git a/code/threed/bunnies.py b/code/threed/bunnies.py new file mode 100644 index 0000000..c0a0a6d --- /dev/null +++ b/code/threed/bunnies.py @@ -0,0 +1,134 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def ortho(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=float) + M[0, 0] = +2.0 / (right - left) + M[1, 1] = +2.0 / (top - bottom) + M[2, 2] = -2.0 / (zfar - znear) + M[3, 3] = 1.0 + M[0, 2] = -(right + left) / float(right - left) + M[1, 3] = -(top + bottom) / float(top - bottom) + M[2, 3] = -(zfar + znear) / float(zfar - znear) + return M + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + + +def mesh(MVP, V, F, cmap=None, clip=True): + V = np.c_[V, np.ones(len(V))] @ MVP.T + V /= V[:, 3].reshape(-1, 1) + V = V[F] + T = V[:, :, :2] + Z = -V[:, :, 2].mean(axis=1) + zmin, zmax = Z.min(), Z.max() + Z = (Z - zmin) / (zmax - zmin) + I = np.argsort(Z) + T = T[I, :] + if cmap is not None: + C = plt.get_cmap(cmap)(Z) + C = C[I, :] + else: + C = 1.0, 1.0, 1.0, 0.5 + return PolyCollection( + T, closed=True, linewidth=0.1, clip_on=clip, facecolor=C, edgecolor="black" + ) + + +fig = plt.figure(figsize=(6, 6)) + +ax = plt.subplot( + 2, 2, 1, xlim=[-1, +1], ylim=[-1, +1], xticks=[], yticks=[], aspect=1, frameon=False +) +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -2.5) +proj = perspective(25, 1, 1, 100) +ax.add_collection(mesh(proj @ view @ model, V, F, "magma", False)) + +ax = plt.subplot( + 2, 2, 2, xlim=[-1, +1], ylim=[-1, +1], xticks=[], yticks=[], aspect=1, frameon=False +) +model = xrotate(90) +view = np.eye(4) +proj = ortho(-1, 1, -1, 1, 0, 100) +ax.add_collection(mesh(proj @ view @ model, 2 * V, F)) + +ax = plt.subplot( + 2, 2, 3, xlim=[-1, +1], ylim=[-1, +1], xticks=[], yticks=[], aspect=1, frameon=False +) +model = yrotate(90) +view = np.eye(4) +proj = ortho(-1, 1, -1, 1, 0, 100) +ax.add_collection(mesh(proj @ view @ model, 2 * V, F)) + +ax = plt.subplot( + 2, 2, 4, xlim=[-1, +1], ylim=[-1, +1], xticks=[], yticks=[], aspect=1, frameon=False +) +model = view = np.eye(4) +proj = ortho(-1, 1, -1, 1, 0, 100) +ax.add_collection(mesh(proj @ view @ model, 2 * V, F)) + +plt.tight_layout() +plt.savefig("../../figures/threed/bunnies.pdf") +plt.show() diff --git a/code/threed/bunny-1.py b/code/threed/bunny-1.py new file mode 100644 index 0000000..ef25d3e --- /dev/null +++ b/code/threed/bunny-1.py @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) +T = V[F][..., :2] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="None", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-1.pdf") +plt.show() diff --git a/code/threed/bunny-2.py b/code/threed/bunny-2.py new file mode 100644 index 0000000..5e0a82b --- /dev/null +++ b/code/threed/bunny-2.py @@ -0,0 +1,57 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) +V = np.c_[V, np.ones(len(V))] @ perspective(25, 1, 1, 100).T +V /= V[:, 3].reshape(-1, 1) +T = V[F][..., :2] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="None", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-2.pdf") +plt.show() diff --git a/code/threed/bunny-3.py b/code/threed/bunny-3.py new file mode 100644 index 0000000..bb867c7 --- /dev/null +++ b/code/threed/bunny-3.py @@ -0,0 +1,58 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) +V += (0, 0, -3.5) +V = np.c_[V, np.ones(len(V))] @ perspective(25, 1, 1, 100).T +V /= V[:, 3].reshape(-1, 1) +T = V[F][..., :2] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="None", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-3.pdf") +plt.show() diff --git a/code/threed/bunny-4.py b/code/threed/bunny-4.py new file mode 100644 index 0000000..5d25aa0 --- /dev/null +++ b/code/threed/bunny-4.py @@ -0,0 +1,85 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -3.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +V = np.c_[V, np.ones(len(V))] @ MVP.T +V /= V[:, 3].reshape(-1, 1) +T = V[F][..., :2] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="None", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-4.pdf") +plt.show() diff --git a/code/threed/bunny-5.py b/code/threed/bunny-5.py new file mode 100644 index 0000000..faac679 --- /dev/null +++ b/code/threed/bunny-5.py @@ -0,0 +1,95 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + + +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -3.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +V_ = np.c_[V, np.ones(len(V))] @ MVP.T +V_ /= V_[:, 3].reshape(-1, 1) +T = V_[F][..., :2] + +fig = plt.figure(figsize=(8, 8)) +for i, (fovy, z) in enumerate([(25, -3), (40, -2.0), (65, -1.25), (80, -1.0)]): + view = translate(0, 0, z) + proj = perspective(fovy, 1, 1, 100) + MVP = proj @ view @ model + V_ = np.c_[V, np.ones(len(V))] @ MVP.T + V_ /= V_[:, 3].reshape(-1, 1) + T = V_[F][..., :2] + ax = plt.subplot(2, 2, i + 1, xlim=[-1, +1], ylim=[-1, +1], aspect=1) + ax.axis("off") + collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="None", edgecolor="black" + ) + ax.add_collection(collection) + ax.set_title("Aperture: %d" % fovy) + +plt.tight_layout() +plt.savefig("../../figures/threed/bunny-5.pdf") +plt.show() diff --git a/code/threed/bunny-6.py b/code/threed/bunny-6.py new file mode 100644 index 0000000..1555256 --- /dev/null +++ b/code/threed/bunny-6.py @@ -0,0 +1,85 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -3.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +V = np.c_[V, np.ones(len(V))] @ MVP.T +V /= V[:, 3].reshape(-1, 1) +T = V[F][..., :2] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="0.9", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-6.pdf") +plt.show() diff --git a/code/threed/bunny-7.py b/code/threed/bunny-7.py new file mode 100644 index 0000000..9b14610 --- /dev/null +++ b/code/threed/bunny-7.py @@ -0,0 +1,91 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -3.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +V = np.c_[V, np.ones(len(V))] @ MVP.T +V /= V[:, 3].reshape(-1, 1) +V = V[F] + +T = V[:, :, :2] +Z = -V[:, :, 2].mean(axis=1) +I = np.argsort(Z) +T = T[I, :] + + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor="0.9", edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-7.pdf") +plt.show() diff --git a/code/threed/bunny-8.py b/code/threed/bunny-8.py new file mode 100644 index 0000000..13f0bf3 --- /dev/null +++ b/code/threed/bunny-8.py @@ -0,0 +1,93 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * np.radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +# Data processing +V, F = [], [] +with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + F.append([int(x) for x in values[1:4]]) +V, F = np.array(V), np.array(F) - 1 + +V = (V - (V.max(0) + V.min(0)) / 2) / max(V.max(0) - V.min(0)) + +model = xrotate(20) @ yrotate(45) +view = translate(0, 0, -3.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +V = np.c_[V, np.ones(len(V))] @ MVP.T +V /= V[:, 3].reshape(-1, 1) +V = V[F] + +T = V[:, :, :2] +Z = -V[:, :, 2].mean(axis=1) +zmin, zmax = Z.min(), Z.max() +Z = (Z - zmin) / (zmax - zmin) +C = plt.get_cmap("magma")(Z) +I = np.argsort(Z) +T, C = T[I, :], C[I, :] + +# Rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1, frameon=False) +collection = PolyCollection( + T, closed=True, linewidth=0.1, facecolor=C, edgecolor="black" +) +ax.add_collection(collection) +plt.savefig("../../figures/threed/bunny-8.pdf") +plt.show() diff --git a/code/threed/bunny.obj b/code/threed/bunny.obj new file mode 100644 index 0000000..18abc08 --- /dev/null +++ b/code/threed/bunny.obj @@ -0,0 +1,7474 @@ +# OBJ file format with ext .obj +# vertex count = 2503 +# face count = 4968 +v -3.4101800e-003 1.3031957e-001 2.1754370e-002 +v -8.1719160e-002 1.5250145e-001 2.9656090e-002 +v -3.0543480e-002 1.2477885e-001 1.0983400e-003 +v -2.4901590e-002 1.1211138e-001 3.7560240e-002 +v -1.8405680e-002 1.7843055e-001 -2.4219580e-002 +v 1.9067940e-002 1.2144925e-001 3.1968440e-002 +v 6.0412000e-003 1.2494359e-001 3.2652890e-002 +v -1.3469030e-002 1.6299355e-001 -1.2000020e-002 +v -3.4393240e-002 1.7236688e-001 -9.8213000e-004 +v -8.4314160e-002 1.0957263e-001 3.7097300e-003 +v -4.2233540e-002 1.7211574e-001 -4.1799800e-003 +v -6.3308390e-002 1.5660615e-001 -1.3838790e-002 +v -7.6903950e-002 1.6708033e-001 -2.6931360e-002 +v -7.2253920e-002 1.1539550e-001 5.1670300e-002 +v 1.2981330e-002 1.1366375e-001 3.8302950e-002 +v -3.7857280e-002 1.7010102e-001 1.4236000e-003 +v 4.8689400e-003 3.7962370e-002 4.5867630e-002 +v -5.7180550e-002 4.0918830e-002 4.6301340e-002 +v -4.5209070e-002 3.8839100e-002 4.4503770e-002 +v -3.3761490e-002 1.2617876e-001 1.7132300e-003 +v -5.0242270e-002 1.5773747e-001 9.3944500e-003 +v -2.1216950e-002 1.5887938e-001 -4.6923700e-003 +v -5.6472950e-002 1.5778406e-001 8.1786500e-003 +v -5.2802060e-002 4.1319860e-002 4.6169800e-002 +v -4.9960340e-002 4.3101950e-002 4.4462650e-002 +v -2.9748750e-002 3.6539860e-002 5.2493310e-002 +v -3.5438900e-003 4.2659770e-002 4.7541530e-002 +v 4.9304900e-003 4.1982660e-002 4.5723390e-002 +v -3.9088180e-002 1.6872020e-001 -1.1924680e-002 +v -5.6901000e-002 4.5437000e-002 4.3236960e-002 +v -4.1244880e-002 4.3098890e-002 4.2129560e-002 +v -2.6471980e-002 4.5034530e-002 5.1219460e-002 +v -2.1866970e-002 4.4022930e-002 5.3243800e-002 +v -3.6996250e-002 1.6899301e-001 1.3256300e-003 +v -6.7216590e-002 1.6171340e-001 -1.3733710e-002 +v 4.9760060e-002 7.0235220e-002 2.3732020e-002 +v -4.9186640e-002 4.6411230e-002 4.1170040e-002 +v -4.4590380e-002 4.3797990e-002 4.2685460e-002 +v -4.3686470e-002 4.7154500e-002 4.0286310e-002 +v -2.2491950e-002 4.6513620e-002 5.1885310e-002 +v -6.5174200e-003 4.5036200e-002 4.7502780e-002 +v 3.7699000e-004 4.4935790e-002 4.6519930e-002 +v 3.4023920e-002 1.1353879e-001 2.4595280e-002 +v -2.6467900e-002 1.8104250e-001 -8.0811700e-003 +v -1.7533470e-002 4.7964250e-002 4.8829630e-002 +v -7.0012600e-003 4.6416520e-002 4.7485540e-002 +v 5.9862300e-003 4.6689140e-002 4.9073620e-002 +v 9.1007200e-003 4.8474490e-002 4.9353190e-002 +v -3.5453700e-002 1.1244769e-001 3.5055410e-002 +v -7.5983200e-002 1.3820800e-001 4.9216580e-002 +v 3.4838440e-002 4.3153410e-002 2.8954310e-002 +v -5.2655550e-002 4.8494220e-002 3.8731190e-002 +v -4.7378940e-002 4.8456670e-002 3.9126790e-002 +v -3.8933750e-002 4.6364270e-002 4.0364780e-002 +v -2.6468940e-002 4.7816430e-002 4.9322590e-002 +v -2.2365790e-002 4.8073650e-002 5.0126500e-002 +v -1.3373430e-002 4.7892410e-002 4.7883850e-002 +v -1.2193490e-002 4.9470300e-002 4.9484490e-002 +v -6.3364000e-004 4.7193060e-002 4.9136900e-002 +v 2.0656800e-003 5.0104680e-002 5.2290220e-002 +v -2.2749270e-002 4.9883880e-002 4.6605520e-002 +v -1.8002080e-002 4.9917850e-002 4.6947970e-002 +v -7.8036800e-003 5.0169310e-002 5.0988650e-002 +v -2.6843800e-003 5.1247420e-002 5.3186790e-002 +v -6.3875650e-002 1.6140094e-001 -2.0064210e-002 +v 3.2434000e-002 4.5333970e-002 3.0316760e-002 +v -8.8064570e-002 1.2496764e-001 5.7412000e-004 +v -4.1503710e-002 1.6748512e-001 3.2765900e-003 +v -6.4457010e-002 1.5342891e-001 -5.1180400e-003 +v -3.4303190e-002 5.0520150e-002 3.8286020e-002 +v -2.2949400e-002 5.1020650e-002 4.3926450e-002 +v -1.4354710e-002 5.4428200e-002 5.0710310e-002 +v 1.3773100e-003 5.2302710e-002 5.3149010e-002 +v 3.6285000e-003 5.3198640e-002 5.3422710e-002 +v 8.0723800e-003 5.1574140e-002 5.1773560e-002 +v -7.2665890e-002 1.3005582e-001 5.1668200e-002 +v 3.7992780e-002 4.9793200e-002 3.1902020e-002 +v 3.8497260e-002 4.8062400e-002 3.1737450e-002 +v 2.1503510e-002 1.2563988e-001 2.1252620e-002 +v -7.6481330e-002 1.4827412e-001 -8.9376200e-003 +v -8.7240410e-002 1.1967213e-001 -1.7813000e-004 +v -4.3719960e-002 1.6822738e-001 2.3425000e-003 +v -4.0652200e-002 1.2266506e-001 2.6290300e-002 +v -4.6686180e-002 5.4570720e-002 3.7587370e-002 +v -4.4071750e-002 5.1058250e-002 3.8977810e-002 +v -3.8144110e-002 5.0599600e-002 3.9302160e-002 +v -1.9875770e-002 5.1607710e-002 4.6142000e-002 +v -1.6911250e-002 5.1843550e-002 4.8459320e-002 +v -1.6249190e-002 5.4292110e-002 5.0306940e-002 +v -1.0446540e-002 5.3685970e-002 5.1958610e-002 +v -4.3090900e-003 5.4467500e-002 5.3908250e-002 +v 7.8152700e-003 5.5050680e-002 5.2750250e-002 +v 3.7955090e-002 1.0488710e-001 -3.2031800e-003 +v -7.9003790e-002 1.2850550e-001 5.3149340e-002 +v -7.9778990e-002 1.3448894e-001 5.0990290e-002 +v -5.9129700e-002 1.5039712e-001 3.4489540e-002 +v -6.5691790e-002 1.4961818e-001 3.8160980e-002 +v -3.1951660e-002 1.2518394e-001 1.9400580e-002 +v -6.9372590e-002 1.6061775e-001 -9.1905000e-003 +v -4.5225500e-002 1.2935459e-001 2.0377520e-002 +v -4.1879110e-002 5.6164390e-002 3.9796700e-002 +v -3.0614840e-002 5.4412650e-002 3.6694290e-002 +v -2.4787600e-002 5.2606220e-002 4.0839760e-002 +v -2.1588860e-002 5.6836920e-002 4.5467040e-002 +v -2.4264000e-004 5.4536020e-002 5.4641200e-002 +v -8.0900510e-002 1.2558713e-001 5.2155370e-002 +v -2.9996210e-002 1.7811137e-001 -5.2358200e-003 +v 3.5515390e-002 5.0449570e-002 3.1439830e-002 +v 4.3315550e-002 5.2145550e-002 3.2492110e-002 +v -6.3938540e-002 1.5262699e-001 3.4481070e-002 +v -4.4489440e-002 6.1077710e-002 3.9545320e-002 +v -3.8979900e-002 5.7996270e-002 4.0151390e-002 +v -7.9087730e-002 1.7044488e-001 -4.1373170e-002 +v -4.6247300e-003 5.7759650e-002 5.3990710e-002 +v -1.4985500e-003 5.5925480e-002 5.4630800e-002 +v 5.1981700e-003 5.7017990e-002 5.3423530e-002 +v 3.0920000e-005 1.2315746e-001 3.4749660e-002 +v 3.3568300e-002 1.1523716e-001 2.1798410e-002 +v 3.8686300e-002 5.6450590e-002 3.1188930e-002 +v -3.4385780e-002 5.4096000e-002 3.8060290e-002 +v -8.5308300e-003 6.0159420e-002 5.5308950e-002 +v -4.4024000e-004 5.8343410e-002 5.4483410e-002 +v -9.1078730e-002 1.1506037e-001 4.0141810e-002 +v 4.0775480e-002 5.4557490e-002 3.2014740e-002 +v 4.5636880e-002 5.7402620e-002 3.1992220e-002 +v 2.0358850e-002 1.2448747e-001 2.5906340e-002 +v -1.4169700e-002 1.2767892e-001 1.3080500e-003 +v -1.1987590e-002 5.7493210e-002 5.2752420e-002 +v 3.2514500e-003 5.9828640e-002 5.5464300e-002 +v -1.2395240e-002 1.2264726e-001 3.3588280e-002 +v 1.3813780e-002 1.2322188e-001 3.2502590e-002 +v -7.7004310e-002 1.5521281e-001 2.4534770e-002 +v -2.8001360e-002 6.1075420e-002 3.7471210e-002 +v -8.5480000e-004 6.0593520e-002 5.5824810e-002 +v -3.8050200e-002 1.1527068e-001 3.3178540e-002 +v -1.6231340e-002 1.2382942e-001 2.9576990e-002 +v -2.5373550e-002 1.5840012e-001 -1.4801300e-003 +v -6.7818590e-002 1.5454353e-001 3.0233720e-002 +v -4.3082600e-003 6.1418570e-002 5.5688490e-002 +v -3.1958900e-003 1.1912518e-001 3.8349580e-002 +v -6.4292400e-003 1.2201090e-001 3.5740890e-002 +v 4.2312960e-002 5.9099150e-002 3.0848420e-002 +v 4.8510010e-002 6.1780760e-002 3.0347250e-002 +v 5.0412290e-002 6.0312610e-002 3.0245060e-002 +v -3.9185590e-002 6.3074530e-002 4.1382890e-002 +v -3.4448660e-002 6.0780500e-002 3.9543990e-002 +v -1.4746030e-002 6.5583910e-002 5.3730860e-002 +v 2.6645200e-003 6.2700010e-002 5.6525210e-002 +v -1.3991610e-002 1.1962575e-001 3.6251540e-002 +v 1.9659170e-002 1.1236219e-001 3.7545270e-002 +v -3.2597160e-002 1.7498725e-001 -2.5953100e-003 +v -2.1513900e-003 9.9437380e-002 4.9849750e-002 +v -5.6001390e-002 6.1830670e-002 2.7931150e-002 +v -5.4707260e-002 6.3461570e-002 3.1670590e-002 +v -5.1307940e-002 6.0521660e-002 3.1434930e-002 +v -4.1979320e-002 6.9629980e-002 4.1824930e-002 +v -3.0272490e-002 6.2474660e-002 3.7982220e-002 +v -1.1387860e-002 6.4742460e-002 5.4918000e-002 +v 6.9544900e-003 6.4700130e-002 5.5599150e-002 +v 4.3015090e-002 9.7690960e-002 1.0258300e-003 +v 4.0635900e-002 6.1574860e-002 2.9841250e-002 +v 4.6183560e-002 6.1910110e-002 3.0223400e-002 +v 3.7552960e-002 1.0685291e-001 2.6303470e-002 +v -7.8640730e-002 1.6387238e-001 -2.8387790e-002 +v -6.1996240e-002 1.4761484e-001 -4.3256800e-003 +v -5.7499800e-003 6.5488980e-002 5.6173390e-002 +v 2.5369000e-004 6.5741170e-002 5.6569260e-002 +v -2.0542550e-002 1.1979518e-001 3.3003670e-002 +v 4.3155900e-003 1.2782561e-001 2.8646880e-002 +v -4.6549580e-002 6.7652130e-002 3.9635790e-002 +v -1.7420580e-002 6.9659490e-002 5.4089530e-002 +v -1.5242190e-002 7.0909900e-002 5.5004790e-002 +v -1.0282890e-002 6.8926360e-002 5.5289610e-002 +v -1.1289000e-004 6.9288200e-002 5.6579790e-002 +v -3.6309330e-002 1.1876943e-001 3.0674020e-002 +v -7.0325800e-002 6.3367770e-002 1.9809180e-002 +v 4.3023100e-002 6.3795810e-002 2.8039210e-002 +v 4.2831110e-002 8.5556040e-002 2.7873760e-002 +v 1.6981600e-002 1.2715003e-001 2.2931490e-002 +v -4.2121490e-002 1.2825104e-001 1.0751500e-003 +v 1.6329230e-002 1.2251895e-001 3.1375390e-002 +v -8.1264160e-002 1.5381172e-001 2.5897830e-002 +v -3.2257870e-002 8.8192600e-002 -2.5130960e-002 +v -1.3774950e-002 7.0887950e-002 5.4695630e-002 +v 5.2929600e-003 6.8006030e-002 5.5670490e-002 +v 7.6962500e-003 7.2375600e-002 5.6062150e-002 +v 3.4830600e-003 1.2002635e-001 3.6911950e-002 +v 6.6532500e-003 1.1673563e-001 3.8716340e-002 +v 4.6086570e-002 6.6473930e-002 2.6808990e-002 +v 5.2327290e-002 6.4327070e-002 2.8281890e-002 +v -6.1897630e-002 1.2297065e-001 -8.7725500e-003 +v -6.3934700e-003 1.0524472e-001 -2.2841900e-002 +v -3.5218330e-002 6.8559830e-002 4.1381470e-002 +v -3.2689880e-002 6.7729720e-002 4.0124390e-002 +v -2.9245440e-002 6.9551520e-002 3.9369010e-002 +v -5.0024500e-003 6.9655000e-002 5.6892510e-002 +v 1.6573960e-002 1.1890153e-001 3.5042300e-002 +v -8.9385100e-002 9.9024040e-002 1.7521830e-002 +v 4.5719230e-002 6.9489400e-002 2.3549340e-002 +v 5.4537210e-002 6.8796720e-002 2.4517690e-002 +v -4.4989450e-002 7.1577330e-002 4.1929250e-002 +v -4.2439400e-003 1.2914902e-001 2.5829230e-002 +v -7.3880090e-002 1.2091638e-001 5.3395800e-002 +v -7.4033870e-002 1.4406894e-001 4.4994970e-002 +v 5.0400010e-002 6.7292480e-002 2.6851470e-002 +v -5.4056890e-002 1.5671602e-001 -2.4865900e-003 +v 2.6148110e-002 1.2014725e-001 2.7308010e-002 +v -1.0736490e-002 1.2990285e-001 1.0993790e-002 +v -4.5078840e-002 8.7261130e-002 -2.1865520e-002 +v -3.8340900e-002 6.8843770e-002 4.1846470e-002 +v -2.9255580e-002 7.5169210e-002 4.1186430e-002 +v -4.7311210e-002 1.6296037e-001 6.0740300e-003 +v -1.1866030e-002 7.3183750e-002 5.6250050e-002 +v -6.3734600e-003 7.2184340e-002 5.7972980e-002 +v -2.9935300e-003 7.2186440e-002 5.8167190e-002 +v -2.5781060e-002 9.3778180e-002 -2.8388220e-002 +v -1.6692560e-002 1.1568553e-001 3.7853150e-002 +v -8.4123410e-002 1.0832050e-001 2.4730980e-002 +v -7.4294080e-002 1.6356850e-001 -1.5534220e-002 +v -9.4297150e-002 1.2617744e-001 1.9224650e-002 +v -3.5207090e-002 1.2505219e-001 2.1635690e-002 +v -4.9495940e-002 7.3436340e-002 4.1673570e-002 +v -3.3064160e-002 7.6654840e-002 4.1277900e-002 +v -7.3157300e-003 7.3919590e-002 5.7971690e-002 +v 2.1850000e-005 7.3496040e-002 5.7696650e-002 +v 4.1934400e-003 7.2915170e-002 5.6298730e-002 +v -7.7256080e-002 1.4565854e-001 4.3122930e-002 +v 4.1073260e-002 8.8724320e-002 -9.7879400e-003 +v 3.7418710e-002 1.0850822e-001 3.3973000e-004 +v -5.5111380e-002 7.4687840e-002 4.1939740e-002 +v -4.2740230e-002 7.6995340e-002 4.2804080e-002 +v -6.8531190e-002 1.5630045e-001 2.0997710e-002 +v -9.9440200e-003 7.6343100e-002 5.7388560e-002 +v -3.2479200e-003 7.5710690e-002 5.8714640e-002 +v 1.3414380e-002 9.3073740e-002 5.1467750e-002 +v -7.3504440e-002 9.3883340e-002 -1.4751720e-002 +v -7.4471830e-002 1.3507476e-001 5.0688900e-002 +v -2.5851310e-002 1.2182948e-001 2.6079670e-002 +v -3.4022940e-002 1.7597076e-001 -3.7271600e-003 +v -7.5405850e-002 1.6839072e-001 -2.6792980e-002 +v -3.6658410e-002 7.5087300e-002 4.2006940e-002 +v -1.7795480e-002 7.7486190e-002 5.6087240e-002 +v -1.1378660e-002 7.9877150e-002 5.7698880e-002 +v -1.0415000e-004 7.6881950e-002 5.8190740e-002 +v 2.7381400e-003 7.9105680e-002 5.6719190e-002 +v 5.5681200e-003 7.6397140e-002 5.6327220e-002 +v -6.1895860e-002 1.5424247e-001 -1.9018600e-002 +v -7.2646960e-002 1.4098943e-001 4.6976640e-002 +v 1.5799740e-002 1.2901416e-001 1.3236870e-002 +v -1.1703420e-002 9.7355720e-002 5.1592080e-002 +v -5.8922160e-002 7.7545490e-002 4.2961390e-002 +v -5.3121320e-002 7.7912430e-002 4.3334920e-002 +v -5.0745740e-002 7.6148400e-002 4.3137630e-002 +v -4.7401820e-002 7.5550340e-002 4.2630140e-002 +v -4.5055620e-002 7.8796280e-002 4.2341310e-002 +v -3.9517650e-002 7.8127780e-002 4.2918620e-002 +v -1.5245570e-002 8.2940770e-002 5.6934590e-002 +v -1.4557790e-002 7.6582160e-002 5.6493250e-002 +v -5.9406000e-003 7.9038240e-002 5.7969830e-002 +v 3.7176540e-002 1.1064404e-001 1.8811330e-002 +v 2.3929700e-003 1.3162713e-001 1.1955100e-002 +v -9.3644210e-002 1.1789378e-001 1.8662080e-002 +v -6.3939810e-002 7.8621830e-002 4.2083520e-002 +v -4.5376460e-002 8.2383550e-002 4.3282120e-002 +v -3.6505460e-002 8.1152260e-002 4.3162320e-002 +v -3.3244340e-002 8.2266590e-002 4.1852180e-002 +v -3.0800650e-002 8.0068420e-002 4.1798070e-002 +v -2.0578500e-003 8.0998290e-002 5.7553840e-002 +v 8.1848100e-003 8.0756170e-002 5.5374510e-002 +v -1.2953370e-002 1.1593580e-001 3.8920230e-002 +v -7.8081470e-002 1.2351940e-001 5.2136990e-002 +v -2.6580930e-002 1.5567694e-001 -4.1963400e-003 +v -8.2471600e-002 1.1624130e-001 -2.3236300e-003 +v -2.7538480e-002 7.9964780e-002 4.7697210e-002 +v 1.2556400e-003 8.3845570e-002 5.7446440e-002 +v 6.1508300e-003 8.3406240e-002 5.6463500e-002 +v -6.2433240e-002 8.4035270e-002 4.4203120e-002 +v -5.9867170e-002 8.0540510e-002 4.3277090e-002 +v -5.5238340e-002 8.1999450e-002 4.4984770e-002 +v -5.4000400e-002 8.0568410e-002 4.4601460e-002 +v -5.0027020e-002 8.1311330e-002 4.4264180e-002 +v -4.1996120e-002 8.1083670e-002 4.2456150e-002 +v -3.9357940e-002 8.3631380e-002 4.3502350e-002 +v -8.6161480e-002 1.0838594e-001 1.8244920e-002 +v -8.6723010e-002 9.9917250e-002 3.5537100e-003 +v -2.2413700e-002 8.3283520e-002 5.5590700e-002 +v -1.6993180e-002 8.2555820e-002 5.7523880e-002 +v -1.2406010e-002 8.5222570e-002 5.7267780e-002 +v -7.4442100e-003 1.1693417e-001 3.9283850e-002 +v -2.1452000e-003 1.1143287e-001 4.2436620e-002 +v -7.5718220e-002 1.2522734e-001 5.3087330e-002 +v -7.7056660e-002 1.3193469e-001 5.2462430e-002 +v -6.1121040e-002 1.5569660e-001 2.2517050e-002 +v -3.7538540e-002 1.2744127e-001 1.5320870e-002 +v -2.0516700e-003 1.0093469e-001 4.5625920e-002 +v -6.4992150e-002 8.4550900e-002 4.4120060e-002 +v -5.7861950e-002 8.3944360e-002 4.4186030e-002 +v -4.5681080e-002 8.4988010e-002 4.4159500e-002 +v -3.5022640e-002 8.2888160e-002 4.2912760e-002 +v -2.9982010e-002 8.5402300e-002 4.3745080e-002 +v -8.8892260e-002 9.9209100e-002 9.5703200e-003 +v -1.9135300e-002 8.3474800e-002 5.7217390e-002 +v -8.3489710e-002 1.0724729e-001 7.5790000e-004 +v -7.0112800e-002 1.1790350e-001 5.2714160e-002 +v -3.5526320e-002 1.7595563e-001 -4.8676200e-003 +v -7.0831390e-002 1.2254425e-001 5.3274880e-002 +v 4.5133810e-002 9.3630690e-002 6.2336800e-003 +v -5.3616700e-002 8.5346850e-002 4.5332470e-002 +v -4.9000840e-002 8.6221680e-002 4.5352040e-002 +v -3.6744880e-002 8.6083690e-002 4.3612890e-002 +v -1.0872600e-002 8.8826770e-002 5.6665490e-002 +v -3.8450200e-003 8.4787810e-002 5.7197980e-002 +v -4.9020070e-002 1.1771293e-001 3.1581430e-002 +v -4.2914400e-002 1.1835991e-001 3.0645040e-002 +v -5.7684530e-002 1.5561695e-001 1.2983110e-002 +v -2.5411730e-002 1.2472533e-001 1.2886000e-004 +v 1.9012230e-002 1.2736197e-001 1.7786580e-002 +v -5.9498600e-002 8.8845470e-002 4.5109290e-002 +v -5.6931050e-002 8.8101500e-002 4.4692930e-002 +v 3.5765600e-003 1.3138981e-001 7.2086000e-003 +v -1.6683350e-002 8.7266690e-002 5.6741190e-002 +v -8.4980800e-003 8.3990470e-002 5.7605220e-002 +v 3.5078200e-003 8.6339520e-002 5.7048320e-002 +v -2.8398700e-002 1.8070650e-001 -7.8469500e-003 +v -7.6565830e-002 1.1674037e-001 5.1489350e-002 +v 1.7869430e-002 9.0898610e-002 4.8712940e-002 +v -4.0342100e-002 1.1669551e-001 3.2460200e-002 +v 5.9105700e-003 1.3140929e-001 1.6823750e-002 +v -8.5777550e-002 9.1701370e-002 -4.6970000e-005 +v -5.0372230e-002 8.8844660e-002 4.5188000e-002 +v -4.4434130e-002 8.7654530e-002 4.3477620e-002 +v -4.2056390e-002 8.6711520e-002 4.2534630e-002 +v -3.3058460e-002 8.6185500e-002 4.2560350e-002 +v -2.9241910e-002 9.0453360e-002 4.4236610e-002 +v -6.8964100e-003 8.4432910e-002 5.7168580e-002 +v -6.6210600e-003 9.0415250e-002 5.6879750e-002 +v -1.2439100e-003 8.9093200e-002 5.6552120e-002 +v 9.4076000e-003 9.0328050e-002 5.4214140e-002 +v 4.0194810e-002 1.0231597e-001 -2.0048600e-003 +v -8.6227130e-002 1.1466841e-001 2.2102000e-003 +v -8.9495490e-002 9.5632430e-002 1.4234810e-002 +v -6.7132160e-002 1.5709447e-001 -6.2032000e-003 +v -5.2935640e-002 9.0913520e-002 4.4568870e-002 +v -3.6744910e-002 8.8886950e-002 4.3312050e-002 +v -1.3626110e-002 8.9787930e-002 5.6674380e-002 +v 2.3337130e-002 1.2353449e-001 2.4874140e-002 +v -3.7053790e-002 1.2715094e-001 3.5474000e-004 +v -7.3696690e-002 1.5613015e-001 1.4359790e-002 +v -6.5592380e-002 9.1042400e-002 4.4092080e-002 +v -5.8997380e-002 9.2030670e-002 4.5335270e-002 +v -3.3238910e-002 8.8573580e-002 4.3697040e-002 +v -3.1834990e-002 9.0722970e-002 4.4173460e-002 +v -2.0022170e-002 8.8032110e-002 5.5589350e-002 +v -1.1213830e-002 9.2366370e-002 5.6105260e-002 +v 3.9108440e-002 1.0829072e-001 1.3142330e-002 +v 2.8675700e-002 1.1959600e-001 2.4545910e-002 +v -6.8940210e-002 1.5652777e-001 -1.9716000e-003 +v -6.2615110e-002 9.1126880e-002 4.5090730e-002 +v 3.0444560e-002 1.1886441e-001 2.0821750e-002 +v -1.5241090e-002 9.1821720e-002 5.5817230e-002 +v -5.6221700e-003 9.3235010e-002 5.5893630e-002 +v 4.7989900e-003 9.1654840e-002 5.4715170e-002 +v -6.8282400e-002 9.2376840e-002 4.2388730e-002 +v -5.5623730e-002 9.2187420e-002 4.5054970e-002 +v -5.1901030e-002 9.5457620e-002 4.3937650e-002 +v -4.8809030e-002 9.1083890e-002 4.4456690e-002 +v -4.5411560e-002 9.1002130e-002 4.3252770e-002 +v -4.4514550e-002 9.4860420e-002 4.2972490e-002 +v -3.9430320e-002 8.9597620e-002 4.3177890e-002 +v -3.5642240e-002 9.2617410e-002 4.4238490e-002 +v -1.2246000e-004 9.3201160e-002 5.5398380e-002 +v 9.5104600e-003 9.5483870e-002 5.0910600e-002 +v 2.1441660e-002 9.1354960e-002 4.8043360e-002 +v -8.9830300e-003 1.6926449e-001 -2.2683480e-002 +v -7.3019050e-002 1.5602104e-001 2.2419340e-002 +v -6.4760430e-002 1.5311588e-001 -2.0371200e-003 +v -6.9368510e-002 9.5242790e-002 4.2129000e-002 +v -6.0117140e-002 9.5552910e-002 4.4183820e-002 +v -2.9241690e-002 9.4290440e-002 4.4821190e-002 +v -2.6561430e-002 9.3289510e-002 4.4975420e-002 +v -1.4394030e-002 9.4587640e-002 5.3993500e-002 +v -8.8691600e-003 9.5400260e-002 5.4445980e-002 +v -1.2188700e-003 9.6201750e-002 5.3815910e-002 +v 4.0479000e-003 9.5817360e-002 5.2936770e-002 +v -4.6019400e-003 1.2428544e-001 3.3471960e-002 +v -7.8436460e-002 1.3928013e-001 4.8329360e-002 +v 1.0774610e-002 1.3079162e-001 1.4341740e-002 +v -5.6623730e-002 9.6322170e-002 4.3667910e-002 +v -3.6298870e-002 9.5695620e-002 4.3580310e-002 +v -2.4379930e-002 9.5866450e-002 4.4434530e-002 +v 1.0915500e-002 1.2633629e-001 2.9857020e-002 +v -5.8622700e-003 9.7350210e-002 5.2743650e-002 +v 1.6973450e-002 9.7106620e-002 4.7440920e-002 +v -6.7231980e-002 9.9173950e-002 4.1593880e-002 +v -5.4994210e-002 9.9640820e-002 4.2955230e-002 +v -4.8617990e-002 9.6452700e-002 4.4183060e-002 +v -5.5369000e-002 1.5442476e-001 1.6160650e-002 +v -9.4243550e-002 1.2207432e-001 2.3568470e-002 +v 1.3242990e-002 9.6738240e-002 4.8750160e-002 +v 2.0639290e-002 9.6602480e-002 4.6971000e-002 +v 7.3429700e-003 1.2098188e-001 3.5973430e-002 +v -1.3493870e-002 1.2882438e-001 5.9690700e-003 +v -2.0110640e-002 1.2504545e-001 2.3588310e-002 +v -6.9438450e-002 1.6479930e-001 -1.7218700e-002 +v -6.4028050e-002 9.7838670e-002 4.2565330e-002 +v -5.1996350e-002 9.9707850e-002 4.2716590e-002 +v -4.3990880e-002 9.9425460e-002 4.2383430e-002 +v -3.9738250e-002 1.0215357e-001 4.0574410e-002 +v -3.5931490e-002 9.9809950e-002 4.2335800e-002 +v -3.0867600e-002 9.6914680e-002 4.4651400e-002 +v -2.8342070e-002 9.7782680e-002 4.3761280e-002 +v -2.5622580e-002 9.8713420e-002 4.4210890e-002 +v -8.5236620e-002 1.1077356e-001 2.4537670e-002 +v 7.1936000e-003 9.8859470e-002 4.8419510e-002 +v 9.6509200e-003 1.0108782e-001 4.7373080e-002 +v 1.3487100e-002 1.0076420e-001 4.7454290e-002 +v 7.7389800e-003 1.3147500e-001 1.1682970e-002 +v 8.0905000e-004 1.1633319e-001 4.0167560e-002 +v -7.2652570e-002 1.6567918e-001 -1.8212480e-002 +v -5.6009400e-003 1.3076674e-001 1.0516060e-002 +v -2.6303720e-002 1.2518875e-001 1.7392980e-002 +v -4.7590430e-002 1.0081180e-001 4.2349150e-002 +v -4.1460830e-002 9.8544800e-002 4.1778620e-002 +v -3.3582070e-002 1.0383908e-001 4.0737990e-002 +v -2.2870240e-002 1.0284737e-001 4.3544750e-002 +v -2.2361970e-002 9.8207610e-002 4.4765940e-002 +v -1.8870510e-002 9.8973200e-002 4.4489280e-002 +v -7.1433690e-002 7.7573520e-002 3.8060760e-002 +v -7.3001150e-002 1.1826712e-001 5.3034590e-002 +v -6.8466430e-002 1.3498146e-001 -8.3359800e-003 +v -7.4683810e-002 1.0786100e-001 -9.0477100e-003 +v -6.4958960e-002 1.5852021e-001 -1.2595320e-002 +v -7.8931700e-002 1.5093057e-001 3.5151900e-002 +v -7.4113550e-002 9.9442520e-002 3.8337710e-002 +v -7.0456930e-002 1.0098777e-001 3.9794060e-002 +v -5.9058760e-002 1.0041260e-001 4.2725130e-002 +v -4.9187330e-002 1.0452012e-001 4.0301390e-002 +v -2.9151180e-002 1.0197369e-001 4.2633060e-002 +v -1.1599720e-002 1.0107813e-001 4.4191660e-002 +v 5.1450400e-003 1.0163906e-001 4.5423010e-002 +v -5.1495700e-002 1.0496738e-001 4.0347210e-002 +v -2.0218210e-002 1.0214391e-001 4.3701160e-002 +v 4.2515900e-003 1.0523743e-001 4.2563550e-002 +v 1.6832800e-002 1.0337487e-001 4.5287270e-002 +v -2.5661080e-002 1.2562669e-001 4.5537500e-003 +v -7.2141950e-002 1.0536685e-001 3.7523210e-002 +v -6.4984570e-002 1.0371550e-001 4.0647810e-002 +v -6.0652480e-002 1.0467197e-001 4.0906390e-002 +v -5.5308980e-002 1.0365394e-001 4.1516690e-002 +v -4.4243240e-002 1.0431726e-001 4.1339990e-002 +v -1.5513340e-002 1.0436131e-001 4.2919420e-002 +v -7.6323200e-003 1.0304531e-001 4.3710640e-002 +v -7.8046900e-003 1.0516619e-001 4.3825460e-002 +v 9.7163200e-003 1.0523506e-001 4.3603830e-002 +v 3.0300390e-002 1.1553645e-001 2.8685010e-002 +v -4.7496910e-002 1.0635662e-001 4.0165640e-002 +v -3.8978950e-002 1.0683037e-001 3.8247660e-002 +v -2.5869310e-002 1.0426705e-001 4.2207540e-002 +v -1.8057930e-002 1.0503919e-001 4.2802830e-002 +v -1.5180030e-002 1.0807750e-001 4.2350430e-002 +v -3.8981500e-003 1.0566175e-001 4.4047190e-002 +v 2.6820000e-005 1.0446731e-001 4.3775910e-002 +v 1.1978350e-002 1.0403629e-001 4.5396310e-002 +v 1.5004970e-002 1.0726898e-001 4.1811990e-002 +v 2.6488060e-002 1.2230287e-001 2.0398110e-002 +v -3.6225630e-002 1.0634244e-001 3.8644860e-002 +v -2.1126780e-002 1.0932290e-001 4.0715320e-002 +v -1.2819810e-002 1.0457100e-001 4.3465690e-002 +v 5.2847900e-003 1.0943666e-001 4.1674980e-002 +v 8.9403700e-003 1.0710645e-001 4.1243400e-002 +v -5.1839670e-002 1.6062039e-001 7.1421300e-003 +v -5.4201370e-002 1.1451730e-001 3.4843990e-002 +v 1.3226250e-002 1.2958070e-001 1.9689610e-002 +v -6.9382410e-002 1.0865787e-001 3.7507800e-002 +v -6.7691040e-002 1.0734145e-001 3.8018440e-002 +v -6.3782400e-002 1.1037270e-001 3.7579790e-002 +v -5.0749390e-002 1.0928682e-001 3.8297580e-002 +v -9.3936200e-003 1.0742813e-001 4.3454570e-002 +v 1.1760100e-003 1.0932531e-001 4.2662800e-002 +v 9.8020300e-003 1.1003994e-001 3.9945400e-002 +v 2.0131290e-002 1.0732778e-001 4.0323840e-002 +v -2.7872800e-003 1.0577531e-001 -2.2459030e-002 +v -5.4996890e-002 1.0774199e-001 3.9424590e-002 +v -4.5966740e-002 1.0905146e-001 3.8754110e-002 +v -4.2324540e-002 1.0737278e-001 3.9456440e-002 +v -3.2161240e-002 1.0896504e-001 3.8102720e-002 +v -3.0770180e-002 1.1597313e-001 3.2858800e-002 +v -1.1608610e-002 1.0983707e-001 4.2475330e-002 +v -2.9428320e-002 9.3166620e-002 -2.4931860e-002 +v -8.0043570e-002 9.2080160e-002 -9.4198200e-003 +v -4.9797430e-002 1.1342104e-001 3.5117920e-002 +v -4.3723850e-002 1.6191369e-001 5.7713400e-003 +v -5.7981740e-002 1.0943152e-001 3.7997640e-002 +v -4.1491180e-002 1.1224766e-001 3.5873450e-002 +v -2.4929830e-002 1.1592775e-001 3.4094730e-002 +v -2.0881690e-002 1.1409528e-001 3.7872990e-002 +v -7.5519700e-003 1.1183813e-001 4.2039690e-002 +v 3.7667200e-003 1.1240547e-001 4.1494710e-002 +v -6.2829620e-002 1.5189480e-001 -9.2373400e-003 +v -5.9195950e-002 1.1320797e-001 3.6234680e-002 +v -5.1079080e-002 9.3892810e-002 -2.1761690e-002 +v -7.3945370e-002 8.4374880e-002 -1.5154490e-002 +v -7.2146240e-002 1.3486431e-001 -7.7592200e-003 +v -1.9408870e-002 1.7041104e-001 -2.0994830e-002 +v -5.5530450e-002 1.4905531e-001 -1.9602100e-003 +v 1.6688460e-002 3.6976600e-002 4.3000600e-002 +v -5.2277330e-002 1.1775075e-001 3.3769460e-002 +v -6.9201380e-002 9.3039200e-002 -1.6486120e-002 +v 2.6579210e-002 1.1702438e-001 3.0867940e-002 +v -2.3574310e-002 3.7036910e-002 5.4144750e-002 +v -7.3775100e-003 3.8988430e-002 4.8929450e-002 +v 1.3234660e-002 3.8453060e-002 4.4501470e-002 +v 1.9487350e-002 4.0809290e-002 4.2641060e-002 +v -6.3953930e-002 1.4694729e-001 3.8484200e-002 +v -4.9579470e-002 3.6096540e-002 4.5955360e-002 +v -4.3323650e-002 3.6286400e-002 4.4042360e-002 +v -2.9047200e-002 1.2556338e-001 7.7617700e-003 +v -1.7343100e-003 3.9476800e-002 4.7262900e-002 +v -3.1358130e-002 1.5362199e-001 -4.6738900e-003 +v 2.5822000e-003 1.0747582e-001 -2.0606030e-002 +v -5.6802300e-002 1.4514674e-001 3.1740300e-002 +v -5.6464330e-002 3.7683110e-002 4.6819640e-002 +v -5.0964750e-002 3.8312290e-002 4.6286140e-002 +v -5.0980410e-002 1.3486613e-001 2.7585000e-002 +v -2.5647410e-002 3.8860730e-002 5.4161390e-002 +v -2.2542110e-002 4.0615780e-002 5.3986030e-002 +v -1.7618010e-002 3.8911170e-002 5.2403440e-002 +v -1.9711750e-002 1.6829145e-001 -1.3020960e-002 +v 2.3780070e-002 9.5222940e-002 4.6347330e-002 +v 1.4744290e-002 4.2716950e-002 4.4510310e-002 +v 2.1691360e-002 4.0161530e-002 4.0846450e-002 +v -6.4067240e-002 9.0172190e-002 -1.8855520e-002 +v 2.0319150e-002 1.0041961e-001 4.5760520e-002 +v -3.6425000e-002 9.3630690e-002 -2.3534630e-002 +v -1.4981170e-002 4.2571420e-002 5.1404530e-002 +v -5.7335340e-002 1.2340101e-001 4.0231470e-002 +v -5.4172560e-002 1.2337919e-001 3.7576440e-002 +v 2.2625210e-002 4.3621680e-002 4.0904580e-002 +v 2.8810520e-002 4.3352290e-002 3.2157720e-002 +v -4.2764160e-002 1.5727487e-001 5.2016200e-003 +v 9.2231900e-003 4.4125090e-002 4.5057440e-002 +v 1.5048210e-002 4.5755840e-002 4.3793870e-002 +v -6.3757290e-002 1.0251144e-001 -1.7484400e-002 +v -3.4070430e-002 1.6148975e-001 -1.3786960e-002 +v -8.2191500e-002 7.5610200e-002 1.6542620e-002 +v -6.6299420e-002 1.2337119e-001 5.0615920e-002 +v -1.5510100e-002 4.5283110e-002 5.0653040e-002 +v 1.8928020e-002 4.4249610e-002 4.3009830e-002 +v 2.5821800e-002 4.6326610e-002 3.8277230e-002 +v 2.7268700e-002 4.4547790e-002 3.6152520e-002 +v -4.5301340e-002 1.5695057e-001 7.2036900e-003 +v 2.3855760e-002 1.0616625e-001 3.9378080e-002 +v 2.1632670e-002 4.8127270e-002 4.0694430e-002 +v 4.3785360e-002 4.8803700e-002 3.1343420e-002 +v 4.8074790e-002 4.8969960e-002 2.8165490e-002 +v 5.2663090e-002 4.7673620e-002 2.1201270e-002 +v -5.2722450e-002 4.4722850e-002 4.4143250e-002 +v -3.0071610e-002 1.7258324e-001 -6.3597700e-003 +v -3.4508050e-002 1.5447469e-001 1.6504600e-003 +v 1.0629710e-002 4.6711810e-002 4.6472020e-002 +v 1.6743440e-002 4.8439000e-002 4.3678630e-002 +v 2.8827050e-002 9.2133370e-002 4.3920090e-002 +v -5.9937100e-002 1.2726188e-001 4.0771270e-002 +v -3.6752090e-002 1.5802075e-001 4.1862000e-003 +v -3.7885390e-002 1.6199719e-001 2.4686000e-004 +v -2.2047790e-002 1.8348586e-001 -1.2094990e-002 +v -2.4364620e-002 1.8096836e-001 -9.8312000e-003 +v -4.4882280e-002 1.5052959e-001 7.6451700e-003 +v 2.6996760e-002 5.1317780e-002 3.8752040e-002 +v 4.7735750e-002 5.2751040e-002 3.0797290e-002 +v 5.1703790e-002 4.8857380e-002 2.4147970e-002 +v -6.7504360e-002 1.1424088e-001 4.8036050e-002 +v -1.6257520e-002 1.6031250e-001 -9.6926000e-003 +v -6.3926300e-002 1.6792441e-001 -4.0730420e-002 +v -4.1665290e-002 1.4996141e-001 4.5405000e-003 +v -3.5203230e-002 1.6493551e-001 -2.6810000e-003 +v 4.1318770e-002 9.9496740e-002 2.4275750e-002 +v 1.4055220e-002 5.2523910e-002 4.8593880e-002 +v 1.9421220e-002 5.1321300e-002 4.4798910e-002 +v 2.3677990e-002 5.1474390e-002 4.1053270e-002 +v 3.4258130e-002 5.1930810e-002 3.2757880e-002 +v 5.5957340e-002 5.3147410e-002 2.3197720e-002 +v -3.9937960e-002 1.4922850e-001 1.6017200e-003 +v -4.6988800e-002 1.2600802e-001 2.6985500e-002 +v -2.7708370e-002 9.0081290e-002 -3.1911460e-002 +v 1.9204630e-002 5.5166510e-002 4.7722150e-002 +v 2.1886000e-002 5.3927560e-002 4.5102460e-002 +v 3.1286270e-002 5.2863840e-002 3.6913620e-002 +v 4.6661160e-002 5.4719230e-002 3.1976810e-002 +v 5.1823730e-002 5.3276700e-002 2.7927010e-002 +v -2.9264880e-002 1.6140418e-001 -2.1039500e-003 +v -6.8700770e-002 1.4463537e-001 4.3041630e-002 +v -5.6070060e-002 1.5000706e-001 2.9867640e-002 +v 4.4717850e-002 9.4802660e-002 1.2024710e-002 +v -4.1804090e-002 1.5582081e-001 6.4548200e-003 +v -6.8369340e-002 1.2289287e-001 5.2437860e-002 +v -6.4114810e-002 9.5509880e-002 -1.8114610e-002 +v -1.8383130e-002 1.8543664e-001 -1.7136370e-002 +v 1.1745400e-002 5.6678340e-002 5.1914060e-002 +v -5.9375360e-002 1.1998238e-001 4.0548240e-002 +v 5.9092080e-002 5.7956980e-002 2.0270120e-002 +v 4.3547740e-002 9.7389400e-002 1.7314650e-002 +v -2.6291780e-002 1.5963381e-001 -5.1845000e-004 +v 1.4904780e-002 5.6350380e-002 4.9522780e-002 +v 2.4286200e-002 5.4958580e-002 4.3086850e-002 +v 2.8952610e-002 5.6125250e-002 4.0388970e-002 +v -4.9507770e-002 1.2949500e-001 3.0259270e-002 +v 4.0824790e-002 9.5170220e-002 2.8657920e-002 +v 1.7774800e-002 5.8243780e-002 4.8864720e-002 +v 3.3573840e-002 5.8515260e-002 3.8310990e-002 +v 3.6385040e-002 5.6996480e-002 3.3601460e-002 +v -6.4205010e-002 1.2243894e-001 4.8008340e-002 +v -6.5424500e-002 1.4011279e-001 4.1308960e-002 +v 5.0801340e-002 5.7308080e-002 3.0001390e-002 +v 5.6671750e-002 5.6970820e-002 2.4291920e-002 +v -4.9349930e-002 1.4913519e-001 1.1274060e-002 +v -6.9760570e-002 1.3442855e-001 4.8265220e-002 +v 1.9537060e-002 6.0003780e-002 4.8576140e-002 +v 2.7013910e-002 5.9952790e-002 4.3454420e-002 +v 5.7679430e-002 6.1392970e-002 2.4201790e-002 +v -5.6916540e-002 1.2623512e-001 3.9426610e-002 +v 2.3469280e-002 1.1656262e-001 3.3537270e-002 +v -5.8298640e-002 1.3885500e-001 3.2937460e-002 +v 6.4598400e-003 6.0297430e-002 5.4780030e-002 +v 1.0406020e-002 5.9162400e-002 5.2484370e-002 +v 2.3183950e-002 5.8654360e-002 4.5871060e-002 +v 3.3040360e-002 6.1773840e-002 3.9781440e-002 +v -6.4348220e-002 1.2628088e-001 4.6650200e-002 +v -5.7031440e-002 1.1562007e-001 3.6494880e-002 +v 5.4451560e-002 5.8342890e-002 2.7653010e-002 +v -3.0134400e-002 1.7011322e-001 -7.3591600e-003 +v -3.7077100e-002 1.5986369e-001 1.6096500e-003 +v -5.6032760e-002 1.3731083e-001 3.1970590e-002 +v -6.7676470e-002 1.4150325e-001 4.3868140e-002 +v 9.9911700e-003 6.2735270e-002 5.4009240e-002 +v 1.4521510e-002 6.1382890e-002 5.0500900e-002 +v 3.0051740e-002 6.2169610e-002 4.1545810e-002 +v 3.7519170e-002 6.1062710e-002 3.4366020e-002 +v 5.3944010e-002 6.1391550e-002 2.8268530e-002 +v 5.9119900e-002 6.3128810e-002 2.1561830e-002 +v -2.4366390e-002 1.7693266e-001 -1.1719630e-002 +v -1.3253420e-002 1.6627152e-001 -1.4120370e-002 +v 3.9218740e-002 1.0669250e-001 2.0450190e-002 +v -1.7968980e-002 1.8078031e-001 -1.8103430e-002 +v 2.1902390e-002 6.0875970e-002 4.7282360e-002 +v 3.5341750e-002 6.1630030e-002 3.7606020e-002 +v -6.2145620e-002 1.3599775e-001 3.6700970e-002 +v 5.6820620e-002 6.3691150e-002 2.5286090e-002 +v -3.2800040e-002 1.5948699e-001 2.1962800e-003 +v 1.1212140e-002 6.6584120e-002 5.3982180e-002 +v 1.2919590e-002 6.4203580e-002 5.2441150e-002 +v 2.0126950e-002 6.3851330e-002 4.7919660e-002 +v 3.5971760e-002 6.6669610e-002 3.7781400e-002 +v 3.9906940e-002 6.4361260e-002 3.1686660e-002 +v -6.6702350e-002 1.3210600e-001 4.5480940e-002 +v -4.1601430e-002 1.5978000e-001 3.5374700e-003 +v 3.3044580e-002 1.0766252e-001 3.1916150e-002 +v 2.4672100e-002 6.3694500e-002 4.5204640e-002 +v 2.6108660e-002 6.8007640e-002 4.3902690e-002 +v 3.3363940e-002 6.7054760e-002 3.9729480e-002 +v 4.2915790e-002 6.6707700e-002 2.6994720e-002 +v 5.4714960e-002 6.4697160e-002 2.6979680e-002 +v -1.6530940e-002 1.6325000e-001 -9.2475200e-003 +v -1.7891600e-002 1.6113800e-001 -6.7072700e-003 +v 4.1118120e-002 9.7491260e-002 -3.9756700e-003 +v 2.3386770e-002 7.0075990e-002 4.7012620e-002 +v 3.8102900e-002 6.5678440e-002 3.5132520e-002 +v 1.0145240e-002 1.2221678e-001 3.4718950e-002 +v 5.8392410e-002 6.6741240e-002 2.1979460e-002 +v 3.8302050e-002 8.4549140e-002 -1.4478830e-002 +v 3.4126440e-002 9.7053980e-002 3.7590390e-002 +v -3.1355740e-002 1.5809888e-001 1.9128800e-003 +v -5.8259510e-002 1.4099493e-001 3.2440640e-002 +v -6.6817230e-002 1.1951525e-001 5.1490220e-002 +v -6.8090040e-002 1.1647050e-001 5.1151230e-002 +v 1.6568300e-002 6.6269890e-002 5.1009890e-002 +v 2.9362870e-002 6.6509780e-002 4.2289380e-002 +v 3.7027180e-002 9.3949630e-002 -1.1674040e-002 +v 5.6412730e-002 6.7659930e-002 2.3969320e-002 +v -6.1295740e-002 1.4519988e-001 3.7137830e-002 +v 8.3873000e-003 1.1336223e-001 3.9792610e-002 +v 1.1807030e-002 7.0920980e-002 5.4240490e-002 +v 2.9741730e-002 7.0647100e-002 4.1653890e-002 +v 3.6294410e-002 7.1220700e-002 3.7114610e-002 +v 3.9899680e-002 7.0294820e-002 3.2720020e-002 +v -6.2763130e-002 1.3778012e-001 3.6678590e-002 +v -1.5815440e-002 1.7504938e-001 -1.8654160e-002 +v -9.2268990e-002 1.1475156e-001 1.7017380e-002 +v -9.4964000e-004 1.0141111e-001 4.4290070e-002 +v -6.3712920e-002 1.1274250e-001 3.8006760e-002 +v -6.1096020e-002 1.1701650e-001 3.9654020e-002 +v 2.0991870e-002 6.9335450e-002 4.9003540e-002 +v 2.5658530e-002 7.0550460e-002 4.4539930e-002 +v 3.2978560e-002 7.3500690e-002 4.0486510e-002 +v 4.2156130e-002 6.9717580e-002 2.8318230e-002 +v -5.5516860e-002 1.2956070e-001 3.6598450e-002 +v -4.0802290e-002 1.6436059e-001 3.7448800e-003 +v -6.2546500e-003 1.0121650e-001 4.4322030e-002 +v -1.0986820e-002 1.6621199e-001 -1.6047550e-002 +v -3.0351420e-002 1.6448158e-001 -5.3291400e-003 +v 2.6110920e-002 1.0088990e-001 4.1733260e-002 +v -6.5599940e-002 1.1329504e-001 4.2318710e-002 +v 2.8814660e-002 9.6712680e-002 4.2257700e-002 +v 1.5263280e-002 7.1571940e-002 5.2717390e-002 +v 2.8982400e-002 7.4088480e-002 4.3447240e-002 +v 4.4872540e-002 7.5516710e-002 2.3155250e-002 +v -7.8225230e-002 1.4962481e-001 -2.5019400e-003 +v -4.6094940e-002 1.5296850e-001 9.0029700e-003 +v -5.2369030e-002 1.4682913e-001 1.8934650e-002 +v -2.1592100e-002 1.5763440e-001 -6.8623600e-003 +v 1.7176770e-002 7.3066230e-002 5.1826600e-002 +v 2.2687500e-002 7.5149180e-002 4.9312500e-002 +v 3.5472040e-002 7.3076670e-002 3.8482270e-002 +v -8.9480840e-002 1.3839976e-001 2.5061450e-002 +v -5.3216730e-002 1.3221978e-001 3.2978380e-002 +v -3.7776780e-002 1.5551947e-001 4.3700800e-003 +v -9.0549380e-002 1.3511875e-001 2.1680550e-002 +v -6.3366580e-002 1.3037076e-001 4.1669940e-002 +v 1.4074270e-002 7.6651720e-002 5.4221350e-002 +v 1.8109790e-002 7.5806590e-002 5.2488260e-002 +v 4.2209940e-002 7.8861480e-002 2.9187200e-002 +v -5.2115930e-002 1.4179906e-001 2.0510310e-002 +v 2.9063090e-002 1.1149602e-001 3.3805790e-002 +v -5.4731460e-002 1.4267229e-001 2.8980480e-002 +v 2.5903640e-002 7.5536040e-002 4.6416650e-002 +v 3.1298760e-002 7.5907440e-002 4.2699060e-002 +v 3.8446170e-002 7.5649430e-002 3.5050640e-002 +v 4.6351670e-002 7.4079520e-002 1.8354320e-002 +v -4.7656560e-002 1.3077525e-001 2.5523570e-002 +v -1.1447430e-002 1.7131059e-001 -1.9602980e-002 +v -3.6647240e-002 1.6640131e-001 -2.8167000e-004 +v -4.6653530e-002 1.5917824e-001 7.8019000e-003 +v -4.5569890e-002 1.4663612e-001 5.6514200e-003 +v 4.1438880e-002 9.2365100e-002 -7.4587000e-003 +v -6.4287420e-002 1.3463625e-001 3.9945640e-002 +v -6.1128890e-002 1.3178328e-001 3.8915910e-002 +v -4.7843540e-002 1.2215063e-001 2.8833160e-002 +v -4.9536830e-002 1.2491344e-001 3.1778440e-002 +v -7.1135380e-002 1.3817656e-001 4.7853960e-002 +v 1.0113870e-002 7.6468110e-002 5.5256790e-002 +v 1.7897450e-002 7.9516550e-002 5.2759530e-002 +v 2.1740850e-002 8.0250650e-002 5.0425390e-002 +v 2.5271590e-002 7.8724920e-002 4.8026570e-002 +v 3.0885040e-002 7.8999480e-002 4.3388770e-002 +v -6.2441930e-002 1.4084781e-001 3.6965840e-002 +v -6.2165060e-002 1.5666850e-001 -1.7837760e-002 +v 2.0657260e-002 1.0416830e-001 4.3004680e-002 +v -6.3602800e-002 1.1571453e-001 4.2572290e-002 +v 1.4424020e-002 8.0085500e-002 5.3755600e-002 +v 2.8779340e-002 8.2553250e-002 4.4527350e-002 +v 4.4450130e-002 8.1846900e-002 2.4552920e-002 +v 4.5541990e-002 8.3338380e-002 1.9700850e-002 +v -4.9665810e-002 1.2063801e-001 3.2163270e-002 +v -2.9177290e-002 1.7619959e-001 -5.6241100e-003 +v -5.8203130e-002 1.3270975e-001 3.6918680e-002 +v 3.8997050e-002 9.7088220e-002 -7.7799300e-003 +v -5.4725800e-002 1.2071262e-001 3.7451450e-002 +v 1.3189120e-002 8.4211180e-002 5.3065830e-002 +v -1.9926300e-002 1.6489742e-001 -9.9900200e-003 +v 2.0153130e-002 1.1849719e-001 3.4271250e-002 +v -5.5859940e-002 1.1774313e-001 3.7253480e-002 +v 1.8045260e-002 8.3623160e-002 5.1285840e-002 +v -6.3757130e-002 1.5912175e-001 -5.0155730e-002 +v -1.8527620e-002 1.7653197e-001 -1.7043540e-002 +v 2.8734400e-002 1.0360053e-001 3.8035240e-002 +v 4.1414010e-002 1.0284216e-001 1.6578920e-002 +v 2.4411730e-002 9.8016880e-002 4.4687400e-002 +v 2.0925180e-002 8.6311430e-002 4.9433120e-002 +v 3.0445010e-002 8.4959560e-002 4.3011090e-002 +v 3.3030090e-002 8.3781640e-002 4.1636930e-002 +v 3.6975090e-002 7.9876480e-002 3.7198390e-002 +v -7.7721460e-002 1.1355888e-001 4.8155990e-002 +v 2.9250000e-002 1.0651935e-001 3.6590330e-002 +v -5.3078180e-002 1.3754688e-001 2.8266470e-002 +v -6.2990590e-002 1.1999459e-001 4.5235530e-002 +v -6.5398320e-002 1.1751956e-001 4.8735570e-002 +v 3.3373910e-002 1.1227890e-001 2.7788130e-002 +v 3.8413590e-002 8.7489930e-002 3.5185850e-002 +v -6.1945930e-002 1.6479234e-001 -5.6647670e-002 +v -2.2876480e-002 1.7392813e-001 -1.3431140e-002 +v 4.3766230e-002 8.8390020e-002 -3.5708800e-003 +v 3.9291530e-002 1.0125969e-001 2.7550520e-002 +v 1.0936230e-002 8.6027290e-002 5.4732670e-002 +v 2.4108720e-002 8.4492600e-002 4.8292310e-002 +v 3.6758390e-002 9.9195470e-002 3.2837670e-002 +v -5.1941640e-002 1.2565987e-001 3.4587860e-002 +v -3.1582110e-002 1.6641850e-001 -5.7320000e-003 +v 7.6405900e-003 8.6427230e-002 5.6117850e-002 +v 1.6771020e-002 8.8644690e-002 5.0522960e-002 +v 3.4404610e-002 8.6932850e-002 4.0574270e-002 +v 3.6143820e-002 8.4439200e-002 3.7936930e-002 +v 4.1258830e-002 1.0361081e-001 2.6760600e-003 +v 2.4766140e-002 1.1081111e-001 3.6728360e-002 +v -2.2601590e-002 1.6250449e-001 -6.0717000e-003 +v -1.2893670e-002 1.7879041e-001 -2.2624750e-002 +v -2.4939150e-002 1.7031135e-001 -1.1329700e-002 +v -4.8468630e-002 1.4559606e-001 8.3661500e-003 +v 1.2534490e-002 8.9593930e-002 5.3394630e-002 +v 2.5872860e-002 8.8482290e-002 4.6655260e-002 +v 3.2756470e-002 8.8969130e-002 4.2215450e-002 +v -2.3343620e-002 1.6103450e-001 -3.1862400e-003 +v -9.2594970e-002 1.1943826e-001 2.6802950e-002 +v -7.4314840e-002 1.3761738e-001 -6.6698800e-003 +v -9.2499230e-002 1.2131500e-001 2.9256200e-002 +v -7.7378260e-002 1.5764266e-001 -1.4133650e-002 +v -9.2907340e-002 1.2307021e-001 3.6523230e-002 +v 2.8423340e-002 8.8011080e-002 4.4234200e-002 +v 3.5251680e-002 9.0836820e-002 3.9183920e-002 +v 1.5760560e-002 9.3203560e-002 4.9939310e-002 +v 3.8785530e-002 9.4954300e-002 3.2520220e-002 +v -6.1511220e-002 1.2373565e-001 4.3062680e-002 +v -6.8145120e-002 1.2748676e-001 5.0148970e-002 +v -2.0616710e-002 1.8237588e-001 -1.4299100e-002 +v 1.5137190e-002 1.1571495e-001 3.7031980e-002 +v -5.0718270e-002 1.5276300e-001 1.1816680e-002 +v 3.0168690e-002 1.0048686e-001 3.9404710e-002 +v -8.7426500e-002 9.5469530e-002 4.0312400e-003 +v -6.0010390e-002 1.4284463e-001 3.5449690e-002 +v -5.8603310e-002 1.4637237e-001 3.3808800e-002 +v 3.2411650e-002 9.3736150e-002 4.0890240e-002 +v -7.5917780e-002 1.4997690e-001 -1.6842050e-002 +v 1.8596570e-002 3.5293940e-002 -8.6782200e-003 +v 1.7209800e-002 3.5259400e-002 -1.4685160e-002 +v 4.4326540e-002 9.0818120e-002 2.2097520e-002 +v 3.8335910e-002 3.8830830e-002 3.0938100e-003 +v 2.2192920e-002 3.6775320e-002 -2.0919300e-003 +v 1.9636020e-002 3.8234010e-002 -1.2507670e-002 +v 2.3682120e-002 3.9762540e-002 3.7148760e-002 +v 4.6693280e-002 4.2465320e-002 6.5649500e-003 +v 2.1621110e-002 3.7657240e-002 -4.7021600e-003 +v 1.6638610e-002 3.8196090e-002 -1.9884930e-002 +v -9.0253980e-002 1.1366307e-001 3.7720210e-002 +v -9.0593870e-002 1.1373094e-001 1.0276770e-002 +v -6.2541690e-002 1.7679461e-001 -5.7821820e-002 +v -1.1091940e-002 1.7992082e-001 -2.5996430e-002 +v -6.2263130e-002 1.5219935e-001 -2.2578880e-002 +v -4.2276760e-002 9.4982570e-002 -2.2562420e-002 +v 4.3293410e-002 4.1864140e-002 2.0634400e-003 +v 4.3779590e-002 4.4530720e-002 -1.2622500e-003 +v 2.1696990e-002 4.0427270e-002 -9.4629500e-003 +v -1.1183700e-002 1.6450000e-001 -1.6151690e-002 +v -6.2372570e-002 1.5313041e-001 -2.8997120e-002 +v -9.2489300e-003 1.7725850e-001 -2.8270200e-002 +v 4.1477400e-002 8.5509410e-002 -9.1575000e-003 +v -8.1268710e-002 1.0879438e-001 2.9440660e-002 +v 4.9575680e-002 4.3815900e-002 1.4582960e-002 +v 5.2987960e-002 4.7747690e-002 5.0420000e-003 +v 2.1977540e-002 4.2855330e-002 -1.4536230e-002 +v 1.8505700e-002 3.8294100e-002 -1.7136500e-002 +v -3.5100500e-002 1.5203437e-001 -1.3279000e-004 +v 4.8749130e-002 4.5265000e-002 2.3023500e-003 +v 3.1912900e-002 9.9870060e-002 -1.4620980e-002 +v -1.4222520e-002 1.6167426e-001 -1.3349060e-002 +v -4.8663640e-002 1.3638523e-001 6.8063900e-003 +v -9.5837200e-003 1.7426102e-001 -2.8390760e-002 +v 5.2801850e-002 4.6539940e-002 1.0427720e-002 +v 5.1433800e-002 4.8485200e-002 1.0401000e-003 +v 2.3911240e-002 9.8021670e-002 -2.0807290e-002 +v 2.4567060e-002 4.4130110e-002 -1.0820840e-002 +v 2.0356810e-002 4.3662400e-002 -2.0456280e-002 +v -2.1882420e-002 1.1087418e-001 -1.9695320e-002 +v -5.3831800e-002 1.4981693e-001 2.5066610e-002 +v 5.4114210e-002 4.7773090e-002 1.7484000e-002 +v 5.6730570e-002 5.0515740e-002 1.0627080e-002 +v 4.5941820e-002 4.8138820e-002 -3.8715700e-003 +v -8.3817760e-002 1.1109094e-001 2.8524490e-002 +v 2.9207770e-002 4.7450250e-002 -8.5081800e-003 +v 2.8454920e-002 4.8067390e-002 -1.2847240e-002 +v 2.6637260e-002 4.7607100e-002 -1.6427740e-002 +v 2.2040110e-002 4.4992500e-002 -1.7528500e-002 +v 1.9120080e-002 4.7167750e-002 -2.2114680e-002 +v -1.5782200e-002 1.0072957e-001 -2.3724130e-002 +v -6.2514170e-002 1.7213119e-001 -5.2788100e-002 +v -6.2345600e-002 1.4745498e-001 -7.6600200e-003 +v 4.5598180e-002 8.8151720e-002 1.3124070e-002 +v -4.9422610e-002 1.4283525e-001 8.9728300e-003 +v -8.2761860e-002 1.1162341e-001 4.4221460e-002 +v -5.2166220e-002 1.5013661e-001 1.7448750e-002 +v -6.3616740e-002 1.4801371e-001 -2.0170260e-002 +v -5.1492690e-002 1.3796388e-001 2.3662180e-002 +v -6.1517580e-002 1.7517449e-001 -6.0631700e-002 +v 5.6524870e-002 5.0125660e-002 1.5564490e-002 +v 5.5257900e-002 5.1416260e-002 3.2062600e-003 +v 5.0318130e-002 5.2786370e-002 -3.4166300e-003 +v -6.2681950e-002 1.6744086e-001 -4.5713890e-002 +v 5.6520150e-002 5.1179900e-002 1.9940560e-002 +v 5.6907980e-002 5.1578130e-002 7.2538300e-003 +v 5.2854160e-002 5.1898670e-002 -6.2070000e-004 +v -3.8921140e-002 3.3767390e-002 -2.9042560e-002 +v 2.9740700e-002 5.0324690e-002 -1.3990860e-002 +v -6.8796190e-002 3.5117720e-002 -5.2067400e-003 +v 5.8826020e-002 5.5503780e-002 1.8647920e-002 +v -2.6160570e-002 1.2309988e-001 -4.4735500e-003 +v -5.3341960e-002 1.4401200e-001 2.4261390e-002 +v 5.8177390e-002 5.2821320e-002 1.5182420e-002 +v 5.9798140e-002 5.6840180e-002 1.3342730e-002 +v 5.4549870e-002 5.6044630e-002 -6.6158000e-004 +v 2.6775460e-002 5.1423450e-002 -2.0234060e-002 +v -8.6960400e-003 1.7291588e-001 -2.6708770e-002 +v -7.7039560e-002 7.1967020e-002 2.6405070e-002 +v -6.3069890e-002 1.5897471e-001 -4.2951850e-002 +v 3.5706690e-002 5.6083040e-002 -8.9993300e-003 +v 3.2600380e-002 5.3707520e-002 -1.1006150e-002 +v 2.9739960e-002 5.2538430e-002 -1.6224950e-002 +v 5.9238530e-002 5.6362780e-002 9.4530800e-003 +v 5.7421750e-002 5.6012210e-002 4.0245600e-003 +v 2.9062990e-002 5.5210580e-002 -1.8042060e-002 +v -1.7224410e-002 9.5214090e-002 -3.2085300e-002 +v -8.5911380e-002 1.0968787e-001 7.6582400e-003 +v 6.0594930e-002 6.1677210e-002 1.5591560e-002 +v 5.9531640e-002 6.0504600e-002 5.8397000e-003 +v 5.7306470e-002 5.9944620e-002 1.8886400e-003 +v 3.8829380e-002 5.9839830e-002 -6.4252500e-003 +v 3.0662770e-002 5.7300390e-002 -1.6518370e-002 +v -2.7762070e-002 1.2068537e-001 -9.0152900e-003 +v -8.8194590e-002 1.0314633e-001 1.7509020e-002 +v 6.0778800e-002 6.1646560e-002 1.0463990e-002 +v 3.5915080e-002 5.9916380e-002 -1.1966510e-002 +v 2.4251860e-002 5.6457470e-002 -2.4254800e-002 +v -6.1954390e-002 1.6865320e-001 -5.2621160e-002 +v -9.0557930e-002 1.1275994e-001 1.6141030e-002 +v -8.8469220e-002 1.1124294e-001 1.2679160e-002 +v 5.9558010e-002 6.3099260e-002 5.9471000e-003 +v 3.0940440e-002 6.0518080e-002 -1.8132720e-002 +v -9.3575750e-002 1.2474629e-001 2.6213300e-002 +v -9.3189820e-002 1.2019919e-001 3.7913720e-002 +v -9.2296100e-003 1.7314463e-001 -2.4197660e-002 +v -8.1739460e-002 7.6861340e-002 2.3313610e-002 +v -3.6992750e-002 1.5063932e-001 -2.0372300e-003 +v 6.0093570e-002 6.5693450e-002 1.8533320e-002 +v 5.9837240e-002 6.6423180e-002 8.5139400e-003 +v 4.0706180e-002 6.4475310e-002 -5.5920300e-003 +v 3.4745940e-002 6.3261340e-002 -1.4646740e-002 +v -6.1879660e-002 1.6000450e-001 -2.5806250e-002 +v -7.6537810e-002 1.5344875e-001 -1.2898750e-002 +v 3.8111070e-002 6.4811810e-002 -1.1142000e-002 +v 3.1909340e-002 6.4657050e-002 -1.8473410e-002 +v -8.3159350e-002 1.4674277e-001 3.0757900e-003 +v -8.7055900e-002 1.0562761e-001 9.7651100e-003 +v -7.1448330e-002 1.8105301e-001 -5.5478550e-002 +v -8.5632110e-002 1.2461094e-001 -2.7335800e-003 +v 6.0728970e-002 6.5806600e-002 1.3974830e-002 +v 3.9909650e-002 6.8171740e-002 -9.5698200e-003 +v 3.4981790e-002 6.7740790e-002 -1.5683210e-002 +v -9.1822030e-002 1.2747346e-001 3.6458650e-002 +v -6.2425420e-002 1.6366637e-001 -4.9667290e-002 +v -7.1168950e-002 1.4740156e-001 -2.7590940e-002 +v -5.0364760e-002 1.3715763e-001 1.9526100e-003 +v -5.0492650e-002 1.4159899e-001 1.6291740e-002 +v 5.9886670e-002 6.8513050e-002 1.6171610e-002 +v -6.1406990e-002 1.7268822e-001 -5.8265750e-002 +v 2.4990740e-002 6.5897320e-002 -2.3568270e-002 +v -7.4852750e-002 1.4993112e-001 -2.7752940e-002 +v -6.2225690e-002 6.0265200e-002 2.0449290e-002 +v -6.2001940e-002 3.6435020e-002 4.3918940e-002 +v 5.8374570e-002 7.1186410e-002 1.3072740e-002 +v -3.6125040e-002 1.2286688e-001 -8.2927900e-003 +v 2.9216510e-002 6.7850250e-002 -2.0418570e-002 +v -4.1681700e-002 1.2575112e-001 -7.0193300e-003 +v -7.4226550e-002 1.6437012e-001 -3.8240340e-002 +v -9.7845700e-003 1.6928488e-001 -2.4756660e-002 +v -8.9577950e-002 1.2078310e-001 3.5229100e-003 +v -6.2311930e-002 1.6371109e-001 -4.0623990e-002 +v 4.3514770e-002 9.1519890e-002 -2.6468100e-003 +v -4.8434350e-002 1.3754973e-001 1.3244980e-002 +v -8.9313160e-002 1.3653006e-001 3.0458750e-002 +v -7.4230190e-002 1.5652681e-001 -2.5167090e-002 +v 3.7378600e-002 7.3093410e-002 -1.2635370e-002 +v 2.6321810e-002 7.0240650e-002 -2.3878680e-002 +v -4.8023620e-002 1.4426649e-001 4.2498600e-003 +v -9.2019580e-002 1.1611534e-001 3.5842730e-002 +v -7.1305510e-002 7.3899020e-002 3.5969780e-002 +v -6.2059290e-002 1.5697807e-001 -3.3784580e-002 +v -9.7015300e-003 1.6738863e-001 -1.9360250e-002 +v 4.3342140e-002 7.1676120e-002 -2.2304600e-003 +v 4.1772460e-002 6.9568020e-002 -6.1596000e-003 +v 3.3505410e-002 7.2809860e-002 -1.7034800e-002 +v 2.9665000e-002 7.1506830e-002 -2.1282340e-002 +v -2.9460160e-002 1.5550263e-001 -1.1914700e-003 +v -8.6396440e-002 1.0479356e-001 5.9820600e-003 +v -5.4910700e-002 1.4662313e-001 2.8438970e-002 +v 4.4203810e-002 8.5204260e-002 -2.1170500e-003 +v 4.3264350e-002 7.5810540e-002 -3.8843900e-003 +v 1.3096990e-002 9.1126480e-002 -2.9269770e-002 +v -6.7069210e-002 9.1144610e-002 -1.7425950e-002 +v -9.0821680e-002 1.2276896e-001 6.0998500e-003 +v 4.5620000e-002 7.4684430e-002 2.6073900e-003 +v -9.3039800e-002 1.2026416e-001 1.1216820e-002 +v 4.4635590e-002 9.2794290e-002 1.7832070e-002 +v -1.1243390e-002 1.6457514e-001 -1.8240780e-002 +v 4.5511190e-002 8.6953050e-002 3.8865500e-003 +v 4.6252720e-002 7.7373870e-002 6.9140800e-003 +v 4.0281640e-002 7.2637130e-002 -9.2881000e-003 +v 4.3218200e-002 9.9486740e-002 5.0153300e-003 +v -5.1108270e-002 1.4520219e-001 1.4279480e-002 +v 4.4692980e-002 9.2688550e-002 2.2466700e-003 +v 4.3422540e-002 9.1860370e-002 2.4538450e-002 +v 4.0751360e-002 1.0554729e-001 7.5074100e-003 +v -8.5613030e-002 9.6277110e-002 -6.6514000e-004 +v 4.0721470e-002 7.8475530e-002 -8.2130000e-003 +v 3.5538080e-002 7.6062960e-002 -1.4434750e-002 +v -9.2736510e-002 1.2073095e-001 3.2692730e-002 +v -6.2278520e-002 1.5166598e-001 -1.4672730e-002 +v 4.4960220e-002 8.0942630e-002 6.1119000e-004 +v 3.7814740e-002 7.9698150e-002 -1.3289630e-002 +v 3.3864490e-002 7.8656690e-002 -1.7632490e-002 +v -9.1044280e-002 1.4199862e-001 2.1729630e-002 +v -7.4004450e-002 1.7818523e-001 -5.3916320e-002 +v -6.1768650e-002 1.6067957e-001 -3.4046350e-002 +v -4.9747450e-002 1.4112519e-001 5.2937500e-003 +v 4.1065440e-002 9.0460700e-002 2.9888620e-002 +v -7.2916360e-002 6.5057400e-002 1.8794620e-002 +v -9.0949690e-002 1.3895375e-001 1.7371130e-002 +v 4.2879050e-002 1.0093777e-001 9.4753200e-003 +v -7.2455480e-002 1.7610676e-001 -5.3535420e-002 +v -7.5862940e-002 1.5071299e-001 -9.0209000e-003 +v -8.5269820e-002 1.0267793e-001 1.3935600e-003 +v -7.7025570e-002 1.1396763e-001 -4.6168100e-003 +v 4.6280880e-002 7.8702020e-002 1.4786330e-002 +v 4.2106910e-002 8.1533160e-002 -6.6690900e-003 +v 3.6523880e-002 8.1991750e-002 -1.6229590e-002 +v -3.7420220e-002 4.5428500e-002 -2.4226790e-002 +v -8.5148910e-002 1.3965520e-001 2.4808500e-003 +v -6.3313300e-002 1.6503258e-001 -3.2895120e-002 +v -6.1591410e-002 1.5681572e-001 -2.5945630e-002 +v 4.5918540e-002 8.7036220e-002 8.4236300e-003 +v 4.4631140e-002 8.4178380e-002 8.2665000e-004 +v -4.4842870e-002 1.4629393e-001 1.7114800e-003 +v -6.4124180e-002 1.7953625e-001 -5.8730420e-002 +v -6.7070300e-002 1.8072682e-001 -5.6618620e-002 +v -6.4793760e-002 1.7885275e-001 -5.5883250e-002 +v -6.4371030e-002 1.7296209e-001 -4.9225660e-002 +v -7.0381530e-002 1.8071180e-001 -5.3172590e-002 +v -7.5269270e-002 1.5232949e-001 3.4374060e-002 +v -1.6273090e-002 1.2844514e-001 1.6683610e-002 +v -6.2116150e-002 1.5600787e-001 1.8034420e-002 +v -5.6010790e-002 1.5381662e-001 2.5369280e-002 +v -3.7277920e-002 1.7289068e-001 -8.6627000e-004 +v -7.4158700e-002 1.7987275e-001 -5.0794750e-002 +v -7.9039960e-002 1.5537445e-001 1.5141810e-002 +v -7.2505530e-002 1.5459529e-001 2.9588830e-002 +v -6.7738180e-002 1.7728865e-001 -5.0375960e-002 +v -7.5346900e-003 1.0021302e-001 4.7488700e-002 +v -5.9575620e-002 1.5472401e-001 2.6373250e-002 +v -7.7382710e-002 1.5346600e-001 3.0894990e-002 +v -8.1496670e-002 1.5473104e-001 1.9697340e-002 +v -7.2223320e-002 1.5896734e-001 -5.4242300e-003 +v -1.3708500e-002 1.8491150e-001 -2.5549550e-002 +v -4.3465340e-002 1.2451145e-001 2.2518890e-002 +v -6.9103650e-002 1.5559479e-001 1.6370800e-003 +v -7.3748080e-002 1.5539253e-001 2.3491700e-003 +v -6.8192410e-002 1.7439828e-001 -4.5365870e-002 +v -6.0052850e-002 1.5280350e-001 3.2887630e-002 +v -2.3459490e-002 1.2615386e-001 1.6613770e-002 +v -7.2777220e-002 1.7854465e-001 -4.8208800e-002 +v -7.6595580e-002 1.7753227e-001 -4.7118080e-002 +v 1.3906410e-002 1.2790838e-001 2.5110240e-002 +v -8.6367510e-002 1.0906537e-001 1.1980640e-002 +v -3.1358850e-002 1.2140977e-001 2.5971090e-002 +v -4.9104590e-002 1.3666879e-001 1.9314030e-002 +v -4.2930640e-002 1.2928436e-001 9.2700700e-003 +v -6.5320350e-002 1.5390322e-001 9.1386000e-004 +v -3.7606490e-002 1.2422605e-001 2.4313530e-002 +v 9.5078400e-003 1.3041865e-001 2.0715020e-002 +v -1.7976800e-003 1.3117283e-001 1.6360660e-002 +v 3.6231700e-003 1.3076791e-001 2.1168600e-002 +v -9.2674700e-002 1.1701945e-001 1.1889520e-002 +v -6.5739720e-002 1.5565338e-001 2.6017600e-002 +v -8.6561940e-002 1.4249188e-001 8.4326800e-003 +v -7.0731530e-002 1.5569959e-001 6.9058200e-003 +v -8.0840700e-003 1.3030537e-001 1.6872280e-002 +v -4.4286250e-002 1.2606625e-001 2.0795220e-002 +v -7.0222260e-002 1.5143521e-001 3.6718910e-002 +v -1.5210690e-002 1.8463639e-001 -2.2057240e-002 +v -1.7270750e-002 1.8699602e-001 -1.9977570e-002 +v -8.3560950e-002 1.5255943e-001 7.6806700e-003 +v -8.8130280e-002 9.7540510e-002 5.6788000e-003 +v -8.8399240e-002 1.3899000e-001 1.0640660e-002 +v -6.7780550e-002 1.5614453e-001 1.4276320e-002 +v -6.5864600e-003 1.2641717e-001 3.0226390e-002 +v -8.8746180e-002 1.3625578e-001 7.1477800e-003 +v -7.7206730e-002 1.5639950e-001 -1.8972540e-002 +v -9.3176480e-002 1.1821016e-001 2.3362360e-002 +v -2.3506850e-002 1.2672006e-001 1.0996900e-002 +v -6.6546650e-002 1.7171115e-001 -4.2127770e-002 +v -6.9136000e-002 1.7247836e-001 -3.9013330e-002 +v 5.7180270e-002 7.1107690e-002 8.0307600e-003 +v -7.5390870e-002 1.7952824e-001 -5.2402050e-002 +v -3.1828840e-002 1.2639115e-001 1.0013410e-002 +v -8.9888800e-003 1.2952269e-001 2.2026810e-002 +v 3.4325880e-002 1.1193312e-001 -2.2406500e-003 +v -8.1414950e-002 9.7100250e-002 -6.8745800e-003 +v -2.3298830e-002 1.8324307e-001 -1.7923000e-002 +v -6.1641660e-002 1.5582039e-001 1.1099820e-002 +v -8.8826450e-002 9.0483320e-002 2.1204700e-002 +v 5.8373130e-002 6.8067590e-002 5.7247600e-003 +v -4.3045630e-002 1.2785122e-001 1.6842260e-002 +v 3.0835720e-002 1.1554234e-001 -3.1785500e-003 +v -8.8631270e-002 9.4881200e-002 7.9337600e-003 +v -9.1715140e-002 1.1709957e-001 3.0809400e-002 +v -7.2083780e-002 1.7499844e-001 -4.1930320e-002 +v -6.9540630e-002 1.5308527e-001 3.3865720e-002 +v 6.0078690e-002 6.8129260e-002 1.1454500e-002 +v -4.0081060e-002 1.2628381e-001 1.9607250e-002 +v 3.2819930e-002 1.1655625e-001 4.4458600e-003 +v -7.2823220e-002 1.4510601e-001 -1.5654680e-002 +v -8.5270210e-002 1.0551770e-001 2.3290940e-002 +v -7.6051320e-002 1.1103825e-001 -6.2722100e-003 +v -8.6537730e-002 1.5154801e-001 2.5875370e-002 +v 5.5888480e-002 7.2579250e-002 1.0669650e-002 +v -5.4642360e-002 1.5522963e-001 1.2612400e-002 +v 3.6729960e-002 1.1116756e-001 3.8670600e-003 +v 3.1501870e-002 1.1725172e-001 1.6855100e-003 +v -7.8751550e-002 9.5240290e-002 -1.0600670e-002 +v -8.9408160e-002 1.4352815e-001 3.0924750e-002 +v -2.0891130e-002 1.8595338e-001 -1.5037360e-002 +v -7.0863560e-002 1.6136525e-001 -9.7324600e-003 +v -7.0919760e-002 1.7136688e-001 -3.2763750e-002 +v -3.0771290e-002 1.2564075e-001 1.6594770e-002 +v -5.4454180e-002 1.5297699e-001 2.2505190e-002 +v -1.5539500e-003 1.2754717e-001 2.9232870e-002 +v 2.9130550e-002 1.2027445e-001 6.1117500e-003 +v 2.5725940e-002 1.2122705e-001 -3.6150000e-005 +v -8.9318970e-002 9.9546980e-002 1.3418110e-002 +v -7.5429500e-002 1.7095605e-001 -3.2879890e-002 +v -2.8596020e-002 1.1901156e-001 2.9888170e-002 +v 2.1069780e-002 1.2497756e-001 1.0998100e-003 +v -9.2240760e-002 1.1816838e-001 4.1201730e-002 +v 2.4094600e-003 1.0016785e-001 4.6938070e-002 +v -5.6627620e-002 1.5270606e-001 2.9629030e-002 +v -5.7264800e-002 1.5506250e-001 1.9322430e-002 +v -3.6452070e-002 1.2199869e-001 2.7670650e-002 +v -7.4108160e-002 1.7355729e-001 -3.7986840e-002 +v 5.1537130e-002 7.3496690e-002 1.2698700e-002 +v -6.6096040e-002 1.5532529e-001 7.1561800e-003 +v 3.6102000e-002 1.1266103e-001 1.0491780e-002 +v 1.6715210e-002 1.2689851e-001 2.2331000e-004 +v -8.0767920e-002 1.4301400e-001 -1.5312800e-003 +v -9.1757600e-002 1.4334588e-001 1.7790710e-002 +v -8.6824940e-002 1.5280775e-001 1.5521450e-002 +v -6.5808100e-002 1.6764344e-001 -3.0558670e-002 +v -7.8217340e-002 1.6873975e-001 -3.3564250e-002 +v -7.2567060e-002 1.4753230e-001 4.1714090e-002 +v 5.8439960e-002 7.0200810e-002 1.7779620e-002 +v 5.6847560e-002 7.2017160e-002 1.7139380e-002 +v 5.4919390e-002 7.3161610e-002 1.5223590e-002 +v 4.7446900e-002 7.3691410e-002 1.2430020e-002 +v 1.2319360e-002 1.2903768e-001 1.3336200e-003 +v -7.9790640e-002 1.0351662e-001 -6.6275400e-003 +v -7.6655210e-002 1.5509766e-001 7.9686300e-003 +v 2.1747320e-002 1.2118456e-001 3.0878810e-002 +v -7.5260490e-002 1.4938613e-001 3.9175980e-002 +v -2.5919610e-002 1.8272826e-001 -1.3541090e-002 +v -6.7983790e-002 1.6974781e-001 -3.1627490e-002 +v 1.6831110e-002 1.2487146e-001 2.8425580e-002 +v 5.4016490e-002 7.2883850e-002 1.8678010e-002 +v 5.0522750e-002 7.3397910e-002 1.6166890e-002 +v -5.9582440e-002 1.5623338e-001 7.9209900e-003 +v 2.5343500e-002 1.2374750e-001 9.9818800e-003 +v 1.9262750e-002 1.2689390e-001 5.5552100e-003 +v -9.0758520e-002 1.4223375e-001 2.6008130e-002 +v -4.6548490e-002 1.3320769e-001 1.6889630e-002 +v -2.4106950e-002 1.8380887e-001 -1.1544760e-002 +v 8.6784400e-003 1.2894574e-001 2.6156880e-002 +v 2.4919200e-003 1.2983563e-001 2.4847110e-002 +v 5.7345150e-002 6.9482720e-002 2.1153510e-002 +v -8.5329840e-002 1.5339912e-001 2.0378290e-002 +v 3.2877320e-002 1.1691463e-001 9.2957500e-003 +v 2.4246630e-002 1.2377758e-001 4.8764500e-003 +v -4.7765650e-002 1.3301969e-001 2.2874020e-002 +v -6.3541830e-002 1.6332115e-001 -2.5912990e-002 +v -6.6605200e-002 1.6477375e-001 -2.0670760e-002 +v -6.8504220e-002 1.6732018e-001 -2.3959570e-002 +v -7.2759160e-002 1.6965906e-001 -2.7013420e-002 +v 4.8206850e-002 7.2698580e-002 1.6994630e-002 +v -2.7383180e-002 1.2324257e-001 2.1658860e-002 +v -4.5077500e-002 1.3124443e-001 1.1145770e-002 +v 2.9253150e-002 1.2057701e-001 1.2299330e-002 +v 1.3677610e-002 1.2967262e-001 6.9327400e-003 +v 8.4210900e-003 1.3090986e-001 6.2754400e-003 +v 9.6836000e-004 1.3064303e-001 2.5865900e-003 +v 3.0802000e-003 9.8307360e-002 5.0535640e-002 +v -5.2420170e-002 1.5310101e-001 1.2927370e-002 +v -7.0359720e-002 1.6906988e-001 -2.6144260e-002 +v 5.4359390e-002 7.1467260e-002 2.1381250e-002 +v 4.5161440e-002 7.1030380e-002 2.2530690e-002 +v 1.9320440e-002 1.2738348e-001 1.1296310e-002 +v -9.3281210e-002 1.2691094e-001 1.3505010e-002 +v -8.7405060e-002 1.0593990e-001 1.3645920e-002 +v -2.2851640e-002 9.0635040e-002 5.2280460e-002 +v -6.2099370e-002 1.5406697e-001 3.0837360e-002 +v -4.5851560e-002 1.2072981e-001 2.7665040e-002 +v 5.0781670e-002 7.2155170e-002 2.0680180e-002 +v -8.9607270e-002 1.3971105e-001 2.9308560e-002 +v -5.3323050e-002 1.5273520e-001 1.6213860e-002 +v -1.5227080e-002 1.2784878e-001 2.1545200e-002 +v 3.3663540e-002 1.1574212e-001 1.7181290e-002 +v 2.4000260e-002 1.2468761e-001 1.5517930e-002 +v -8.4166840e-002 9.7756820e-002 -3.2761900e-003 +v -3.6223590e-002 1.2777519e-001 9.8501500e-003 +v -3.9189580e-002 1.2828193e-001 5.0346300e-003 +v -3.3674050e-002 1.7774449e-001 -8.1799500e-003 +v -7.4488620e-002 1.5649443e-001 -2.5954600e-003 +v -4.6755620e-002 1.3284294e-001 8.1212800e-003 +v -8.4970410e-002 1.5322309e-001 1.2654460e-002 +v -1.0866210e-002 1.2691699e-001 2.7575440e-002 +v -3.1074000e-003 1.3072898e-001 5.6428500e-003 +v -8.8760540e-002 9.7037440e-002 2.1079040e-002 +v -6.4811320e-002 3.4530640e-002 1.5508440e-002 +v -6.4300260e-002 3.5086450e-002 2.4272050e-002 +v -6.6727020e-002 3.5895770e-002 3.3849430e-002 +v 1.9838510e-002 9.6518890e-002 -2.2785880e-002 +v -3.8670510e-002 1.6070199e-001 -1.2357760e-002 +v -7.6890090e-002 1.3041906e-001 -6.9570100e-003 +v -7.2539730e-002 3.5399270e-002 7.0298800e-003 +v -6.9209050e-002 3.5454810e-002 1.2042140e-002 +v -6.4160810e-002 3.5900770e-002 1.7687570e-002 +v -6.6804150e-002 3.7377740e-002 3.3296290e-002 +v -6.2928350e-002 3.9061660e-002 4.2707680e-002 +v -7.1752230e-002 3.6789350e-002 8.6966700e-003 +v -6.5171380e-002 3.7289500e-002 2.5953770e-002 +v -6.6392030e-002 3.7712350e-002 2.9621950e-002 +v -6.4558720e-002 3.9639900e-002 3.9411530e-002 +v -6.0145790e-002 4.1202050e-002 4.4293830e-002 +v -6.0318430e-002 3.8442990e-002 4.5245950e-002 +v -3.6756310e-002 8.8663360e-002 -2.3868800e-002 +v -3.9494750e-002 3.7551570e-002 4.2870900e-002 +v -7.2016030e-002 3.7572700e-002 3.9789400e-003 +v -7.1693630e-002 3.9461000e-002 6.0145000e-003 +v -7.1165950e-002 3.9366310e-002 8.1142100e-003 +v -6.9000300e-002 3.8467710e-002 1.0768900e-002 +v -6.7253420e-002 3.8142160e-002 1.3533960e-002 +v -6.1125670e-002 3.7790050e-002 1.9710900e-002 +v -3.9179680e-002 4.2406740e-002 4.1476070e-002 +v -3.5145960e-002 3.8585920e-002 4.7732690e-002 +v -2.8950940e-002 3.9285940e-002 5.3309090e-002 +v -1.8223900e-002 9.7494570e-002 4.6847940e-002 +v -6.6916260e-002 1.2278907e-001 -8.9077400e-003 +v -6.3754640e-002 3.8250120e-002 1.6593500e-002 +v -6.4415760e-002 4.1283840e-002 2.8243480e-002 +v -8.5856340e-002 9.7025390e-002 2.7414960e-002 +v -3.7501130e-002 4.0221900e-002 4.4296550e-002 +v -3.4333970e-002 4.0923630e-002 4.8425810e-002 +v -3.1172890e-002 4.0294330e-002 5.1312460e-002 +v -6.9997320e-002 4.2073080e-002 6.6897800e-003 +v -8.0379330e-002 9.7800660e-002 3.3645750e-002 +v -2.6273160e-002 7.7631160e-002 4.8356180e-002 +v -3.7501450e-002 4.2736690e-002 4.2988400e-002 +v -2.6177500e-002 4.2498930e-002 5.3315220e-002 +v -6.9637250e-002 4.1881270e-002 3.1825800e-003 +v -6.7156510e-002 4.1972860e-002 1.0240940e-002 +v -8.7405510e-002 1.0205209e-001 2.2020360e-002 +v -2.3944380e-002 7.8800140e-002 5.3534730e-002 +v -6.0902360e-002 4.3429500e-002 4.2678530e-002 +v -3.1217880e-002 4.3847510e-002 4.9780920e-002 +v -7.5729440e-002 1.0354026e-001 3.6070970e-002 +v -6.2425320e-002 4.1885720e-002 1.4646770e-002 +v -6.1051660e-002 4.4392230e-002 1.2421940e-002 +v 2.5855060e-002 8.9610660e-002 -2.2701840e-002 +v -7.7644960e-002 8.2214940e-002 3.5797660e-002 +v -6.0381270e-002 4.5921420e-002 4.0088740e-002 +v -2.4982010e-002 8.1777650e-002 5.3421060e-002 +v -3.4453850e-002 4.4563960e-002 4.5422990e-002 +v -2.9842910e-002 4.6782280e-002 4.7746920e-002 +v -1.5119580e-002 9.9930020e-002 4.4500270e-002 +v -6.7306470e-002 4.4176830e-002 7.5958300e-003 +v -5.7852990e-002 4.6444500e-002 1.1062610e-002 +v -5.1815260e-002 1.6392582e-001 1.7488800e-003 +v -5.5174130e-002 4.8383880e-002 3.8517780e-002 +v -7.8849150e-002 1.1867375e-001 5.0622870e-002 +v -2.7229070e-002 8.7991480e-002 4.7909730e-002 +v -7.5536880e-002 1.5977062e-001 -1.0438650e-002 +v -3.6151280e-002 4.6505140e-002 4.0740900e-002 +v -2.5439220e-002 9.0677870e-002 4.8852330e-002 +v -8.0050370e-002 1.1670406e-001 4.8762460e-002 +v -5.2513640e-002 4.7577880e-002 1.4858440e-002 +v -3.2043560e-002 5.0461830e-002 3.9341520e-002 +v -3.1487770e-002 4.6930210e-002 4.5253210e-002 +v -2.0321500e-002 9.3999570e-002 5.1588540e-002 +v -7.2145040e-002 9.1556450e-002 4.1494780e-002 +v -5.3644200e-002 4.9358170e-002 1.2201850e-002 +v -8.2403890e-002 1.2186563e-001 4.9365030e-002 +v -4.9754420e-002 4.9738300e-002 3.7037110e-002 +v -3.2332060e-002 4.8672840e-002 4.2523960e-002 +v -2.3122950e-002 9.4515900e-002 4.7358870e-002 +v -8.6347140e-002 9.1722090e-002 2.6811080e-002 +v -5.7713110e-002 4.8717820e-002 7.2765100e-003 +v -8.6970360e-002 8.8912090e-002 2.4879860e-002 +v -9.2237750e-002 1.2488519e-001 4.0786530e-002 +v -1.5862800e-002 9.7021620e-002 5.0139360e-002 +v -2.7720040e-002 5.0502090e-002 4.3340720e-002 +v -8.5918770e-002 1.4263412e-001 3.9849810e-002 +v -7.5097360e-002 9.0073560e-002 3.9581000e-002 +v -8.9430840e-002 1.4730552e-001 2.7694960e-002 +v -5.3288350e-002 5.1925760e-002 1.1730350e-002 +v -5.0168720e-002 5.3462260e-002 1.6255440e-002 +v -8.5986050e-002 1.4670902e-001 3.4827030e-002 +v -6.9937250e-002 8.6076860e-002 4.2175690e-002 +v -5.0399320e-002 5.1831330e-002 3.4037400e-002 +v -8.3298980e-002 1.4960772e-001 3.3740890e-002 +v -2.9174820e-002 5.2264530e-002 3.7637320e-002 +v -8.8763730e-002 1.1944938e-001 4.6560090e-002 +v -7.7693460e-002 1.7367969e-001 -4.1478670e-002 +v -8.3418140e-002 9.4127440e-002 3.0898450e-002 +v -5.6067510e-002 5.3470630e-002 7.3718200e-003 +v -7.8935630e-002 1.4817228e-001 3.9463070e-002 +v -6.7902770e-002 8.7817230e-002 4.3526990e-002 +v -4.4111240e-002 9.2883990e-002 -2.2373210e-002 +v -8.6605100e-002 1.3226807e-001 4.6783020e-002 +v -9.2654280e-002 1.2084025e-001 4.1629650e-002 +v -5.0887310e-002 5.2727900e-002 1.4455790e-002 +v -4.9763410e-002 5.6241200e-002 3.3624250e-002 +v -8.9771330e-002 1.2904861e-001 4.3022990e-002 +v -2.8054240e-002 5.4551030e-002 3.6786850e-002 +v -2.5867080e-002 5.6689210e-002 3.9182240e-002 +v -8.3702200e-002 1.2226381e-001 -3.7301400e-003 +v -8.1455470e-002 1.3012213e-001 5.2117660e-002 +v -5.1458550e-002 5.5878150e-002 1.5900350e-002 +v -7.8597700e-002 1.7441574e-001 -4.6607580e-002 +v -5.2909820e-002 5.7043070e-002 2.0988410e-002 +v -5.2978500e-002 5.9553770e-002 2.6211920e-002 +v -5.2130640e-002 5.6302970e-002 2.6672460e-002 +v -4.7714500e-002 6.1944520e-002 3.6705820e-002 +v -8.3539790e-002 8.1169560e-002 2.7014070e-002 +v -1.8340000e-002 5.7489970e-002 4.9763020e-002 +v -8.0069810e-002 9.0586130e-002 3.4593070e-002 +v -8.3812250e-002 8.6337700e-002 2.9223270e-002 +v -5.5436650e-002 5.9420250e-002 2.3018970e-002 +v -8.2227680e-002 1.4513771e-001 4.0600080e-002 +v -2.4187580e-002 7.2269150e-002 4.7681090e-002 +v -2.5353150e-002 6.2567200e-002 4.0642170e-002 +v -9.1132110e-002 1.2282100e-001 4.4115160e-002 +v -4.6076290e-002 1.6819719e-001 7.3744000e-004 +v -8.7829280e-002 1.4351461e-001 3.5707670e-002 +v -8.6990640e-002 1.3812326e-001 4.2316550e-002 +v -1.5715900e-002 6.0822970e-002 5.2365440e-002 +v -8.3803580e-002 1.2561100e-001 5.0440490e-002 +v -6.2786680e-002 1.1274190e-001 -1.3605440e-002 +v -8.1033840e-002 8.4698180e-002 3.3106400e-002 +v -8.8563540e-002 1.1624535e-001 4.5392840e-002 +v -2.0268380e-002 6.2266810e-002 4.8212120e-002 +v -1.2619630e-002 6.1635030e-002 5.4424080e-002 +v -7.0491190e-002 8.1818160e-002 4.0609890e-002 +v -8.3882520e-002 1.3331465e-001 4.9113540e-002 +v -5.6560350e-002 4.8355540e-002 3.6607050e-002 +v 9.9444900e-003 1.0919723e-001 -1.9472810e-002 +v -5.5928250e-002 3.5917310e-002 4.6376100e-002 +v -7.6003260e-002 1.6361344e-001 -1.8021110e-002 +v -8.3798850e-002 1.0290691e-001 2.8038330e-002 +v -8.8252110e-002 1.2692730e-001 4.6141300e-002 +v -7.9126720e-002 1.0619883e-001 3.2050700e-002 +v -8.8206230e-002 9.4485700e-002 2.3744010e-002 +v -8.9110330e-002 1.3851394e-001 3.7658780e-002 +v -1.9321360e-002 9.2123890e-002 5.3820650e-002 +v -5.8265630e-002 9.0926390e-002 -2.0948690e-002 +v -2.7046310e-002 6.7014450e-002 3.9672140e-002 +v -2.1416300e-002 1.7977662e-001 -2.1732520e-002 +v -7.8240000e-003 1.0924112e-001 -2.2185670e-002 +v -2.3988340e-002 8.5995590e-002 5.3716430e-002 +v -6.0483580e-002 1.5567975e-001 4.3343800e-003 +v -8.6389150e-002 1.2168475e-001 4.8412440e-002 +v -7.4084360e-002 1.4987744e-001 -3.2610050e-002 +v -2.0580600e-002 7.9572500e-002 5.6013880e-002 +v -8.3837500e-002 1.3927865e-001 4.4893850e-002 +v -2.2933960e-002 3.5632910e-002 5.2865490e-002 +v -8.6153620e-002 1.2735612e-001 4.8563960e-002 +v -6.5728590e-002 1.0709818e-001 -1.4317670e-002 +v -2.1481090e-002 7.4194460e-002 5.2857680e-002 +v -7.6423900e-002 1.5736285e-001 -9.0354600e-003 +v -7.7216010e-002 8.5594880e-002 3.7420770e-002 +v -8.4150830e-002 1.2955013e-001 5.0483700e-002 +v -8.1221440e-002 8.1003250e-002 3.1255840e-002 +v -8.1704000e-002 1.0167226e-001 3.0939660e-002 +v -8.6252730e-002 1.0106846e-001 2.5413770e-002 +v -8.0944970e-002 1.3903572e-001 4.7359080e-002 +v -7.8908350e-002 9.4830900e-002 3.5435500e-002 +v -7.3440160e-002 9.5412600e-002 4.0210650e-002 +v -5.2675780e-002 8.8220740e-002 -2.1886300e-002 +v -7.6440670e-002 7.7511060e-002 3.3748300e-002 +v -2.1791140e-002 1.0658035e-001 -2.2327000e-002 +v -8.8360940e-002 1.4996706e-001 2.6044170e-002 +v -2.4078870e-002 6.7906700e-002 4.5178370e-002 +v -2.0018090e-002 6.7569300e-002 5.1565340e-002 +v -8.3577750e-002 1.2052625e-001 4.9177500e-002 +v -1.4655950e-002 1.7456543e-001 -2.5972690e-002 +v -2.7395940e-002 8.4108300e-002 4.8745680e-002 +v -4.1933580e-002 8.8463400e-002 -2.2126350e-002 +v -3.1693900e-002 1.0261265e-001 -2.2352310e-002 +v -2.7890200e-002 1.0440703e-001 -2.2830920e-002 +v -7.3790400e-002 1.2016662e-001 -7.8851200e-003 +v -4.6124160e-002 1.0506369e-001 -2.0457580e-002 +v -2.7412650e-002 7.3269450e-002 4.2641380e-002 +v -4.5532880e-002 3.4736480e-002 -2.1363200e-002 +v -4.4993030e-002 3.9017010e-002 -2.1097830e-002 +v -4.6462610e-002 3.6800270e-002 -1.7778710e-002 +v -8.8366460e-002 1.1361863e-001 5.8227800e-003 +v 5.1746240e-002 7.2897250e-002 9.0647400e-003 +v -7.0385250e-002 3.7450300e-002 -9.3190000e-004 +v -6.0923170e-002 3.8621820e-002 2.2468850e-002 +v -7.7696720e-002 1.7027889e-001 -4.3117910e-002 +v -4.3793210e-002 1.6955506e-001 -7.3026400e-003 +v -7.7587180e-002 1.7717875e-001 -5.0221090e-002 +v -4.0541880e-002 3.8886010e-002 -2.7364950e-002 +v -4.4215850e-002 3.6131460e-002 -2.4252210e-002 +v -6.6634880e-002 4.0430310e-002 -5.0180700e-003 +v -6.9242120e-002 4.1474050e-002 1.9289000e-004 +v -7.5640690e-002 1.5930400e-001 -2.6908460e-002 +v -6.3087030e-002 3.9614170e-002 2.5181560e-002 +v -7.2303020e-002 1.5186699e-001 -4.1544310e-002 +v -4.1051490e-002 4.1528620e-002 -2.4061000e-002 +v -4.6990580e-002 3.8892380e-002 -1.4016920e-002 +v -8.9559690e-002 1.2851666e-001 4.5457500e-003 +v -7.6987340e-002 1.5369375e-001 -2.2970800e-003 +v -7.0121670e-002 1.6882633e-001 -5.1173650e-002 +v -6.4792610e-002 4.1724530e-002 3.1616900e-002 +v -4.2148060e-002 1.2409627e-001 -9.5602500e-003 +v -4.8069700e-002 1.2493027e-001 -8.4076400e-003 +v -4.2150480e-002 4.3343970e-002 -2.1508710e-002 +v -6.7315160e-002 4.4034000e-002 1.5741800e-003 +v -7.3386640e-002 1.5463418e-001 -2.9943830e-002 +v -5.5352770e-002 4.2936210e-002 1.9135490e-002 +v -6.0067770e-002 4.1419500e-002 2.2953280e-002 +v -6.5488460e-002 4.0937780e-002 3.5315470e-002 +v -8.0066400e-002 1.5039650e-001 6.0518000e-004 +v -4.4031300e-002 4.1949070e-002 -1.7993960e-002 +v -4.5186510e-002 4.2453420e-002 -1.4193620e-002 +v -8.3109430e-002 1.0265445e-001 -3.2933400e-003 +v -6.5472800e-002 4.5627570e-002 4.5575400e-003 +v -7.5427730e-002 1.5201213e-001 -1.4393690e-002 +v -5.4473420e-002 4.5937510e-002 2.3612600e-002 +v -6.2464100e-002 4.3722000e-002 2.8493310e-002 +v -6.2832600e-002 4.5182750e-002 3.4622890e-002 +v -6.3538130e-002 4.3524020e-002 3.7974010e-002 +v -6.0255260e-002 4.4749620e-002 -4.1316200e-003 +v -6.3242050e-002 4.5549700e-002 4.8428000e-004 +v -6.2249430e-002 4.6540050e-002 7.1903500e-003 +v -9.1003650e-002 1.4885725e-001 2.1507030e-002 +v -5.7094130e-002 4.5996540e-002 2.6865280e-002 +v -5.7276490e-002 4.7299580e-002 2.9889950e-002 +v -3.9519900e-002 1.7385855e-001 -7.5752600e-003 +v -8.9641110e-002 1.3841920e-001 3.4141800e-002 +v -9.2601430e-002 1.3018652e-001 2.5183580e-002 +v -9.2280860e-002 1.2762053e-001 2.9751670e-002 +v -3.3957310e-002 4.1025060e-002 -2.9660250e-002 +v -9.0199540e-002 1.1657506e-001 5.6754900e-003 +v -5.8515890e-002 4.7731310e-002 2.1246000e-004 +v -7.1723560e-002 1.4617438e-001 -2.1567820e-002 +v -5.2389820e-002 4.5449130e-002 1.7686300e-002 +v -5.9414350e-002 4.7277990e-002 3.4172420e-002 +v -5.7520620e-002 1.5877600e-001 4.1621200e-003 +v -8.0959140e-002 1.0926674e-001 -2.0189900e-003 +v -5.1904000e-002 4.6100060e-002 1.9421290e-002 +v -5.1830050e-002 4.8568730e-002 2.1647030e-002 +v -7.7650400e-002 1.5658012e-001 -1.6599150e-002 +v -3.7416450e-002 4.7682130e-002 -1.7147280e-002 +v -7.8876110e-002 1.5347012e-001 3.9875800e-003 +v -5.7635420e-002 5.0425540e-002 4.6108400e-003 +v -5.2625440e-002 5.0434620e-002 2.9046740e-002 +v -5.2998720e-002 4.9169020e-002 3.3967600e-002 +v -7.3502600e-002 1.6871934e-001 -4.4791800e-002 +v -5.4420720e-002 4.7836520e-002 -5.9186900e-003 +v -5.2312740e-002 5.1085350e-002 2.4485690e-002 +v -7.9129930e-002 1.6736568e-001 -3.5506230e-002 +v 9.4115700e-003 1.2350285e-001 -9.8291000e-003 +v -3.2715700e-002 1.0896631e-001 -1.8941410e-002 +v -3.1133380e-002 4.9607260e-002 -1.9406940e-002 +v 4.5997330e-002 6.9814450e-002 3.0143300e-003 +v 3.3525460e-002 1.0966209e-001 -6.9894800e-003 +v -5.5047160e-002 5.2767560e-002 -3.9461300e-003 +v -5.6897890e-002 4.9655570e-002 -1.5319000e-003 +v -5.0290500e-002 4.9098930e-002 1.7164780e-002 +v -5.0595170e-002 4.9923270e-002 1.9174130e-002 +v -5.1887420e-002 5.3324670e-002 2.8705560e-002 +v -6.7684480e-002 1.6533627e-001 -5.5466400e-002 +v -3.0271440e-002 5.2106080e-002 -1.7676140e-002 +v -9.1087300e-003 1.1141669e-001 -2.0543230e-002 +v -5.7069360e-002 5.4424380e-002 2.3395500e-003 +v -3.2748380e-002 1.7759875e-001 -1.1627470e-002 +v -2.9009580e-002 5.1265290e-002 -2.2175780e-002 +v -3.1383130e-002 5.1791310e-002 -1.3886800e-002 +v -5.5673960e-002 5.6983850e-002 -3.3510400e-003 +v -5.0916050e-002 5.3813610e-002 1.9753140e-002 +v -8.8875380e-002 1.5169443e-001 2.0086580e-002 +v -7.7153050e-002 1.7378676e-001 -4.7867620e-002 +v -7.8577770e-002 1.6420639e-001 -3.1825860e-002 +v -2.7545910e-002 5.4021570e-002 -2.5147390e-002 +v -5.4463660e-002 5.5357450e-002 1.0326840e-002 +v -8.7041410e-002 1.3058932e-001 9.1161000e-004 +v -9.0009340e-002 1.3278082e-001 5.9220600e-003 +v -9.2232620e-002 1.3195400e-001 1.5430650e-002 +v -4.8639980e-002 1.6472475e-001 -5.0591500e-003 +v -5.4066480e-002 5.9959350e-002 -7.5992200e-003 +v -5.7434090e-002 5.7683500e-002 8.7259700e-003 +v -8.6794730e-002 1.3850688e-001 4.5575900e-003 +v -9.2989530e-002 1.3092307e-001 1.9919290e-002 +v -9.1282030e-002 1.3311897e-001 2.4688630e-002 +v 2.1815020e-002 1.1770533e-001 -1.0015300e-002 +v -2.9647120e-002 5.8104260e-002 -2.1311320e-002 +v -3.1289530e-002 5.5208570e-002 -1.4387840e-002 +v -5.9002160e-002 5.9234620e-002 2.6140800e-003 +v -9.0241700e-002 1.3575994e-001 1.4149160e-002 +v -6.1569420e-002 1.7084875e-001 -6.1679170e-002 +v -6.6070180e-002 1.6557822e-001 -5.8644080e-002 +v -2.4539930e-002 1.8005865e-001 -1.8726950e-002 +v -1.6131750e-002 1.8298848e-001 -2.6037190e-002 +v -3.0809390e-002 5.6998040e-002 -1.7835020e-002 +v 1.0464280e-002 9.6180450e-002 -2.5898970e-002 +v -5.7491630e-002 5.9530160e-002 -1.0786100e-003 +v -8.9146460e-002 1.3650500e-001 2.5952780e-002 +v 4.3714500e-003 1.0391901e-001 -2.1515100e-002 +v -9.0377040e-002 1.3252490e-001 3.1082650e-002 +v -9.0795450e-002 1.3855232e-001 2.0562560e-002 +v -9.4237710e-002 1.2615419e-001 2.2201450e-002 +v -9.0336910e-002 1.3119830e-001 3.8138790e-002 +v -4.5082610e-002 1.2218447e-001 -1.1569430e-002 +v 1.1348010e-002 9.8243750e-002 -2.3024250e-002 +v -3.9227920e-002 9.9184630e-002 -2.1912720e-002 +v -6.5509530e-002 1.5857325e-001 -5.5600270e-002 +v -7.7409510e-002 1.6260515e-001 -2.0754580e-002 +v -4.8580010e-002 1.6689211e-001 -2.5256100e-003 +v -7.6922910e-002 1.5351394e-001 -9.0785600e-003 +v -6.7750580e-002 1.5734825e-001 -5.3982110e-002 +v 5.2906410e-002 6.5230450e-002 -5.1112000e-004 +v -2.9054820e-002 6.1084120e-002 -2.4918230e-002 +v -3.1066920e-002 6.5058860e-002 -2.2751080e-002 +v 2.4249720e-002 1.0266151e-001 -1.8313830e-002 +v -5.5473660e-002 1.6050213e-001 1.3763500e-003 +v -6.6642850e-002 1.6040875e-001 -5.6842680e-002 +v -7.8200320e-002 1.6073213e-001 -2.3999690e-002 +v -1.8320680e-002 1.1968625e-001 -1.1110660e-002 +v 2.1712970e-002 1.0956342e-001 -1.5081090e-002 +v -6.8382640e-002 1.5980248e-001 -5.4208800e-002 +v -2.5445620e-002 6.0208550e-002 -3.0864700e-002 +v -2.6540330e-002 6.5084000e-002 -3.1664870e-002 +v -2.8425710e-002 6.2199610e-002 -2.7938500e-002 +v -3.2605750e-002 6.1264600e-002 -1.5453010e-002 +v -7.0872290e-002 1.1611638e-001 -7.9563700e-003 +v -6.9780530e-002 1.5938570e-001 -4.9418240e-002 +v -3.0324870e-002 6.7694720e-002 -2.7654950e-002 +v -3.2977370e-002 6.6365180e-002 -1.8385530e-002 +v 1.3533490e-002 1.0255388e-001 -2.1579310e-002 +v 4.4408530e-002 6.9758860e-002 9.4765000e-004 +v -2.1999000e-003 1.1215881e-001 -1.9658660e-002 +v -7.2028500e-002 6.7046610e-002 -7.2256000e-004 +v -7.8699630e-002 1.7313910e-001 -4.2720470e-002 +v -8.3211970e-002 1.5072131e-001 4.2128500e-003 +v -8.7439060e-002 1.3374875e-001 2.3974700e-003 +v 2.6348020e-002 8.4562230e-002 -2.3151710e-002 +v -7.4901490e-002 7.0419350e-002 -2.2854300e-003 +v -5.4576350e-002 9.1562950e-002 -2.2098700e-002 +v -7.3242520e-002 1.5231332e-001 -3.5703520e-002 +v -7.4550960e-002 1.7218738e-001 -4.7551010e-002 +v -2.8680680e-002 6.8283500e-002 -3.0610160e-002 +v 1.7372900e-002 1.0246037e-001 -2.1487700e-002 +v -8.1257430e-002 7.3025200e-002 7.1020400e-003 +v -7.4982300e-002 1.5407794e-001 -1.8974470e-002 +v -9.1556500e-002 1.3196262e-001 1.0638150e-002 +v -8.2448000e-004 9.5165120e-002 -3.2056320e-002 +v -7.7618830e-002 7.3999130e-002 -5.3263500e-003 +v -7.9858790e-002 7.2755040e-002 3.0420200e-003 +v -8.1627470e-002 7.3470610e-002 1.1161690e-002 +v -7.3679290e-002 1.4785987e-001 -2.0236290e-002 +v -9.1309820e-002 1.4848588e-001 1.6270070e-002 +v -9.0850140e-002 1.4625613e-001 1.4809050e-002 +v -6.8543890e-002 1.7513008e-001 -5.7187900e-002 +v -2.7253960e-002 1.0747453e-001 -2.1279680e-002 +v 2.1443580e-002 1.2273826e-001 -2.9316700e-003 +v -7.9061200e-002 7.3724300e-002 -8.4521000e-004 +v -8.2063500e-002 7.5993670e-002 1.7615500e-003 +v -8.3736580e-002 7.6771840e-002 8.9586000e-003 +v -9.0205720e-002 1.4947775e-001 1.3035090e-002 +v 8.4818000e-004 1.1670025e-001 -1.7337090e-002 +v -7.4577550e-002 1.5164041e-001 -2.8647990e-002 +v -2.9087460e-002 7.2924630e-002 -3.3354470e-002 +v -3.1184020e-002 7.3989530e-002 -3.0339870e-002 +v -3.2606620e-002 7.1955620e-002 -2.4866580e-002 +v -8.0575990e-002 7.6607800e-002 -2.9879400e-003 +v -8.9491020e-002 1.4392581e-001 1.2488490e-002 +v -7.7388410e-002 1.4656426e-001 -4.3543000e-003 +v -7.2896160e-002 1.5834962e-001 -3.4109420e-002 +v 7.1346500e-003 1.1468229e-001 -1.8345640e-002 +v -3.4502610e-002 7.6130020e-002 -2.2373150e-002 +v -8.3890740e-002 8.0789530e-002 2.2951400e-003 +v -8.3740480e-002 7.7240270e-002 4.6673300e-003 +v -8.6204620e-002 8.0930750e-002 1.0535420e-002 +v -8.6061500e-002 7.9931100e-002 1.4440780e-002 +v -8.1542760e-002 7.7950660e-002 2.6727280e-002 +v 2.6666170e-002 1.1268609e-001 -1.0509540e-002 +v -7.6041430e-002 1.5663068e-001 -2.1420480e-002 +v -9.0012110e-002 1.5083344e-001 1.5752740e-002 +v -7.1156510e-002 1.6335125e-001 -4.5360530e-002 +v -3.3210960e-002 7.6873190e-002 -2.7708380e-002 +v -7.3263090e-002 7.9983830e-002 -1.3749940e-002 +v -7.9285950e-002 8.0048830e-002 -7.0125500e-003 +v -8.6034510e-002 8.2645720e-002 1.9542680e-002 +v -8.4335410e-002 8.0729950e-002 2.2180460e-002 +v -7.1351460e-002 1.5727092e-001 -4.2183090e-002 +v -7.3548450e-002 1.6120822e-001 -3.5288420e-002 +v 1.6732620e-002 1.0991230e-001 -1.7020040e-002 +v -3.0978770e-002 7.7020860e-002 -3.2816490e-002 +v -6.2359240e-002 1.7544824e-001 -6.1485990e-002 +v -1.7587870e-002 1.1491318e-001 -1.7205040e-002 +v -8.2354050e-002 8.0876320e-002 -2.4038900e-003 +v -7.8578910e-002 1.4050129e-001 -4.6031000e-003 +v -2.8931080e-002 7.9247620e-002 -3.5049800e-002 +v -3.1225710e-002 8.0413100e-002 -3.2182320e-002 +v -3.3258680e-002 7.9621670e-002 -2.7146060e-002 +v -4.4697400e-002 1.1791537e-001 -1.4725860e-002 +v -7.9723740e-002 8.4226660e-002 -8.7608600e-003 +v -8.5042160e-002 8.3817830e-002 -7.7640000e-005 +v -8.6776400e-002 8.4344860e-002 1.2419030e-002 +v -8.6674670e-002 8.2665010e-002 1.5174340e-002 +v -8.5106250e-002 8.5176580e-002 2.5679440e-002 +v -7.6975760e-002 8.2935940e-002 -1.1450630e-002 +v -8.2776390e-002 8.3430890e-002 -4.3687000e-003 +v -8.6180440e-002 8.2572150e-002 6.3639000e-003 +v -9.1160820e-002 1.4144362e-001 1.5673910e-002 +v -7.4638800e-002 1.4398484e-001 -7.1504600e-003 +v -8.3448500e-002 1.3393299e-001 -1.6873200e-003 +v -7.5804700e-002 1.5134475e-001 -1.9881200e-002 +v -7.4924140e-002 1.5273013e-001 -1.9397440e-002 +v -5.2314440e-002 1.2159646e-001 -1.0798060e-002 +v -3.0734050e-002 8.5427560e-002 -3.0506670e-002 +v -3.2590560e-002 8.1942660e-002 -2.9100210e-002 +v -8.6454830e-002 8.6940490e-002 9.1667000e-004 +v -1.2501820e-002 1.0634409e-001 -2.2360190e-002 +v -8.8585880e-002 1.4605869e-001 9.8780000e-003 +v -8.5609750e-002 1.4712513e-001 6.5981100e-003 +v -8.7511210e-002 1.5061504e-001 1.0152460e-002 +v -6.0113540e-002 3.5550440e-002 4.4907580e-002 +v -8.8284200e-002 8.6869110e-002 8.1029200e-003 +v -8.8812560e-002 8.7765490e-002 1.4226540e-002 +v -8.8001070e-002 8.6626430e-002 1.5466680e-002 +v -8.6991110e-002 8.6444700e-002 2.2420950e-002 +v -7.4609990e-002 1.4727815e-001 -1.4172380e-002 +v -3.4707910e-002 8.4035880e-002 -2.4302260e-002 +v -8.4964900e-002 8.9962540e-002 -3.0068000e-003 +v -8.8091450e-002 8.7741580e-002 4.8489900e-003 +v -9.1490470e-002 1.4543178e-001 2.2277220e-002 +v -9.4380420e-002 1.2183919e-001 1.7904340e-002 +v -2.9164530e-002 8.5393440e-002 -3.3666780e-002 +v -3.0557790e-002 8.8625920e-002 -2.7550670e-002 +v -7.7770550e-002 8.7844840e-002 -1.1694810e-002 +v -8.0728260e-002 8.8204150e-002 -7.8003100e-003 +v -8.3272540e-002 8.9476690e-002 -5.6502900e-003 +v -8.9398710e-002 8.9539000e-002 1.1645550e-002 +v -8.9698390e-002 1.3971257e-001 1.3774760e-002 +v -7.7134890e-002 1.5151225e-001 -5.5823000e-003 +v -5.1121410e-002 1.6374125e-001 -2.6640500e-003 +v -8.6442960e-002 1.2767438e-001 -1.4864100e-003 +v -6.9605590e-002 1.5490763e-001 -5.0188670e-002 +v -8.7265180e-002 9.2110030e-002 4.2059000e-003 +v -8.9086250e-002 9.2377120e-002 1.0569860e-002 +v -8.9612340e-002 9.1599880e-002 1.7812280e-002 +v -8.2732460e-002 1.4196856e-001 1.2529100e-003 +v -7.2618370e-002 1.4368135e-001 -1.0987100e-002 +v -7.7677230e-002 1.6610992e-001 -3.6777320e-002 +v -1.5078060e-002 9.3863440e-002 -3.4317310e-002 +v -7.1057280e-002 1.5476885e-001 -4.5778530e-002 +v -9.2331920e-002 1.2523886e-001 9.1589500e-003 +v -7.6046700e-002 9.1037250e-002 -1.3643150e-002 +v -8.2942810e-002 9.3291700e-002 -6.1856300e-003 +v -1.0411170e-002 9.4592340e-002 -3.3784850e-002 +v -2.9331140e-002 1.1476230e-001 -1.5844640e-002 +v -3.7218250e-002 1.1594244e-001 -1.5173050e-002 +v -1.2429920e-002 1.0286006e-001 -2.3822480e-002 +v 6.6509600e-003 8.8144500e-002 -3.2945810e-002 +v -6.4119900e-003 9.2876210e-002 -3.4817640e-002 +v 1.5800150e-002 1.1996558e-001 -1.1415630e-002 +v 2.9102740e-002 1.0247506e-001 -1.5768380e-002 +v 4.2080690e-002 6.3480630e-002 -2.5405300e-003 +v 2.8723120e-002 9.7943220e-002 -1.7497350e-002 +v -1.9987640e-002 1.0278313e-001 -2.3392920e-002 +v 3.3748350e-002 8.3644140e-002 -1.8630450e-002 +v -1.8685680e-002 1.8689625e-001 -2.0248700e-002 +v 6.4154900e-003 1.1790181e-001 -1.6282740e-002 +v 5.6305210e-002 6.7769910e-002 2.6525000e-003 +v -5.3608300e-003 1.1289400e-001 -1.9613290e-002 +v 4.5769430e-002 6.4628800e-002 -1.2166100e-003 +v -1.0090870e-002 9.8229650e-002 -2.7731360e-002 +v -6.0458520e-002 1.1755645e-001 -1.1354580e-002 +v 1.2933940e-002 1.1887250e-001 -1.3979370e-002 +v 1.5235680e-002 9.4977900e-002 -2.4437140e-002 +v -3.0892950e-002 4.7409030e-002 -2.4954000e-002 +v -1.7766190e-002 1.8572344e-001 -2.3049280e-002 +v -1.3034890e-002 1.1002855e-001 -2.0161170e-002 +v -7.1206550e-002 3.8608570e-002 7.7218000e-004 +v 1.7904800e-002 1.0627709e-001 -1.7729250e-002 +v -3.3623490e-002 1.1840428e-001 -1.1927480e-002 +v -4.9906840e-002 1.1788332e-001 -1.4402480e-002 +v -6.6878100e-003 1.1747209e-001 -1.5359280e-002 +v -1.5451470e-002 1.8597600e-001 -2.4795870e-002 +v -3.0603900e-002 3.8038460e-002 -3.0123840e-002 +v -1.3220270e-002 1.8397188e-001 -2.7519460e-002 +v -4.7859450e-002 1.1162729e-001 -1.7482120e-002 +v -1.3098990e-002 9.0776040e-002 -3.6659270e-002 +v -6.3117340e-002 1.5425437e-001 2.9730400e-003 +v -5.5139750e-002 1.1051601e-001 -1.7672740e-002 +v -1.1096770e-002 1.8202324e-001 -2.8042450e-002 +v -2.6568900e-002 3.4695830e-002 -2.9113750e-002 +v -6.6396600e-003 1.0222209e-001 -2.3519320e-002 +v -5.6996400e-002 1.5741713e-001 6.0244000e-004 +v 1.9076550e-002 9.1870620e-002 -2.4890230e-002 +v 1.3473090e-002 1.2429893e-001 -6.8361400e-003 +v -2.1730490e-002 9.8410960e-002 -2.4306850e-002 +v -1.7142170e-002 9.8057460e-002 -2.4924330e-002 +v -5.8698110e-002 1.5137318e-001 -6.5801000e-004 +v 3.5641100e-003 1.2764883e-001 -4.4672400e-003 +v -8.5369800e-003 9.9921220e-002 -2.4351070e-002 +v -1.2171980e-002 1.8125102e-001 -2.9061170e-002 +v -6.1113980e-002 1.5305212e-001 9.9983000e-004 +v -2.9570620e-002 1.1713871e-001 -1.3675530e-002 +v 3.0530110e-002 1.1221207e-001 -8.1860600e-003 +v -3.1714100e-002 3.5111530e-002 -3.0658990e-002 +v -1.3691130e-002 1.7914707e-001 -2.8126410e-002 +v 1.1620840e-002 1.1548972e-001 -1.6385680e-002 +v -6.1993570e-002 1.5028063e-001 -1.6297100e-003 +v 3.6684020e-002 1.0099570e-001 -9.8485900e-003 +v 4.8512670e-002 7.1798180e-002 6.0005000e-003 +v -4.6583000e-004 1.1983662e-001 -1.3610580e-002 +v 1.6747170e-002 9.0113950e-002 -2.7127190e-002 +v 6.9832400e-003 9.7730080e-002 -2.4800310e-002 +v -4.3226830e-002 4.6263570e-002 -1.1771730e-002 +v -8.3562500e-003 1.1373600e-001 -1.8239810e-002 +v -1.2354410e-002 1.1556773e-001 -1.6486930e-002 +v 4.6834470e-002 7.4354100e-002 1.0139500e-002 +v 2.5319170e-002 1.0931725e-001 -1.3579660e-002 +v -4.2459500e-002 1.1392482e-001 -1.6188050e-002 +v 5.7744640e-002 6.4158440e-002 2.6277600e-003 +v -5.9710530e-002 3.6535780e-002 -9.4949000e-003 +v -3.2078400e-003 1.0962100e-001 -2.1523850e-002 +v 2.7020740e-002 6.1345700e-002 -2.2292060e-002 +v 7.1030200e-003 1.0191162e-001 -2.1230990e-002 +v -3.8225680e-002 1.2465525e-001 -7.3257400e-003 +v 2.5941540e-002 1.1576352e-001 -8.2193900e-003 +v -6.1297960e-002 3.3900220e-002 -9.3216600e-003 +v -5.9466670e-002 1.4743956e-001 -1.8885400e-003 +v 1.0506610e-002 1.0087700e-001 -2.2109510e-002 +v 3.3081340e-002 1.0273382e-001 -1.2787210e-002 +v 1.2517840e-002 1.0475378e-001 -1.9915960e-002 +v 2.3087990e-002 9.3998720e-002 -2.2210680e-002 +v 3.1555430e-002 9.2484730e-002 -1.8204280e-002 +v 6.2723100e-003 9.9910370e-002 -2.2296890e-002 +v -4.0917240e-002 4.6121780e-002 -1.7942580e-002 +v 3.5407360e-002 9.8188850e-002 -1.2008970e-002 +v 9.4135900e-003 1.2121902e-001 -1.2937780e-002 +v 5.3735190e-002 7.2027350e-002 6.8010000e-003 +v 2.5620340e-002 1.1880719e-001 -5.0330800e-003 +v -3.8150260e-002 4.2466610e-002 -2.6893990e-002 +v -2.8212410e-002 1.1116862e-001 -1.8001930e-002 +v -6.0253590e-002 1.4339100e-001 -3.7906300e-003 +v 1.9016880e-002 1.0401450e-001 -1.9333120e-002 +v 7.5446700e-003 9.1682150e-002 -3.1643140e-002 +v -7.0760800e-003 1.2240119e-001 -1.1364410e-002 +v -1.9047500e-002 9.6562130e-002 -2.7579900e-002 +v -1.6953390e-002 1.0669256e-001 -2.2002990e-002 +v -6.7307000e-004 1.0119875e-001 -2.2857770e-002 +v -9.0179300e-003 1.2528031e-001 -7.7912000e-003 +v -6.8136180e-002 1.8006113e-001 -5.8816050e-002 +v -2.3600190e-002 1.1513818e-001 -1.5577390e-002 +v -5.9831220e-002 4.2842260e-002 -6.6469100e-003 +v 5.3124070e-002 5.9012380e-002 -2.8853800e-003 +v -3.6931840e-002 3.7107370e-002 -2.9714170e-002 +v -5.6215140e-002 1.4139213e-001 -2.8027300e-003 +v 3.6695880e-002 1.0372844e-001 -7.9621500e-003 +v -3.5885070e-002 1.2040038e-001 -1.0640470e-002 +v -9.3569500e-003 8.5423730e-002 -3.8112540e-002 +v -6.0127340e-002 1.2041391e-001 -9.3791100e-003 +v -3.9842790e-002 1.2156113e-001 -1.1570310e-002 +v 2.8322200e-002 1.0847957e-001 -1.2623390e-002 +v -1.8733500e-003 1.1593910e-001 -1.7169430e-002 +v 3.8648150e-002 9.0153340e-002 -1.2549680e-002 +v -1.7359200e-003 9.2244170e-002 -3.4310460e-002 +v 5.0000820e-002 6.1612070e-002 -3.4649900e-003 +v 5.5858960e-002 6.2910170e-002 6.9037000e-004 +v 2.0461520e-002 1.1515372e-001 -1.3103780e-002 +v -1.5165840e-002 1.1798075e-001 -1.4465520e-002 +v -7.0859540e-002 7.1510150e-002 3.3895100e-002 +v 2.2674030e-002 8.6606050e-002 -2.4925490e-002 +v 3.5358840e-002 8.7438890e-002 -1.7109050e-002 +v 1.8400920e-002 1.2145507e-001 -7.6804200e-003 +v -2.5425900e-002 4.1421010e-002 -2.9204830e-002 +v -8.2085100e-003 9.6777440e-002 -3.0809780e-002 +v -5.6810660e-002 3.3873940e-002 -1.1166310e-002 +v -3.4588640e-002 4.4744960e-002 -2.7122900e-002 +v -4.0251680e-002 1.1827531e-001 -1.3674080e-002 +v 1.6387020e-002 1.1402346e-001 -1.5496900e-002 +v 4.2635280e-002 6.0797460e-002 -3.4583700e-003 +v -5.0687200e-002 3.5935870e-002 -1.2380790e-002 +v 7.3446800e-003 9.4509570e-002 -2.9683220e-002 +v -1.9706700e-002 9.2917340e-002 -3.4636880e-002 +v -1.2083040e-002 1.2219229e-001 -9.7120900e-003 +v 4.8805930e-002 6.8457810e-002 1.6952900e-003 +v -3.0869700e-003 9.8402500e-002 -2.7403170e-002 +v -5.3198790e-002 1.3672896e-001 -1.6580500e-003 +v -4.7290060e-002 1.3055355e-001 1.6909100e-003 +v 4.4651700e-003 1.2044039e-001 -1.3931400e-002 +v -2.3850100e-003 1.2290534e-001 -1.0382460e-002 +v -2.4833330e-002 9.5858030e-002 -2.5162110e-002 +v -4.2296900e-002 3.6291920e-002 -2.7253600e-002 +v -5.4388260e-002 1.3404922e-001 -3.9920400e-003 +v -5.0539380e-002 1.3336659e-001 -1.0872200e-003 +v 2.6040300e-003 9.6942660e-002 -2.8407060e-002 +v -7.8163100e-003 1.2821209e-001 -1.9430400e-003 +v 6.5111700e-003 1.3002517e-001 9.2881000e-004 +v 3.4742860e-002 9.2274140e-002 -1.5654590e-002 +v -6.7787700e-002 1.8088887e-001 -5.8191050e-002 +v -3.3715410e-002 1.1151566e-001 -1.8078440e-002 +v 4.4630400e-003 1.2427294e-001 -9.4291400e-003 +v -2.3370170e-002 9.3392760e-002 -3.2031820e-002 +v -4.8982070e-002 1.2980647e-001 -1.3229400e-003 +v -7.8164000e-004 1.2822918e-001 -3.2490000e-003 +v 2.4960400e-003 8.9857600e-002 -3.3628450e-002 +v 7.4553300e-003 1.1196790e-001 -1.9554260e-002 +v 2.8791140e-002 9.1157340e-002 -2.0370210e-002 +v -5.3590150e-002 1.2437450e-001 -7.3470400e-003 +v -4.7743630e-002 1.2064432e-001 -1.2812990e-002 +v -1.9616230e-002 1.2109197e-001 -9.5487700e-003 +v -6.5047370e-002 1.7999148e-001 -5.9758600e-002 +v -5.1704160e-002 3.7620360e-002 -1.1763450e-002 +v -5.2124270e-002 1.2929832e-001 -4.1187000e-003 +v -4.5334450e-002 1.2891494e-001 1.5819100e-003 +v -3.0471200e-003 1.2919453e-001 -1.0688000e-003 +v 7.2129600e-003 1.2721957e-001 -5.2073700e-003 +v 1.1669320e-002 1.2720154e-001 -3.1850900e-003 +v 5.3056400e-002 6.9708830e-002 3.1291400e-003 +v -6.3021150e-002 1.7810951e-001 -6.0393570e-002 +v 2.8204800e-002 6.4391270e-002 -2.0698040e-002 +v 3.4400180e-002 1.0503000e-001 -1.0224920e-002 +v 3.0975190e-002 1.0790250e-001 -1.1058430e-002 +v -4.8984390e-002 1.1480518e-001 -1.5966690e-002 +v -3.2821710e-002 1.2300500e-001 -5.9088300e-003 +v -5.0792860e-002 1.2716487e-001 -4.8183200e-003 +v -3.5301670e-002 1.2547815e-001 -3.1542800e-003 +v 5.6455250e-002 6.9951490e-002 4.9191700e-003 +v -1.6240450e-002 1.2512177e-001 -3.6499700e-003 +v -1.6970400e-002 1.1119793e-001 -1.9586410e-002 +v -5.4088120e-002 3.9781210e-002 -1.0544680e-002 +v -3.4190490e-002 4.7514010e-002 -2.2301500e-002 +v 1.3699090e-002 9.3914220e-002 -2.6427690e-002 +v 8.8000000e-004 9.9234930e-002 -2.4355670e-002 +v -4.6459460e-002 1.2723953e-001 -4.8843300e-003 +v -4.1735500e-002 1.2687599e-001 -4.1742000e-003 +v -2.1000480e-002 1.2313643e-001 -6.1190100e-003 +v -1.2130450e-002 1.2572568e-001 -5.2007900e-003 +v -4.3822400e-003 1.2640753e-001 -6.9495200e-003 +v 1.4085700e-003 3.4781990e-002 -2.3265200e-002 +v -1.4846200e-002 3.5070930e-002 -2.6071900e-002 +v -2.1399500e-002 3.4795120e-002 -2.7958820e-002 +v 1.2009220e-002 3.5961900e-002 -2.1735750e-002 +v 3.8249200e-003 3.6129220e-002 -2.3878090e-002 +v -5.1139560e-002 9.6617580e-002 -2.2095120e-002 +v -5.4813320e-002 9.8102480e-002 -2.1425370e-002 +v -2.7597040e-002 1.6979824e-001 -1.8170420e-002 +v 1.3359870e-002 3.9377410e-002 -2.2496330e-002 +v 4.3919300e-003 3.8674430e-002 -2.4170290e-002 +v -6.8478200e-003 3.6444540e-002 -2.5177120e-002 +v -1.3280260e-002 3.7699590e-002 -2.6391810e-002 +v -4.7672760e-002 3.6116650e-002 -1.3301210e-002 +v -4.5590120e-002 1.0853826e-001 -1.8796680e-002 +v -5.0095670e-002 1.0990925e-001 -1.8504510e-002 +v -6.5766640e-002 3.6469550e-002 -7.2073000e-003 +v -2.3455840e-002 1.6824727e-001 -1.8822880e-002 +v -4.5918000e-003 3.8404570e-002 -2.5412870e-002 +v -2.4954130e-002 3.7441060e-002 -2.9152720e-002 +v 2.9007770e-002 3.7358220e-002 -2.7474000e-004 +v -7.9468800e-003 4.1489920e-002 -2.5911270e-002 +v -1.6803800e-002 3.9753810e-002 -2.7565350e-002 +v -6.5156150e-002 1.4034537e-001 -7.6848600e-003 +v -4.7080100e-002 4.0700690e-002 -1.1869830e-002 +v -6.8470630e-002 3.7477700e-002 -4.9557400e-003 +v 3.7326850e-002 4.0209510e-002 -8.5850000e-004 +v 3.5349870e-002 4.1257050e-002 -2.8075100e-003 +v 5.1820700e-003 4.1536320e-002 -2.4065670e-002 +v 1.8660660e-002 1.0030784e-001 -2.2127290e-002 +v -6.0510780e-002 1.0748450e-001 -1.7042300e-002 +v -6.2374340e-002 4.0146090e-002 -7.4040200e-003 +v 2.5456950e-002 3.9483890e-002 -4.0251400e-003 +v -2.2828000e-004 4.3394940e-002 -2.5124420e-002 +v -8.1088400e-003 4.3439060e-002 -2.6140070e-002 +v -1.7362450e-002 4.3237420e-002 -2.7665190e-002 +v -2.6416670e-002 4.4674020e-002 -2.8209740e-002 +v 3.8064500e-003 1.0944331e-001 -2.0203790e-002 +v -5.8232370e-002 9.5690400e-002 -2.0616030e-002 +v -6.6122370e-002 4.2341260e-002 -2.7538800e-003 +v -6.0959920e-002 9.4173040e-002 -1.9015670e-002 +v 3.1352250e-002 4.2649280e-002 -4.6745000e-003 +v -3.3540900e-002 3.6342620e-002 4.9089960e-002 +v 1.7252780e-002 4.4335610e-002 -2.3067190e-002 +v 1.0637660e-002 4.4161560e-002 -2.4926170e-002 +v 4.3843100e-003 4.5806710e-002 -2.6788990e-002 +v -8.2506400e-003 4.5148720e-002 -2.8441070e-002 +v -1.5748410e-002 4.5043860e-002 -2.7877790e-002 +v 2.8990330e-002 4.4697850e-002 -6.1863000e-003 +v 8.1686400e-003 4.5053030e-002 -2.5178740e-002 +v -9.6291000e-004 4.5378230e-002 -2.7308280e-002 +v -1.7033400e-003 4.7819200e-002 -2.9928930e-002 +v -3.1535830e-002 4.4740410e-002 -2.8079410e-002 +v -3.3619650e-002 1.5691468e-001 -1.1024870e-002 +v -5.0751180e-002 4.3109620e-002 -1.0018680e-002 +v 3.6890890e-002 4.7353200e-002 -6.1057100e-003 +v 2.4975630e-002 4.2644580e-002 -7.0169900e-003 +v 2.4562420e-002 4.8369560e-002 -1.9672760e-002 +v 1.3964040e-002 4.5579170e-002 -2.4706510e-002 +v 1.3376130e-002 4.8630300e-002 -2.6551500e-002 +v 3.7308900e-003 4.8127990e-002 -2.9025970e-002 +v -8.7947000e-003 4.7056850e-002 -2.9881630e-002 +v -1.3753770e-002 5.1865060e-002 -3.2243480e-002 +v -2.1200840e-002 4.6657090e-002 -2.7951320e-002 +v 3.9693540e-002 4.5658580e-002 -4.5274100e-003 +v 3.3627400e-002 4.8717730e-002 -6.3904600e-003 +v -6.5352120e-002 9.9294570e-002 -1.6820150e-002 +v 1.2868100e-003 5.0383670e-002 -3.0357440e-002 +v -8.1797500e-003 4.9845800e-002 -3.1071390e-002 +v -1.7184350e-002 4.8210500e-002 -2.9741930e-002 +v -2.6049450e-002 4.7692500e-002 -2.6149500e-002 +v -8.4747010e-002 1.1078350e-001 3.9488380e-002 +v -5.1316870e-002 4.8270690e-002 -7.9310500e-003 +v -8.2506510e-002 1.2765487e-001 -4.6796400e-003 +v 3.8663690e-002 5.1696670e-002 -6.6910200e-003 +v -7.5643160e-002 9.9440450e-002 -1.1927610e-002 +v 2.0284470e-002 5.1349190e-002 -2.4895380e-002 +v 5.9436000e-003 5.0976660e-002 -2.9119360e-002 +v -2.5528290e-002 5.1472710e-002 -2.6884680e-002 +v -3.5562670e-002 4.9399890e-002 -1.2865040e-002 +v -4.2818980e-002 1.6220182e-001 -1.0337510e-002 +v -6.5593600e-002 1.7665711e-001 -6.0504730e-002 +v -3.4151080e-002 1.7442797e-001 -1.3312550e-002 +v 4.3673180e-002 5.0162230e-002 -5.9843500e-003 +v -5.0342410e-002 1.5546197e-001 -5.1927700e-003 +v 2.5464180e-002 5.4029700e-002 -2.1691010e-002 +v 1.0149790e-002 4.9258540e-002 -2.7750590e-002 +v -2.2043190e-002 5.3612020e-002 -3.0135610e-002 +v -3.2875520e-002 5.1677630e-002 -1.0888650e-002 +v -3.7613820e-002 4.9534770e-002 -1.1626140e-002 +v -4.0750630e-002 4.9285110e-002 -1.1286200e-002 +v -4.6385170e-002 4.7490850e-002 -1.0085980e-002 +v 4.4473170e-002 5.3293010e-002 -6.3327900e-003 +v 3.3205620e-002 5.1020650e-002 -7.2382500e-003 +v 1.5678350e-002 5.1169270e-002 -2.6397810e-002 +v 6.8341700e-003 5.5010170e-002 -3.0561130e-002 +v 2.1424700e-003 5.5502800e-002 -3.1334400e-002 +v 5.9285000e-004 5.2867950e-002 -3.0513830e-002 +v -3.6481400e-003 5.1869000e-002 -3.1457940e-002 +v -9.4245600e-003 5.5399220e-002 -3.3653980e-002 +v -1.9302150e-002 5.8224770e-002 -3.3919700e-002 +v -6.1084270e-002 1.3386190e-001 -7.2248900e-003 +v -4.3309760e-002 5.5656840e-002 -1.1402110e-002 +v -6.1080540e-002 1.6833773e-001 -5.9192060e-002 +v 4.7574690e-002 5.2943630e-002 -5.1300300e-003 +v -3.7403030e-002 1.1150775e-001 -1.8243310e-002 +v 1.9972490e-002 5.4409710e-002 -2.7108230e-002 +v 5.3974800e-003 5.8382570e-002 -3.0903760e-002 +v -1.0603590e-002 5.3602910e-002 -3.3403350e-002 +v -3.4998290e-002 5.2331560e-002 -1.0347380e-002 +v -4.6471230e-002 5.1304340e-002 -9.8299800e-003 +v -6.7945360e-002 1.1493603e-001 -9.5107300e-003 +v -7.1048210e-002 1.5161088e-001 -4.4679270e-002 +v -5.8903800e-003 3.4790620e-002 -2.4224470e-002 +v 1.6842140e-002 5.5555670e-002 -2.8284560e-002 +v 1.0711040e-002 5.4687610e-002 -2.9767520e-002 +v -1.1826800e-003 5.9492420e-002 -3.3360920e-002 +v -5.2325900e-003 5.5688960e-002 -3.2840220e-002 +v -5.1705830e-002 5.2470760e-002 -7.4047200e-003 +v -5.2626360e-002 6.0043760e-002 -8.9566900e-003 +v -7.2598590e-002 9.7762720e-002 -1.4434510e-002 +v 4.4331260e-002 5.5818010e-002 -6.0362700e-003 +v 3.8463400e-002 5.4934820e-002 -6.1822500e-003 +v 3.8838620e-002 5.7808260e-002 -5.2584800e-003 +v -9.2015400e-003 5.9510130e-002 -3.4437110e-002 +v -3.5262560e-002 5.5284900e-002 -1.0545060e-002 +v -3.8336450e-002 5.4503540e-002 -1.0905320e-002 +v -1.7727540e-002 3.6289540e-002 5.2222250e-002 +v 5.0006490e-002 5.8095800e-002 -4.6211800e-003 +v 4.6133970e-002 5.9278810e-002 -4.7769600e-003 +v 1.5110300e-002 5.9819840e-002 -2.8645750e-002 +v 1.0312380e-002 5.7586530e-002 -2.9995250e-002 +v -6.1353400e-003 6.0256790e-002 -3.4695830e-002 +v -1.2318220e-002 5.9396390e-002 -3.5268510e-002 +v -1.4466910e-002 6.3136020e-002 -3.6865870e-002 +v -4.6650260e-002 5.9840950e-002 -1.2135840e-002 +v -5.6572080e-002 1.2480275e-001 -7.1885700e-003 +v -7.9237500e-002 1.2055419e-001 -5.6744800e-003 +v -7.9334790e-002 1.2560650e-001 -6.1175900e-003 +v 2.2340000e-002 5.8492230e-002 -2.6014120e-002 +v 7.6270400e-003 6.2098330e-002 -3.1135840e-002 +v 3.3101700e-003 6.0456840e-002 -3.2481070e-002 +v -1.6811880e-002 6.1275230e-002 -3.5929330e-002 +v -3.2491910e-002 5.7196350e-002 -1.2104730e-002 +v -3.4108240e-002 6.1466560e-002 -1.3053130e-002 +v -3.3896980e-002 5.7025330e-002 -1.1047570e-002 +v -3.8623580e-002 5.8303290e-002 -1.1505750e-002 +v -4.5008400e-002 6.2723940e-002 -1.3390450e-002 +v -5.6896010e-002 1.3398739e-001 -5.6270700e-003 +v -4.4853890e-002 1.5746031e-001 -8.6731600e-003 +v -7.8609550e-002 6.9656870e-002 1.1810740e-002 +v -2.3730020e-002 1.0186156e-001 -2.3836400e-002 +v -2.8122930e-002 9.9322390e-002 -2.3580130e-002 +v -5.0076720e-002 1.4997652e-001 -3.6419700e-003 +v -3.3048420e-002 9.5958590e-002 -2.3426460e-002 +v 1.9520390e-002 6.2064770e-002 -2.7292470e-002 +v -3.8864710e-002 1.0333987e-001 -2.0641400e-002 +v -4.8952940e-002 5.6281090e-002 -1.0220880e-002 +v -5.3993040e-002 1.4498656e-001 -1.1093400e-003 +v -4.5530560e-002 9.8510850e-002 -2.1729510e-002 +v -5.0910960e-002 1.0074570e-001 -2.1619430e-002 +v 2.3245830e-002 6.2792530e-002 -2.5047990e-002 +v 9.7412800e-003 6.3181400e-002 -3.1141370e-002 +v -8.6614000e-004 6.4559630e-002 -3.4490930e-002 +v -8.5264000e-003 6.4001730e-002 -3.5850480e-002 +v -4.8451500e-002 6.4794120e-002 -1.3029910e-002 +v -5.2325160e-002 1.0614813e-001 -1.9271240e-002 +v -5.5265350e-002 1.0216682e-001 -1.9897100e-002 +v -5.9042010e-002 9.9032210e-002 -1.9222950e-002 +v -5.7846760e-002 1.0433496e-001 -1.8525740e-002 +v -2.7113460e-002 1.7332156e-001 -1.8538890e-002 +v 2.2832000e-002 6.7082570e-002 -2.6297510e-002 +v 1.4519060e-002 6.4595540e-002 -2.9855690e-002 +v 1.1471330e-002 6.7581440e-002 -3.0901170e-002 +v -1.7739360e-002 6.6260830e-002 -3.7657310e-002 +v -6.5059750e-002 1.3452104e-001 -8.0899900e-003 +v -7.5829320e-002 1.4244605e-001 -5.8090000e-003 +v -4.1362350e-002 6.1637330e-002 -1.2813770e-002 +v -5.6147890e-002 6.1921550e-002 -5.7541100e-003 +v -6.2126110e-002 6.2845360e-002 -4.5202600e-003 +v -3.7292480e-002 1.6449057e-001 -1.3627050e-002 +v -1.9818920e-002 1.6509494e-001 -1.7608980e-002 +v 6.2881100e-003 6.5416350e-002 -3.2563040e-002 +v -5.9250500e-003 6.9515630e-002 -3.5933480e-002 +v -1.0538630e-002 6.7999180e-002 -3.6517060e-002 +v -3.5385700e-002 6.6817430e-002 -1.5434860e-002 +v -5.3994500e-002 6.4638700e-002 -9.3254900e-003 +v -6.3852310e-002 6.5572310e-002 -6.9393300e-003 +v -6.3920880e-002 1.2774242e-001 -8.5494600e-003 +v -2.6940700e-002 3.6184050e-002 5.3351850e-002 +v 1.9618650e-002 6.7007390e-002 -2.8356120e-002 +v 1.2275180e-002 6.9933940e-002 -3.1553160e-002 +v 5.4265100e-003 6.8247960e-002 -3.2730520e-002 +v -4.4084200e-003 6.6619200e-002 -3.4870250e-002 +v -2.1911350e-002 6.7144790e-002 -3.6535750e-002 +v -4.5643150e-002 1.5466949e-001 -7.2969400e-003 +v -5.1673460e-002 6.6850660e-002 -1.2120350e-002 +v -5.8105180e-002 6.6465950e-002 -1.0044340e-002 +v -5.6992260e-002 1.4311862e-001 -2.2403000e-003 +v -8.0651110e-002 1.3119854e-001 -4.4397800e-003 +v -5.6544310e-002 1.2850938e-001 -6.2014700e-003 +v 1.7758080e-002 7.0138540e-002 -2.9404680e-002 +v 6.4980500e-003 7.0791870e-002 -3.3525310e-002 +v 7.5831000e-004 7.0434460e-002 -3.4462560e-002 +v -1.3235950e-002 6.9292820e-002 -3.7917490e-002 +v -6.7390780e-002 1.1889688e-001 -8.7301400e-003 +v -3.8119520e-002 6.4162310e-002 -1.3829140e-002 +v 1.8527400e-003 1.1303356e-001 -1.9794270e-002 +v -7.5950810e-002 6.8170610e-002 1.8117970e-002 +v -1.0001990e-002 7.2671480e-002 -3.7661370e-002 +v -1.7976070e-002 7.0613770e-002 -3.8443880e-002 +v -2.3035990e-002 7.2778460e-002 -3.8072640e-002 +v -2.6120100e-002 7.1177480e-002 -3.5451530e-002 +v -6.8535420e-002 1.3929375e-001 -7.8046600e-003 +v -3.5263040e-002 7.1067650e-002 -1.8011860e-002 +v -4.1558180e-002 6.9774010e-002 -1.6774100e-002 +v -5.2831730e-002 7.0298920e-002 -1.4864960e-002 +v -6.6978850e-002 6.7638980e-002 -6.8094400e-003 +v -1.0244470e-002 1.7895826e-001 -2.9538870e-002 +v -7.5272650e-002 1.2680098e-001 -8.0241700e-003 +v -8.7359900e-002 1.1248315e-001 4.2049490e-002 +v 8.7503000e-003 7.4301560e-002 -3.3398210e-002 +v -6.4249520e-002 1.6045024e-001 -5.7041470e-002 +v -4.4354010e-002 7.3372220e-002 -1.7874430e-002 +v -4.5762580e-002 6.9445320e-002 -1.5928780e-002 +v -4.7957440e-002 7.2542990e-002 -1.6106990e-002 +v -5.7822630e-002 6.9538010e-002 -1.4416470e-002 +v -7.2071600e-002 7.1538150e-002 -7.4714400e-003 +v 2.5472930e-002 7.4094500e-002 -2.4938540e-002 +v 1.5719730e-002 7.3756350e-002 -2.9747770e-002 +v 4.8214000e-003 7.3763980e-002 -3.4552450e-002 +v -2.2528600e-003 7.3921320e-002 -3.5887190e-002 +v -7.3834900e-003 7.4799620e-002 -3.7223830e-002 +v -2.0225340e-002 7.7095190e-002 -3.9044290e-002 +v -3.4016180e-002 7.2101270e-002 -2.0823150e-002 +v -3.8493370e-002 7.2839870e-002 -1.7502230e-002 +v -6.4392550e-002 7.3116330e-002 -1.5335340e-002 +v -6.4480660e-002 7.0187350e-002 -1.2261750e-002 +v -2.3854330e-002 1.6164528e-001 -1.4504190e-002 +v 2.2104450e-002 7.2692600e-002 -2.6900140e-002 +v 1.5532370e-002 7.6586960e-002 -2.9606940e-002 +v 1.1574050e-002 7.4860570e-002 -3.1383860e-002 +v -1.4731560e-002 7.7640750e-002 -3.8490670e-002 +v -1.6018820e-002 7.4288800e-002 -3.8864420e-002 +v -5.1103620e-002 7.3071950e-002 -1.6243060e-002 +v -5.7989540e-002 7.4017880e-002 -1.7522320e-002 +v -6.9608380e-002 7.2322890e-002 -1.0934430e-002 +v -7.5996110e-002 1.1714132e-001 -6.5577200e-003 +v -3.7987660e-002 1.0751453e-001 -1.9975760e-002 +v 1.0696210e-002 7.9889200e-002 -3.2009580e-002 +v -5.3433400e-003 7.8264580e-002 -3.7476940e-002 +v -2.6081990e-002 7.6191290e-002 -3.6780200e-002 +v -3.9161040e-002 1.5718885e-001 -1.0580510e-002 +v -6.5609880e-002 7.5860010e-002 -1.6750060e-002 +v -7.0177600e-002 7.5663330e-002 -1.3839210e-002 +v -7.4291360e-002 7.4808360e-002 -9.3537900e-003 +v -6.3428890e-002 1.7185387e-001 -6.1412170e-002 +v 3.0684890e-002 7.5726870e-002 -2.0778090e-002 +v 1.9305010e-002 7.9017870e-002 -2.7743990e-002 +v -8.5992100e-003 7.9338730e-002 -3.7905180e-002 +v -2.3200110e-002 7.6568500e-002 -3.8386500e-002 +v -3.8117820e-002 7.6390120e-002 -1.8644360e-002 +v -4.4231130e-002 7.7664130e-002 -1.9026580e-002 +v -5.1025500e-002 7.5705070e-002 -1.8186900e-002 +v -7.0595130e-002 1.2994832e-001 -8.7629200e-003 +v 2.8147660e-002 7.8785370e-002 -2.2432450e-002 +v 7.6016000e-003 7.9435920e-002 -3.3714560e-002 +v 4.9502400e-003 7.8027250e-002 -3.4409750e-002 +v -1.5858350e-002 8.1165550e-002 -3.9185590e-002 +v -1.8502080e-002 8.3343870e-002 -3.9010720e-002 +v -7.9739350e-002 1.3606854e-001 -4.1482100e-003 +v -3.0980180e-002 1.6634656e-001 -1.6241160e-002 +v -3.5749800e-002 7.7248350e-002 -1.9374020e-002 +v -4.8944740e-002 7.9086360e-002 -1.9575700e-002 +v -5.5065860e-002 7.8089190e-002 -1.9755480e-002 +v 2.3706000e-002 8.0240410e-002 -2.5450120e-002 +v 1.2254110e-002 8.3456700e-002 -3.0771580e-002 +v 1.8549900e-003 8.4692790e-002 -3.4838500e-002 +v -2.0857000e-004 7.8941410e-002 -3.5782080e-002 +v -4.2710000e-004 8.2947370e-002 -3.6380660e-002 +v -4.4101600e-003 8.2794510e-002 -3.7467250e-002 +v -3.3202320e-002 1.0578320e-001 -2.0647590e-002 +v -3.9206970e-002 8.1536380e-002 -2.0571000e-002 +v -6.0355410e-002 7.9766610e-002 -1.9375540e-002 +v -4.1771830e-002 1.0396706e-001 -2.0832940e-002 +v -1.1204010e-002 8.2713320e-002 -3.8489610e-002 +v -2.3181500e-002 8.1686990e-002 -3.8329160e-002 +v -2.7233190e-002 8.0570950e-002 -3.6620670e-002 +v -3.5470180e-002 8.0196070e-002 -2.2325910e-002 +v -4.4864210e-002 8.1997900e-002 -2.0473520e-002 +v -5.0647890e-002 8.2309430e-002 -2.1365890e-002 +v -5.5522610e-002 8.1927600e-002 -2.1353790e-002 +v -8.8089610e-002 1.1135484e-001 1.8516150e-002 +v -7.2036080e-002 1.1107918e-001 4.5361400e-002 +v -3.3359780e-002 1.6986395e-001 -1.5448990e-002 +v -6.6839030e-002 6.2170510e-002 2.1576840e-002 +v 3.0730560e-002 8.1968990e-002 -2.0040460e-002 +v 1.6224320e-002 8.6480380e-002 -2.8952010e-002 +v -6.9855630e-002 1.0027892e-001 -1.4847830e-002 +v -6.3836170e-002 8.1704600e-002 -1.8908860e-002 +v -6.7914820e-002 8.0136290e-002 -1.7128200e-002 +v -4.5752080e-002 1.6340754e-001 -8.1780500e-003 +v 1.1727540e-002 8.8010780e-002 -3.0860110e-002 +v 7.3334800e-003 8.5270000e-002 -3.2829380e-002 +v -3.4356500e-003 8.7017890e-002 -3.6461000e-002 +v -2.6964110e-002 8.4512810e-002 -3.6361740e-002 +v -3.6553370e-002 8.5316190e-002 -2.2576200e-002 +v -3.8791090e-002 8.5232710e-002 -2.1917600e-002 +v -5.7676940e-002 8.6258340e-002 -2.1098320e-002 +v -6.2581810e-002 8.6394530e-002 -1.9169290e-002 +v -7.1395340e-002 1.2468846e-001 -8.5944200e-003 +v 1.4801570e-002 9.9040900e-002 -2.2842920e-002 +v -2.1162860e-002 1.7491852e-001 -2.1977110e-002 +v -1.4824250e-002 8.7288840e-002 -3.8317070e-002 +v -2.3285750e-002 8.9468030e-002 -3.6027250e-002 +v -5.1595650e-002 8.4422070e-002 -2.1600960e-002 +v -6.9481040e-002 8.5656460e-002 -1.7198420e-002 +v -7.0917210e-002 1.0754846e-001 -1.1496630e-002 +v 3.0145320e-002 8.6284000e-002 -2.0408140e-002 +v -5.5578110e-002 1.1567692e-001 -1.4645990e-002 +v -8.0981100e-003 8.9070080e-002 -3.6552200e-002 +v -8.1206310e-002 1.1205088e-001 -8.8299000e-004 +v -1.8772170e-002 8.9838040e-002 -3.6991710e-002 +v -2.1100420e-002 8.6587670e-002 -3.7849050e-002 +v -2.5809910e-002 8.8889590e-002 -3.5082250e-002 +v -4.8984800e-002 9.0731760e-002 -2.1817170e-002 +v -3.5874870e-002 3.4776000e-002 -3.0845200e-002 +v -3.3164390e-002 3.3606540e-002 -2.9721880e-002 +v -2.5964020e-002 3.3487000e-002 -2.6321120e-002 +v -1.6717530e-002 3.3611640e-002 -2.4625420e-002 +v -5.3486300e-003 3.3829010e-002 -2.2600430e-002 +v 6.4843500e-003 3.4293000e-002 -2.0854930e-002 +v 1.3950350e-002 3.4880000e-002 -1.8612870e-002 +v -4.2465980e-002 3.4189100e-002 -2.7260650e-002 +v -3.3241100e-002 3.3578760e-002 -2.6719450e-002 +v 6.2813500e-003 3.4165800e-002 -1.8764230e-002 +v -4.4265790e-002 3.3663660e-002 -2.1914420e-002 +v -2.3671460e-002 3.3630970e-002 -2.3217760e-002 +v -1.1558580e-002 3.3895430e-002 -2.1054260e-002 +v -2.0406400e-003 3.4053940e-002 -1.9331070e-002 +v 1.7323900e-003 3.4459660e-002 -1.6607870e-002 +v -2.7316070e-002 3.3910070e-002 -2.1353750e-002 +v -1.3371080e-002 3.4361580e-002 -1.9023720e-002 +v 9.5887300e-003 3.4207220e-002 -1.5424050e-002 +v -1.4981540e-002 3.5878180e-002 -1.7992380e-002 +v -2.3474300e-003 3.5903130e-002 -1.5929740e-002 +v 2.2544300e-003 3.6411540e-002 -1.4783970e-002 +v -3.5199130e-002 3.3835210e-002 -2.0508290e-002 +v -2.6075450e-002 3.5918600e-002 -1.9405170e-002 +v 8.2740600e-003 3.5645200e-002 -1.2648700e-002 +v 1.0473640e-002 3.4742600e-002 -1.1262870e-002 +v 1.4055380e-002 3.4483430e-002 -1.4495730e-002 +v -3.6970520e-002 3.5680360e-002 -1.5007790e-002 +v -2.4719500e-003 3.8408770e-002 -1.4159030e-002 +v -3.9481890e-002 3.3618220e-002 -2.3612470e-002 +v -4.1091510e-002 3.4006000e-002 -1.1997540e-002 +v -3.1589810e-002 3.5592330e-002 -1.9204150e-002 +v -2.0086310e-002 3.8064450e-002 -1.7220790e-002 +v -1.1113250e-002 3.8290290e-002 -1.5646360e-002 +v 4.4522600e-003 3.7705190e-002 -1.2957650e-002 +v 1.5870480e-002 3.4416230e-002 -2.9666500e-003 +v -4.7872000e-002 3.4136300e-002 -1.5418250e-002 +v -4.7521640e-002 3.3622720e-002 -1.2804590e-002 +v -3.3407340e-002 3.7577040e-002 -1.6158190e-002 +v -2.7851470e-002 3.8404330e-002 -1.7210420e-002 +v -8.5065300e-003 3.9028950e-002 -1.3000800e-002 +v 6.4552500e-003 3.8165190e-002 -1.0164860e-002 +v 7.4147100e-003 3.4659190e-002 -3.0116800e-003 +v 1.1966200e-002 3.4335400e-002 -5.9571300e-003 +v 2.0414820e-002 3.5567580e-002 -3.7806900e-003 +v -1.9288780e-002 3.8762570e-002 -1.4202620e-002 +v -1.1390100e-003 3.9176760e-002 -1.0381370e-002 +v 3.8149200e-003 3.9024470e-002 -8.0827300e-003 +v 7.5208200e-003 3.6733400e-002 -6.7614300e-003 +v 1.9968120e-002 3.4843990e-002 -1.8984900e-003 +v -4.5058400e-002 3.3600490e-002 -1.2527510e-002 +v -3.0754850e-002 3.8639810e-002 -1.4050770e-002 +v -5.1499810e-002 3.3729110e-002 -1.2082510e-002 +v -2.3756860e-002 3.8585750e-002 -1.1093270e-002 +v 3.9734700e-003 3.8208550e-002 -3.7963500e-003 +v 9.5485400e-003 3.4232620e-002 1.7162000e-003 +v 2.9086550e-002 3.5799990e-002 3.5630900e-003 +v -5.5965200e-002 3.3529910e-002 -9.1246200e-003 +v -1.9523510e-002 3.8505210e-002 -4.5434500e-003 +v 1.6363470e-002 3.4394790e-002 2.2948600e-003 +v 2.1324740e-002 3.4624040e-002 5.6444000e-003 +v -3.9670300e-002 3.6174000e-002 -7.3397700e-003 +v -1.4251730e-002 3.8648030e-002 -4.3030400e-003 +v 2.3262300e-003 3.5348200e-002 2.3246000e-003 +v 1.4014300e-002 3.5703800e-002 3.8878900e-003 +v 1.5322800e-002 3.6239700e-002 3.6628500e-003 +v 2.3753130e-002 3.4670710e-002 3.9885300e-003 +v 3.2369180e-002 3.5816010e-002 7.0246300e-003 +v -6.3715900e-002 3.3776930e-002 -8.0065600e-003 +v -6.4266880e-002 3.3562500e-002 -5.1253200e-003 +v -3.8066600e-002 3.8518600e-002 -7.3079600e-003 +v -9.4308800e-003 3.8887690e-002 -7.4848700e-003 +v 3.9677800e-003 3.4200210e-002 4.9754500e-003 +v 9.4292600e-003 3.6030400e-002 4.5275100e-003 +v 2.9859020e-002 3.4980130e-002 9.8349300e-003 +v -5.2730060e-002 3.3497900e-002 -1.8117500e-003 +v -4.1271000e-002 3.3855400e-002 -1.8800800e-003 +v -3.1105000e-003 3.8946190e-002 -2.7793900e-003 +v 6.2194100e-003 3.5134100e-002 6.5492800e-003 +v 2.0897900e-002 3.5937100e-002 8.7849000e-003 +v 3.5606010e-002 3.6526640e-002 9.8155300e-003 +v -6.7078340e-002 3.3840100e-002 -6.1688300e-003 +v -8.1140000e-004 3.7424170e-002 4.7721500e-003 +v 3.1492300e-003 3.4125310e-002 1.1762220e-002 +v 4.9172000e-003 3.3997100e-002 9.1666100e-003 +v 2.5130800e-002 3.4546910e-002 1.1012580e-002 +v 2.8248620e-002 3.5046370e-002 1.6016700e-002 +v -6.7032970e-002 6.5145960e-002 2.7292860e-002 +v -4.6380170e-002 3.3605230e-002 -8.9435000e-004 +v -3.3163400e-002 3.8195400e-002 -5.2520000e-004 +v -3.2074200e-002 3.8323400e-002 -4.2109000e-004 +v -2.1692690e-002 3.8266010e-002 4.5100800e-003 +v 2.3930750e-002 3.4816710e-002 1.7739160e-002 +v 4.2719120e-002 3.9977070e-002 8.9321600e-003 +v -5.8604080e-002 3.3462230e-002 -2.1667000e-004 +v -3.7314400e-002 3.3633000e-002 4.5724700e-003 +v -1.0423990e-002 3.8488570e-002 6.2292700e-003 +v -1.3896900e-003 3.8651360e-002 2.3966500e-003 +v -3.0845000e-004 3.5462480e-002 8.2607200e-003 +v -1.4089000e-003 3.6193080e-002 1.2944550e-002 +v 2.2252900e-002 3.6583300e-002 1.3979700e-002 +v -7.0961830e-002 3.4345730e-002 -7.8374000e-004 +v -6.9066180e-002 3.3717630e-002 -1.9761000e-004 +v -6.4825640e-002 3.3505860e-002 2.8222500e-003 +v -4.7059660e-002 3.3501860e-002 3.5646400e-003 +v -3.6953800e-003 3.8172780e-002 1.3046800e-002 +v 3.3475850e-002 3.6447340e-002 1.6266960e-002 +v 3.7249610e-002 3.7509920e-002 1.4815820e-002 +v -4.5675940e-002 3.3703640e-002 6.4300300e-003 +v -3.8639270e-002 3.3937310e-002 8.5506500e-003 +v -9.5064100e-003 3.8352640e-002 1.5570660e-002 +v 2.1499800e-002 3.5807100e-002 1.8169400e-002 +v 4.4876460e-002 4.1230990e-002 1.6008250e-002 +v -7.2474010e-002 3.6255930e-002 1.5532600e-003 +v -7.1498130e-002 3.4452970e-002 4.2026500e-003 +v -2.7790900e-002 3.8062900e-002 7.9376100e-003 +v -1.6556410e-002 3.8286470e-002 1.0215790e-002 +v 8.1043500e-003 3.4842900e-002 1.8134600e-002 +v 2.3589460e-002 3.5890600e-002 2.5337690e-002 +v 4.1261350e-002 4.0585070e-002 2.0751930e-002 +v -5.1350870e-002 3.3645700e-002 8.0329400e-003 +v -4.7104300e-002 3.5549500e-002 8.0803900e-003 +v -1.4103500e-003 3.6999940e-002 1.6982030e-002 +v 9.1714000e-004 3.4803380e-002 1.5634690e-002 +v 2.8887900e-003 3.4636250e-002 1.8849770e-002 +v 1.3279200e-002 3.4379500e-002 2.1423700e-002 +v 1.4322700e-002 3.4425500e-002 2.1593200e-002 +v 1.7490100e-002 3.4646300e-002 2.2040900e-002 +v 2.9868460e-002 3.6248820e-002 1.9872200e-002 +v -3.9222000e-002 3.6326200e-002 1.0789900e-002 +v -3.0307100e-002 3.3995400e-002 1.4706400e-002 +v 2.0081230e-002 3.5172700e-002 2.8018770e-002 +v 2.4989010e-002 3.8104580e-002 2.9429570e-002 +v 3.3584130e-002 3.8303930e-002 2.2928670e-002 +v 4.9015720e-002 4.4573630e-002 2.0659450e-002 +v -5.8225970e-002 6.6607310e-002 3.5050280e-002 +v -6.7330830e-002 3.3846440e-002 8.7266300e-003 +v -3.4692330e-002 3.3828710e-002 1.2438580e-002 +v -2.9803200e-002 3.4287000e-002 1.6353100e-002 +v 1.7023800e-003 3.6310890e-002 2.1179600e-002 +v 4.5137020e-002 4.4625440e-002 2.5516510e-002 +v -6.8876490e-002 1.1022176e-001 3.9004630e-002 +v -5.7680560e-002 3.3622690e-002 1.4040310e-002 +v -5.3210500e-002 3.3585300e-002 1.3987000e-002 +v -3.5711600e-002 3.5891600e-002 1.5502900e-002 +v -2.8861500e-002 3.5396700e-002 1.7350000e-002 +v -2.6580500e-002 3.7742600e-002 1.5705300e-002 +v -1.0974400e-003 3.8147840e-002 2.0427010e-002 +v 3.5047710e-002 4.0973940e-002 2.6970390e-002 +v -6.9685460e-002 3.4478780e-002 9.7984300e-003 +v -5.4019000e-002 3.3309900e-002 1.5848000e-002 +v 4.4816800e-003 3.7117830e-002 2.4755300e-002 +v 6.6605500e-003 3.5204730e-002 2.4315930e-002 +v 8.3833000e-003 3.4748700e-002 2.4057310e-002 +v 3.8883100e-002 4.1032980e-002 2.4976570e-002 +v -2.6441900e-003 3.8727070e-002 2.5131260e-002 +v 3.2222300e-003 3.8708440e-002 2.5898750e-002 +v 9.0016500e-003 3.6890930e-002 2.8482190e-002 +v 1.3196980e-002 3.4835790e-002 3.1630980e-002 +v 2.2291600e-002 3.7053310e-002 3.3101020e-002 +v 2.8948390e-002 3.9160020e-002 2.7234810e-002 +v -8.7773470e-002 1.1181412e-001 3.7144310e-002 +v -1.7870490e-002 3.8203890e-002 2.0243220e-002 +v 1.0087420e-002 3.7047690e-002 3.0822500e-002 +v 4.2296550e-002 4.5435770e-002 2.9040920e-002 +v -8.4341340e-002 1.1388013e-001 4.6513480e-002 +v -7.3795710e-002 1.0895629e-001 3.9217250e-002 +v -5.1243340e-002 6.4239200e-002 3.4258040e-002 +v -6.1777390e-002 3.4017860e-002 1.6900580e-002 +v -3.6665100e-002 3.5304200e-002 2.3032000e-002 +v -1.4930180e-002 3.8643510e-002 2.9378330e-002 +v -8.0894520e-002 1.0967225e-001 3.7910230e-002 +v -8.9822620e-002 1.1387199e-001 3.2845310e-002 +v -6.9655510e-002 6.8728370e-002 3.1127880e-002 +v -7.8449800e-002 1.0988832e-001 4.2517920e-002 +v -7.5824140e-002 1.0794900e-001 3.7128750e-002 +v -5.5740630e-002 3.4128050e-002 2.6674360e-002 +v -3.8279600e-002 3.5429000e-002 2.4380600e-002 +v -3.5283340e-002 3.4179780e-002 2.2744860e-002 +v -2.5798070e-002 3.7865000e-002 1.9981460e-002 +v 6.9064300e-003 3.9004270e-002 2.9548510e-002 +v 1.5448990e-002 3.4852440e-002 3.6984890e-002 +v 1.9128230e-002 3.5640640e-002 3.6642280e-002 +v -6.3664970e-002 6.6047840e-002 3.1828080e-002 +v 3.9604800e-002 4.4939530e-002 2.9992360e-002 +v -8.0294310e-002 7.1702430e-002 1.5995300e-002 +v -5.4185430e-002 6.7322700e-002 3.6935610e-002 +v -7.3110210e-002 1.4847168e-001 -2.8748470e-002 +v -5.8999980e-002 7.3751550e-002 4.1197080e-002 +v -5.9520730e-002 6.1040260e-002 -2.3753800e-003 +v -6.2791800e-002 3.4596760e-002 2.3505640e-002 +v -4.1895500e-002 3.3668300e-002 2.6940000e-002 +v 8.9808200e-003 3.7639400e-002 3.3900800e-002 +v 8.5287800e-003 3.4888000e-002 3.6265100e-002 +v -8.9803890e-002 1.1498106e-001 4.2771650e-002 +v -6.5545420e-002 7.4430370e-002 3.9168070e-002 +v -6.4644190e-002 6.1723230e-002 2.2552000e-004 +v 5.2496900e-003 3.9507100e-002 3.3271200e-002 +v 2.0250320e-002 3.7033170e-002 3.9327190e-002 +v -6.7006400e-002 6.3292870e-002 -1.7493900e-003 +v -6.4479770e-002 6.0651470e-002 4.2343200e-003 +v -5.7219630e-002 5.7000470e-002 4.9175800e-003 +v -7.4362810e-002 7.2437050e-002 3.1430040e-002 +v -6.2019000e-002 3.4343180e-002 3.1883280e-002 +v -4.6870820e-002 3.4444130e-002 3.0513130e-002 +v -2.0814280e-002 3.8400960e-002 2.7868430e-002 +v 1.6439350e-002 3.5635110e-002 4.1281040e-002 +v -6.9087160e-002 1.1205014e-001 4.5320060e-002 +v -7.1811570e-002 1.4861318e-001 -3.4639490e-002 +v -6.9538770e-002 6.3074750e-002 3.5758200e-003 +v -8.4863890e-002 7.8392100e-002 1.6462010e-002 +v -9.1188780e-002 1.1588893e-001 2.4705540e-002 +v -8.8827760e-002 1.1359169e-001 2.3873640e-002 +v -7.1302830e-002 1.1325363e-001 4.9444530e-002 +v -5.4876950e-002 7.0282330e-002 3.8828200e-002 +v -7.7208880e-002 1.0715887e-001 3.4738290e-002 +v -6.1241780e-002 5.9007440e-002 8.0916600e-003 +v -6.5885650e-002 3.5025080e-002 2.9416520e-002 +v -5.7889430e-002 3.4419570e-002 3.6265760e-002 +v -5.1847710e-002 3.4470270e-002 3.4635180e-002 +v -3.4834600e-002 3.4721400e-002 3.4578200e-002 +v -3.0984700e-002 3.8191900e-002 3.2390100e-002 +v -4.9613100e-003 3.9364900e-002 3.6702200e-002 +v 1.2224170e-002 3.5177480e-002 4.2620580e-002 +v -7.4898220e-002 1.1458863e-001 5.0776480e-002 +v -8.0469100e-002 1.1357963e-001 4.6643440e-002 +v -7.4107560e-002 6.9586030e-002 2.7264400e-002 +v -7.9002620e-002 7.6339320e-002 2.9248090e-002 +v -6.5297080e-002 3.4778970e-002 3.3744340e-002 +v -3.3656400e-002 3.4344100e-002 3.6914100e-002 +v 4.9318500e-003 3.4814800e-002 4.3462110e-002 +v 1.1347440e-002 3.6213020e-002 4.4652280e-002 +v -6.0569260e-002 7.1154540e-002 3.8653760e-002 +v -8.8979470e-002 1.1450869e-001 2.8446030e-002 +v -6.8543520e-002 6.1090480e-002 1.0557760e-002 +v -8.2710960e-002 1.1648975e-001 4.8518530e-002 +v -4.1913210e-002 3.4467720e-002 3.3200040e-002 +v -1.1289800e-002 3.9529200e-002 3.8844100e-002 +v -2.8261900e-003 3.4885340e-002 4.5611410e-002 +v -6.4561210e-002 5.9484140e-002 1.3061680e-002 +v -5.8581440e-002 5.7801460e-002 1.3429540e-002 +v -2.3320000e-002 3.9169500e-002 3.8473300e-002 +v -1.8159900e-002 3.9322300e-002 3.9402900e-002 +v -1.6471400e-002 3.4812800e-002 4.3684700e-002 +v 3.2906600e-003 3.5833470e-002 4.6024610e-002 +v -8.5229630e-002 1.1200712e-001 3.0416940e-002 +v -8.5644730e-002 1.1131719e-001 3.4234780e-002 +v -7.4530360e-002 6.6680690e-002 4.6953300e-003 +v -7.1112970e-002 6.2751470e-002 8.7995500e-003 +v -6.1149380e-002 5.8834410e-002 1.6539440e-002 +v -4.6912270e-002 3.4627180e-002 3.9739710e-002 +v -4.0760350e-002 3.4668230e-002 4.0492530e-002 +v -2.6323100e-002 3.4658000e-002 4.3473500e-002 +v -3.1836600e-003 3.6229910e-002 4.7873100e-002 +v -7.9940490e-002 1.0916678e-001 3.4119800e-002 +v -5.9712170e-002 6.3165280e-002 2.8789180e-002 +v -5.1176600e-002 6.8061880e-002 3.7398330e-002 +v -5.0126580e-002 7.0933150e-002 3.9481010e-002 +v -7.2790130e-002 6.4399880e-002 1.5205950e-002 +v -6.8511230e-002 6.1214650e-002 1.5354080e-002 +v -3.9343210e-002 3.5440180e-002 4.2492560e-002 +v -8.1305900e-003 3.5008350e-002 4.7502400e-002 +v -6.6080670e-002 7.0202740e-002 3.5552860e-002 +v -6.8602600e-002 1.4992277e-001 -4.0051350e-002 +v -7.1722100e-002 6.7023040e-002 2.4959750e-002 +v -7.5115010e-002 6.6557040e-002 1.0244090e-002 +v -6.5146650e-002 3.5945650e-002 3.9775080e-002 +v -3.6898600e-002 3.5924640e-002 4.4794170e-002 +v -9.4780400e-003 3.5977600e-002 4.9434210e-002 +v -8.5175960e-002 1.1706809e-001 4.8139420e-002 +v -6.3366400e-002 6.2790260e-002 2.5647610e-002 +v -6.6633330e-002 6.1001700e-002 1.8101240e-002 +v -5.8167590e-002 5.9985190e-002 2.2606060e-002 +v -6.4212210e-002 3.4992560e-002 3.9401920e-002 +v -5.3425790e-002 3.4560020e-002 4.2782420e-002 +v -1.8031490e-002 3.4859970e-002 4.9264760e-002 +v -1.1440410e-002 3.7640770e-002 5.0275730e-002 +v -7.5165320e-002 1.1154286e-001 4.6707180e-002 +v -7.7168390e-002 6.9826450e-002 5.0605600e-003 +v -7.2801360e-002 6.4382590e-002 1.2089080e-002 +v -7.8022000e-002 7.0995160e-002 2.1322150e-002 +v -6.1263370e-002 3.4690410e-002 4.1994900e-002 +v -5.4403750e-002 3.5007310e-002 4.4874590e-002 +v -4.5754280e-002 3.5206980e-002 4.3518120e-002 +v -3.3832440e-002 3.5168820e-002 4.6957890e-002 +v -2.8657630e-002 3.5083380e-002 5.0549440e-002 +v -1.5306440e-002 3.5246410e-002 5.0133810e-002 +v -6.5283650e-002 1.5592447e-001 -4.9865930e-002 +v -6.6467860e-002 1.4871539e-001 -3.1579300e-002 +v -6.2095980e-002 1.6388324e-001 -5.8385930e-002 +v -6.3274890e-002 1.5245731e-001 -3.2221730e-002 +v -4.3755720e-002 1.4773408e-001 -2.1433200e-003 +v -6.5696940e-002 1.4561631e-001 -1.8974710e-002 +v -6.6713650e-002 1.5358824e-001 -4.9097100e-002 +v -1.0482810e-002 1.6668287e-001 -2.1746090e-002 +v -6.2744510e-002 1.6397531e-001 -5.9398280e-002 +v -7.0413230e-002 1.4129200e-001 -8.4590800e-003 +v -6.1530380e-002 1.4037628e-001 -6.2734700e-003 +v -1.1452460e-002 1.7220633e-001 -2.6844980e-002 +v -6.3731140e-002 1.6577037e-001 -6.0103610e-002 +v -2.8218820e-002 1.5758144e-001 -1.0999490e-002 +v -1.8471270e-002 1.5967716e-001 -1.1169510e-002 +v -6.6700710e-002 1.5236775e-001 -4.5266390e-002 +v -4.9896410e-002 1.4670859e-001 -1.8614200e-003 +v -3.1449640e-002 1.5460463e-001 -7.6802300e-003 +v -6.7447660e-002 1.5507675e-001 -5.1594250e-002 +v -1.0906650e-002 1.7649301e-001 -2.9246300e-002 +v -7.2083600e-002 1.4965550e-001 -3.9265860e-002 +v -6.4230830e-002 1.4877806e-001 -2.5899710e-002 +v -6.3056640e-002 1.4341650e-001 -7.4907700e-003 +v -5.3043350e-002 1.4092550e-001 -4.7408000e-004 +v -3.9269410e-002 1.5205232e-001 -6.6203800e-003 +v -6.4796930e-002 1.5210615e-001 -3.6185520e-002 +v -6.4400320e-002 1.5834400e-001 -5.4256370e-002 +v -6.6178120e-002 1.4218350e-001 -9.3766300e-003 +v -6.7751430e-002 1.4605207e-001 -2.3333300e-002 +v -6.4731580e-002 1.5410067e-001 -4.0464820e-002 +v -2.4265590e-002 1.5687690e-001 -7.8509300e-003 +v -1.5723180e-002 1.6312344e-001 -1.6396570e-002 +v -7.0887660e-002 1.4404618e-001 -1.4908480e-002 +v -4.4341830e-002 1.5113809e-001 -5.6859800e-003 +v -6.2896810e-002 1.4694778e-001 -1.3098620e-002 +v -6.3755400e-002 1.4428875e-001 -1.1395730e-002 +v -6.8214560e-002 1.4390932e-001 -1.4984170e-002 +v -5.0271440e-002 1.4336563e-001 1.5153000e-003 +v -2.8535590e-002 1.6208479e-001 -1.4786030e-002 +v -6.5810700e-002 1.4359119e-001 -1.2585380e-002 +v -5.6179200e-002 1.3774406e-001 -4.0674300e-003 +v -6.8866880e-002 1.4723338e-001 -2.8739870e-002 +v -6.0965420e-002 1.7002113e-001 -6.0839390e-002 +v -1.3895490e-002 1.6787168e-001 -2.1897230e-002 +v -6.9413000e-002 1.5121847e-001 -4.4538540e-002 +v -5.5039800e-002 5.7309700e-002 1.6990900e-002 +f 1069 1647 1578 +f 1058 909 939 +f 421 1176 238 +f 1055 1101 1042 +f 238 1059 1126 +f 1254 30 1261 +f 1065 1071 1 +f 1037 1130 1120 +f 1570 2381 1585 +f 2434 2502 2473 +f 1632 1654 1646 +f 1144 1166 669 +f 1202 1440 305 +f 1071 1090 1 +f 1555 1570 1584 +f 1184 1174 404 +f 65 432 12 +f 1032 1085 574 +f 1789 2207 2223 +f 1154 1118 1184 +f 1141 1086 1154 +f 99 1117 342 +f 404 1174 419 +f 489 2000 1998 +f 1118 1174 1184 +f 1196 403 136 +f 1495 717 1490 +f 1804 402 1207 +f 2272 1398 891 +f 1100 1002 804 +f 1596 1595 2381 +f 208 420 1207 +f 402 208 1207 +f 1455 1935 1925 +f 1176 1059 238 +f 1150 1040 348 +f 1957 1537 2051 +f 1124 1189 939 +f 1804 1207 1823 +f 1381 1300 1109 +f 383 384 1182 +f 1085 1086 1141 +f 1040 1046 132 +f 220 1495 1188 +f 420 261 1207 +f 261 420 1065 +f 1055 1133 1101 +f 1054 421 403 +f 182 1109 2 +f 1181 1207 320 +f 545 1570 1561 +f 35 342 432 +f 1024 574 1141 +f 432 342 12 +f 1489 1081 1547 +f 1181 320 1805 +f 1516 1683 1507 +f 357 1117 1047 +f 1561 1570 1555 +f 1090 1196 1206 +f 1047 1203 1051 +f 1165 202 1121 +f 1099 341 301 +f 1174 240 419 +f 922 921 833 +f 1121 1080 385 +f 815 21 1183 +f 35 99 342 +f 1083 398 262 +f 106 94 1317 +f 94 292 1317 +f 292 95 1317 +f 940 1039 1033 +f 1300 1306 433 +f 21 212 471 +f 1120 1131 1037 +f 833 921 688 +f 1117 357 342 +f 106 271 94 +f 386 227 1375 +f 1130 1044 1053 +f 419 240 219 +f 1255 1244 32 +f 1557 1081 1489 +f 2062 2120 2109 +f 2034 2110 430 +f 23 315 1111 +f 291 94 271 +f 291 292 94 +f 50 386 95 +f 964 734 665 +f 1616 1585 1611 +f 445 1084 402 +f 574 1085 1141 +f 1654 341 1653 +f 220 1188 1640 +f 342 69 12 +f 417 261 328 +f 292 50 95 +f 204 227 386 +f 50 204 386 +f 1276 1471 1311 +f 1206 1196 136 +f 1033 1055 1042 +f 1037 1044 1130 +f 1180 320 417 +f 1121 202 1080 +f 325 203 271 +f 291 76 292 +f 292 237 50 +f 2159 1696 1767 +f 583 929 850 +f 1584 1585 1616 +f 1495 1490 1188 +f 1557 1489 1660 +f 1078 1069 1494 +f 1972 1992 1971 +f 183 1226 2000 +f 325 429 203 +f 292 76 237 +f 1152 227 1143 +f 1488 1412 1489 +f 1638 1646 1653 +f 1947 1869 2468 +f 203 306 291 +f 306 76 291 +f 237 248 50 +f 204 1143 227 +f 2395 14 429 +f 1502 881 2500 +f 1 1090 202 +f 1652 1653 1099 +f 2117 1863 2496 +f 50 248 204 +f 160 792 994 +f 884 888 857 +f 544 2117 2496 +f 1090 1206 202 +f 2463 879 2492 +f 429 306 203 +f 498 188 418 +f 865 884 857 +f 994 998 1014 +f 884 897 888 +f 1795 948 1802 +f 208 1035 1071 +f 1065 1 1066 +f 377 435 1377 +f 304 429 14 +f 304 306 429 +f 73 60 74 +f 248 592 204 +f 846 2264 829 +f 897 912 906 +f 1004 991 992 +f 1422 1421 1233 +f 980 10 303 +f 1058 922 909 +f 2436 2449 2418 +f 394 435 377 +f 435 475 446 +f 475 474 446 +f 336 337 361 +f 338 235 372 +f 624 148 129 +f 812 306 596 +f 1726 992 1019 +f 945 1514 1511 +f 1069 1627 1628 +f 1812 1823 1181 +f 1165 1121 169 +f 447 475 435 +f 2487 2458 901 +f 42 59 46 +f 401 7 187 +f 1010 970 797 +f 1513 220 1640 +f 2474 2491 2462 +f 594 307 1014 +f 398 1513 1640 +f 307 594 1026 +f 545 2381 1570 +f 403 421 238 +f 445 402 127 +f 1611 1631 1616 +f 1805 1180 1148 +f 394 447 435 +f 2341 2413 2376 +f 75 74 60 +f 541 47 42 +f 47 59 42 +f 541 42 28 +f 917 931 1103 +f 897 906 883 +f 2484 2068 779 +f 888 883 857 +f 261 1065 328 +f 363 1307 349 +f 377 363 394 +f 444 747 464 +f 323 338 362 +f 92 116 74 +f 592 634 97 +f 982 1027 1004 +f 1020 982 1004 +f 1084 1054 1035 +f 208 402 1084 +f 421 1119 1176 +f 1207 1181 1823 +f 1179 1187 1160 +f 263 296 1343 +f 1298 296 1307 +f 1307 296 349 +f 405 363 349 +f 405 394 363 +f 405 447 394 +f 362 372 384 +f 338 372 362 +f 983 1004 987 +f 122 134 139 +f 415 440 414 +f 75 92 74 +f 226 186 246 +f 796 787 700 +f 1119 1059 1176 +f 122 114 91 +f 624 129 116 +f 641 558 631 +f 1311 1318 1487 +f 100 1162 1170 +f 1653 341 1099 +f 1316 1983 273 +f 263 277 296 +f 296 358 349 +f 436 447 405 +f 109 554 570 +f 504 1385 2501 +f 115 122 91 +f 2068 2460 779 +f 43 777 163 +f 378 405 349 +f 358 378 349 +f 448 447 436 +f 448 476 447 +f 78 77 108 +f 75 60 47 +f 1764 2481 1795 +f 717 714 1512 +f 1490 717 1501 +f 238 1126 168 +f 1878 1866 826 +f 2025 2360 2367 +f 251 278 263 +f 278 277 263 +f 277 318 296 +f 296 318 358 +f 318 350 358 +f 378 436 405 +f 384 372 1182 +f 454 440 415 +f 987 1004 992 +f 493 476 448 +f 323 788 338 +f 403 238 136 +f 1565 1503 1474 +f 297 277 278 +f 297 318 277 +f 358 350 378 +f 378 388 436 +f 476 493 500 +f 73 105 60 +f 323 337 312 +f 953 1573 2358 +f 142 161 119 +f 454 443 440 +f 1862 1871 1405 +f 297 319 318 +f 560 47 541 +f 170 1323 111 +f 357 1047 1050 +f 1119 98 1059 +f 1838 1877 1900 +f 2359 230 251 +f 350 364 378 +f 449 448 436 +f 449 493 448 +f 185 186 226 +f 443 469 479 +f 874 165 2480 +f 463 444 464 +f 64 105 91 +f 1182 440 1129 +f 1958 1651 2502 +f 1238 2034 191 +f 251 279 278 +f 278 279 297 +f 364 388 378 +f 483 493 449 +f 134 148 139 +f 244 268 259 +f 910 942 930 +f 105 115 91 +f 24 30 18 +f 1132 487 1059 +f 1869 1947 2021 +f 2497 2494 2463 +f 2359 2385 230 +f 230 280 251 +f 251 280 279 +f 279 308 297 +f 297 308 319 +f 319 364 318 +f 364 350 318 +f 388 395 436 +f 436 395 449 +f 493 472 500 +f 122 129 134 +f 125 142 124 +f 373 400 393 +f 24 557 30 +f 2264 2278 2251 +f 1261 30 1269 +f 1730 1862 1877 +f 252 280 230 +f 343 364 319 +f 364 343 388 +f 63 64 91 +f 399 393 416 +f 416 444 463 +f 162 189 142 +f 768 373 326 +f 189 661 177 +f 189 199 661 +f 847 887 864 +f 533 747 444 +f 1744 1022 1418 +f 1170 524 729 +f 121 1342 128 +f 1236 1244 26 +f 280 281 279 +f 281 308 279 +f 343 319 308 +f 343 365 388 +f 388 365 395 +f 365 406 395 +f 406 449 395 +f 483 477 493 +f 477 491 472 +f 493 477 472 +f 78 109 77 +f 166 174 196 +f 481 150 814 +f 63 59 64 +f 326 373 393 +f 643 260 43 +f 230 253 252 +f 449 441 483 +f 441 477 483 +f 415 416 463 +f 226 246 245 +f 464 470 454 +f 323 362 337 +f 52 37 1283 +f 253 281 252 +f 281 280 252 +f 309 308 281 +f 330 343 308 +f 366 365 343 +f 441 449 406 +f 464 814 15 +f 883 906 887 +f 337 362 371 +f 479 498 290 +f 247 746 1003 +f 25 37 557 +f 640 930 669 +f 2486 2499 2459 +f 309 330 308 +f 343 330 366 +f 441 437 477 +f 290 498 418 +f 124 119 108 +f 77 124 108 +f 589 125 109 +f 570 589 109 +f 125 162 142 +f 1045 433 1034 +f 1207 261 320 +f 2004 2474 2495 +f 1215 1228 2285 +f 365 396 406 +f 396 422 406 +f 422 437 441 +f 406 422 441 +f 59 47 60 +f 51 78 66 +f 361 371 383 +f 196 215 214 +f 463 454 415 +f 27 41 535 +f 53 1283 37 +f 84 1299 1283 +f 1805 320 1180 +f 254 253 222 +f 254 281 253 +f 309 366 330 +f 396 365 366 +f 456 477 437 +f 484 491 477 +f 2480 2485 2493 +f 418 188 187 +f 53 85 1283 +f 85 84 1283 +f 420 1071 1065 +f 264 281 254 +f 298 309 281 +f 368 366 367 +f 368 396 366 +f 1639 1564 1139 +f 560 48 47 +f 82 471 212 +f 25 38 37 +f 202 1206 1080 +f 264 298 281 +f 298 331 309 +f 309 331 366 +f 331 367 366 +f 396 368 422 +f 422 456 437 +f 491 1192 313 +f 1699 2064 1710 +f 462 443 479 +f 371 362 384 +f 2502 2476 2464 +f 371 384 383 +f 21 732 212 +f 1571 1629 1627 +f 38 39 53 +f 37 38 53 +f 39 85 53 +f 1173 1184 404 +f 1006 2142 1674 +f 201 255 254 +f 255 264 254 +f 368 407 422 +f 450 456 422 +f 450 484 456 +f 456 484 477 +f 314 1192 491 +f 2027 2501 2489 +f 2475 2471 2488 +f 551 492 732 +f 464 481 814 +f 1081 1494 1547 +f 201 231 255 +f 407 450 422 +f 484 494 491 +f 494 327 491 +f 327 314 491 +f 876 797 995 +f 847 856 829 +f 125 143 162 +f 134 129 148 +f 1564 1571 1627 +f 417 320 261 +f 328 1065 1066 +f 170 156 201 +f 156 231 201 +f 231 282 255 +f 282 264 255 +f 450 485 484 +f 484 485 494 +f 2463 2486 2479 +f 159 185 167 +f 492 68 212 +f 732 492 212 +f 68 82 212 +f 1311 1471 1296 +f 101 156 111 +f 332 264 282 +f 332 298 264 +f 332 331 298 +f 331 332 367 +f 407 423 450 +f 450 423 485 +f 804 1002 1443 +f 2484 779 946 +f 689 443 462 +f 440 689 1129 +f 166 167 174 +f 38 31 39 +f 112 145 101 +f 101 145 156 +f 156 256 231 +f 332 423 368 +f 367 332 368 +f 368 423 407 +f 946 779 920 +f 1432 1261 1449 +f 461 478 453 +f 464 15 470 +f 31 54 39 +f 39 54 85 +f 86 101 85 +f 145 210 156 +f 282 283 332 +f 283 369 332 +f 369 423 332 +f 423 408 485 +f 854 876 965 +f 78 108 66 +f 440 443 689 +f 374 2465 961 +f 929 519 979 +f 54 86 85 +f 156 241 256 +f 256 282 231 +f 256 283 282 +f 389 423 369 +f 389 408 423 +f 408 457 485 +f 457 49 485 +f 485 49 494 +f 494 135 327 +f 175 83 314 +f 1167 1140 1483 +f 196 174 215 +f 697 16 68 +f 1038 82 16 +f 140 117 141 +f 1654 1653 1646 +f 1234 54 31 +f 86 112 101 +f 210 241 156 +f 923 917 911 +f 697 34 16 +f 145 193 210 +f 256 265 283 +f 265 310 283 +f 283 310 369 +f 310 344 369 +f 344 370 369 +f 370 389 369 +f 409 408 389 +f 409 466 408 +f 466 457 408 +f 466 49 457 +f 49 135 494 +f 174 225 215 +f 1014 766 602 +f 826 2220 2215 +f 1078 1494 1081 +f 1273 70 86 +f 120 112 86 +f 146 145 112 +f 146 193 145 +f 265 256 241 +f 223 265 241 +f 486 49 466 +f 175 327 135 +f 105 122 115 +f 480 15 681 +f 225 234 215 +f 731 34 697 +f 86 54 1273 +f 70 120 86 +f 193 241 210 +f 299 310 265 +f 310 333 344 +f 344 351 370 +f 424 466 409 +f 135 49 175 +f 214 215 234 +f 48 75 47 +f 34 9 1038 +f 16 34 1038 +f 203 291 271 +f 9 558 754 +f 1195 397 1120 +f 120 146 112 +f 146 194 193 +f 266 265 223 +f 266 299 265 +f 299 333 310 +f 333 351 344 +f 382 383 392 +f 399 416 415 +f 266 333 299 +f 351 352 370 +f 424 486 466 +f 487 175 49 +f 7 117 187 +f 1182 414 440 +f 41 42 46 +f 290 289 497 +f 2502 2464 2473 +f 372 399 414 +f 1570 1585 1584 +f 1066 1 1165 +f 1 202 1165 +f 120 70 102 +f 157 146 120 +f 194 223 193 +f 223 241 193 +f 352 379 370 +f 370 379 389 +f 410 409 389 +f 2478 1409 1958 +f 806 945 1002 +f 157 194 146 +f 267 266 223 +f 267 333 266 +f 379 410 389 +f 410 438 409 +f 438 424 409 +f 190 205 143 +f 337 371 361 +f 2215 830 826 +f 1631 1646 1638 +f 102 157 120 +f 157 195 194 +f 195 223 194 +f 195 211 223 +f 223 211 267 +f 267 300 333 +f 300 334 351 +f 333 300 351 +f 351 334 352 +f 410 411 438 +f 438 486 424 +f 487 49 486 +f 875 594 989 +f 108 581 66 +f 225 245 244 +f 312 336 335 +f 151 754 107 +f 274 1386 300 +f 352 334 379 +f 923 1729 1096 +f 244 245 268 +f 463 464 454 +f 414 399 415 +f 15 480 470 +f 1647 1069 1078 +f 909 922 833 +f 387 417 328 +f 133 157 102 +f 1314 133 102 +f 133 195 157 +f 1148 1179 1160 +f 1046 1167 182 +f 379 411 410 +f 792 339 229 +f 391 7 668 +f 185 226 174 +f 461 290 497 +f 2027 504 2501 +f 1196 1054 403 +f 728 1019 752 +f 2459 2483 2461 +f 1291 1264 55 +f 133 1356 195 +f 195 1356 211 +f 412 438 411 +f 4 486 438 +f 458 4 438 +f 4 487 486 +f 1720 1572 1771 +f 245 275 268 +f 1869 2021 2059 +f 235 399 372 +f 64 60 105 +f 836 2492 879 +f 1315 133 1314 +f 1331 1382 1356 +f 1310 926 1128 +f 7 1121 117 +f 119 161 611 +f 380 379 334 +f 379 380 411 +f 467 4 458 +f 495 487 4 +f 495 1126 487 +f 416 400 533 +f 479 469 498 +f 74 116 73 +f 478 461 497 +f 393 400 416 +f 61 1291 55 +f 505 1999 2474 +f 1999 2491 2474 +f 199 189 36 +f 1164 1165 169 +f 1179 387 249 +f 390 411 380 +f 411 390 412 +f 458 438 412 +f 495 168 1126 +f 480 469 470 +f 116 122 105 +f 418 187 140 +f 185 174 167 +f 166 148 167 +f 470 469 443 +f 40 55 32 +f 61 71 1291 +f 71 103 1291 +f 1184 1173 1154 +f 634 514 97 +f 425 458 412 +f 917 923 931 +f 2472 2489 853 +f 754 641 567 +f 44 567 1163 +f 454 470 443 +f 40 32 1249 +f 33 40 1249 +f 56 55 40 +f 56 61 55 +f 451 1265 439 +f 1180 417 1179 +f 1099 301 1077 +f 1189 1058 939 +f 1059 221 1132 +f 598 1074 1075 +f 412 426 425 +f 650 186 185 +f 234 244 259 +f 226 245 225 +f 1033 1042 1030 +f 2492 836 247 +f 7 169 1121 +f 1462 1322 1482 +f 425 467 458 +f 496 4 467 +f 1751 2468 2480 +f 290 418 140 +f 326 789 762 +f 142 177 161 +f 165 1751 2480 +f 87 103 71 +f 103 87 104 +f 1180 1179 1148 +f 417 387 1179 +f 2081 2060 2031 +f 1154 1173 1141 +f 181 131 197 +f 442 425 426 +f 614 144 143 +f 876 1010 797 +f 40 45 56 +f 56 45 61 +f 87 71 61 +f 1563 1437 1590 +f 1121 385 117 +f 1148 1160 1137 +f 1449 1459 1439 +f 1028 2462 929 +f 442 459 425 +f 459 467 425 +f 168 495 4 +f 496 168 4 +f 1763 1403 1444 +f 140 187 117 +f 244 234 225 +f 246 740 269 +f 372 414 1182 +f 40 547 45 +f 45 62 61 +f 62 87 61 +f 87 88 104 +f 1084 517 1054 +f 387 328 1064 +f 2467 2497 2485 +f 286 1363 302 +f 205 189 162 +f 290 140 289 +f 214 234 224 +f 393 399 809 +f 315 1131 397 +f 302 321 353 +f 1164 169 391 +f 427 459 442 +f 217 496 467 +f 217 168 496 +f 978 969 2074 +f 361 383 382 +f 269 276 245 +f 1440 11 305 +f 62 88 87 +f 328 1066 1064 +f 1066 1165 1164 +f 242 287 302 +f 1363 242 302 +f 287 321 302 +f 1179 249 1187 +f 983 1020 1004 +f 464 747 481 +f 788 323 276 +f 269 245 246 +f 88 89 1325 +f 171 172 242 +f 360 353 321 +f 360 1354 353 +f 1057 1064 1164 +f 2184 2188 2183 +f 460 459 451 +f 460 467 459 +f 149 168 217 +f 149 136 168 +f 116 129 122 +f 109 124 77 +f 159 167 148 +f 28 42 41 +f 57 88 62 +f 45 57 62 +f 1336 1325 89 +f 89 72 1336 +f 147 172 171 +f 172 258 242 +f 258 257 242 +f 257 287 242 +f 257 321 287 +f 345 360 321 +f 360 381 1354 +f 1069 938 1655 +f 387 473 249 +f 270 217 467 +f 130 136 149 +f 851 847 829 +f 983 987 975 +f 189 177 142 +f 88 72 89 +f 184 258 172 +f 257 288 321 +f 1265 451 459 +f 270 149 217 +f 226 225 174 +f 27 28 41 +f 109 125 124 +f 547 57 45 +f 57 58 88 +f 88 58 72 +f 2476 2484 2458 +f 147 184 172 +f 184 213 258 +f 258 243 257 +f 243 288 257 +f 345 321 288 +f 391 169 7 +f 468 460 451 +f 468 488 460 +f 270 467 460 +f 488 270 460 +f 1206 136 130 +f 481 793 150 +f 143 205 162 +f 142 119 124 +f 58 90 72 +f 90 128 72 +f 147 173 184 +f 173 213 184 +f 213 233 258 +f 258 233 243 +f 354 360 345 +f 354 381 360 +f 1026 991 307 +f 268 312 259 +f 1206 130 1080 +f 116 105 73 +f 139 148 166 +f 275 312 268 +f 188 401 187 +f 2479 2459 2461 +f 58 63 90 +f 1064 1066 1164 +f 1064 473 387 +f 288 311 345 +f 311 354 345 +f 996 994 307 +f 452 468 439 +f 452 478 468 +f 478 488 468 +f 141 130 149 +f 1564 1639 1563 +f 547 41 57 +f 2081 2107 2060 +f 382 381 354 +f 497 270 488 +f 289 149 270 +f 289 141 149 +f 114 122 139 +f 59 60 64 +f 275 323 312 +f 401 668 7 +f 41 46 57 +f 57 46 58 +f 1459 1345 1269 +f 1342 121 158 +f 166 173 158 +f 213 224 233 +f 233 259 243 +f 243 322 288 +f 322 311 288 +f 453 478 452 +f 497 289 270 +f 912 911 906 +f 276 323 275 +f 276 275 245 +f 46 63 58 +f 90 121 128 +f 173 214 213 +f 213 214 224 +f 259 322 243 +f 336 311 322 +f 336 354 311 +f 361 382 354 +f 1043 439 1290 +f 497 488 478 +f 385 130 141 +f 385 1080 130 +f 144 190 143 +f 535 41 547 +f 121 166 158 +f 335 336 322 +f 354 336 361 +f 2004 2481 1764 +f 698 439 1043 +f 289 140 141 +f 923 1096 931 +f 650 185 159 +f 46 59 63 +f 63 91 90 +f 90 114 121 +f 121 139 166 +f 173 196 214 +f 259 335 322 +f 2478 2502 2434 +f 312 337 336 +f 90 91 114 +f 114 139 121 +f 166 196 173 +f 224 234 233 +f 234 259 233 +f 259 312 335 +f 1124 916 1189 +f 542 541 530 +f 462 479 290 +f 269 783 276 +f 813 567 641 +f 276 783 788 +f 82 1038 1333 +f 816 701 703 +f 672 137 603 +f 625 635 624 +f 2457 2439 1973 +f 767 533 529 +f 2468 1869 2480 +f 662 190 639 +f 711 720 719 +f 630 639 614 +f 161 654 638 +f 781 991 982 +f 1227 31 516 +f 648 639 630 +f 630 614 590 +f 2098 544 1899 +f 578 579 586 +f 697 492 551 +f 529 533 400 +f 869 859 870 +f 1732 924 914 +f 1004 1027 991 +f 801 591 603 +f 636 676 651 +f 876 949 965 +f 2207 1789 1859 +f 76 739 237 +f 188 681 15 +f 578 604 599 +f 797 616 995 +f 510 2035 1365 +f 76 812 617 +f 617 739 76 +f 1468 93 1765 +f 596 546 812 +f 1457 1305 1477 +f 760 197 150 +f 671 773 765 +f 586 609 604 +f 591 700 632 +f 476 2312 474 +f 2084 2027 2489 +f 582 590 571 +f 1555 2449 1996 +f 674 546 596 +f 812 655 617 +f 161 177 661 +f 599 604 636 +f 700 787 576 +f 776 675 572 +f 776 674 675 +f 617 634 739 +f 591 632 649 +f 612 546 674 +f 617 655 634 +f 728 752 706 +f 571 2311 2305 +f 775 674 776 +f 775 612 674 +f 612 628 546 +f 546 628 812 +f 812 628 655 +f 620 630 615 +f 620 648 630 +f 667 653 646 +f 810 782 785 +f 150 197 814 +f 534 1517 2000 +f 702 572 2378 +f 748 776 572 +f 655 613 634 +f 911 917 905 +f 648 679 662 +f 727 771 713 +f 750 807 799 +f 639 190 144 +f 662 679 200 +f 702 748 572 +f 775 776 748 +f 628 718 655 +f 626 658 645 +f 791 778 790 +f 612 811 628 +f 613 514 634 +f 1380 1756 1673 +f 570 590 614 +f 720 741 719 +f 1074 795 835 +f 614 639 144 +f 612 775 811 +f 718 735 655 +f 655 735 613 +f 798 338 788 +f 636 652 676 +f 571 590 555 +f 528 730 687 +f 690 702 2312 +f 476 690 2312 +f 811 718 628 +f 721 778 727 +f 748 702 690 +f 735 686 613 +f 1517 2002 2127 +f 654 685 667 +f 569 588 606 +f 513 531 538 +f 538 549 548 +f 549 553 548 +f 550 588 549 +f 1903 869 870 +f 691 775 748 +f 691 600 775 +f 600 811 775 +f 811 563 718 +f 563 736 718 +f 718 736 735 +f 736 647 735 +f 735 647 686 +f 686 745 613 +f 745 514 613 +f 569 606 605 +f 654 667 638 +f 851 857 847 +f 588 569 549 +f 690 691 748 +f 680 514 745 +f 2127 2002 2094 +f 747 701 481 +f 400 373 529 +f 600 536 811 +f 536 563 811 +f 1306 227 1152 +f 522 24 18 +f 523 24 522 +f 865 857 851 +f 2031 2060 1540 +f 767 701 747 +f 618 652 609 +f 652 636 609 +f 573 22 710 +f 642 699 730 +f 1522 1518 2476 +f 500 629 691 +f 690 500 691 +f 691 629 600 +f 780 644 641 +f 579 578 561 +f 131 668 197 +f 197 668 814 +f 789 809 798 +f 622 760 150 +f 621 563 536 +f 673 745 686 +f 673 818 745 +f 818 680 745 +f 680 96 514 +f 2495 2462 1028 +f 1028 583 575 +f 663 794 664 +f 629 761 600 +f 761 757 600 +f 600 757 536 +f 621 696 563 +f 755 736 563 +f 696 755 563 +f 633 736 755 +f 633 647 736 +f 623 686 647 +f 633 623 647 +f 686 623 673 +f 819 680 818 +f 680 819 96 +f 1729 1677 1096 +f 2482 1899 2471 +f 537 536 757 +f 536 537 621 +f 673 819 818 +f 2428 222 230 +f 25 24 523 +f 25 557 24 +f 38 25 19 +f 710 22 272 +f 663 759 794 +f 1120 878 1195 +f 537 696 621 +f 696 633 755 +f 822 2215 2220 +f 97 96 1053 +f 750 784 743 +f 887 905 864 +f 768 784 373 +f 512 513 548 +f 573 664 22 +f 696 715 633 +f 673 521 819 +f 2454 2453 2445 +f 883 887 847 +f 306 812 76 +f 642 528 759 +f 798 809 235 +f 994 792 998 +f 587 626 586 +f 1900 1918 1937 +f 645 652 618 +f 537 786 696 +f 521 593 819 +f 515 19 523 +f 741 749 719 +f 789 326 809 +f 539 581 550 +f 657 777 723 +f 684 713 660 +f 692 712 720 +f 652 666 692 +f 507 761 629 +f 472 507 629 +f 507 757 761 +f 623 633 673 +f 724 521 673 +f 515 516 19 +f 304 675 674 +f 178 778 721 +f 947 1447 2358 +f 626 645 618 +f 586 626 618 +f 784 768 742 +f 753 537 757 +f 537 753 786 +f 724 981 521 +f 521 981 593 +f 979 559 850 +f 637 660 677 +f 787 631 576 +f 141 117 385 +f 809 399 235 +f 641 754 558 +f 542 553 561 +f 742 768 762 +f 444 416 533 +f 528 687 796 +f 813 598 566 +f 1490 1501 1557 +f 753 757 507 +f 786 715 696 +f 633 724 673 +f 2090 2062 2109 +f 646 653 660 +f 660 694 683 +f 677 660 683 +f 1872 839 838 +f 1224 18 30 +f 326 393 809 +f 799 529 373 +f 313 507 472 +f 715 774 633 +f 974 699 841 +f 703 820 816 +f 692 711 676 +f 1014 355 766 +f 875 752 1019 +f 627 646 660 +f 711 692 720 +f 652 692 676 +f 799 373 784 +f 813 566 567 +f 2462 2482 2475 +f 764 644 780 +f 1479 1924 1916 +f 753 738 786 +f 738 607 786 +f 786 607 715 +f 715 524 774 +f 633 774 724 +f 559 979 672 +f 758 798 783 +f 683 694 705 +f 820 703 562 +f 764 687 644 +f 744 743 725 +f 313 753 507 +f 607 524 715 +f 664 801 22 +f 646 627 610 +f 800 820 562 +f 750 769 807 +f 767 747 533 +f 578 586 604 +f 862 593 981 +f 688 2382 1083 +f 306 304 674 +f 738 584 607 +f 168 136 238 +f 773 552 765 +f 2473 2464 2458 +f 773 793 552 +f 626 619 658 +f 1007 1139 1013 +f 562 529 799 +f 744 750 743 +f 659 683 693 +f 677 683 659 +f 313 737 753 +f 753 737 738 +f 607 729 524 +f 27 518 28 +f 553 569 580 +f 657 163 777 +f 580 569 605 +f 789 798 758 +f 769 562 807 +f 820 671 816 +f 638 646 611 +f 1074 598 644 +f 750 799 784 +f 1931 907 898 +f 2483 2487 2461 +f 737 584 738 +f 1439 1438 1431 +f 2098 1213 544 +f 48 578 75 +f 796 631 787 +f 815 732 21 +f 581 588 550 +f 625 636 651 +f 778 1011 810 +f 693 705 725 +f 693 683 705 +f 236 1921 1966 +f 584 729 607 +f 2237 1866 2227 +f 530 541 28 +f 237 739 248 +f 512 530 28 +f 727 778 771 +f 684 727 713 +f 2237 2220 826 +f 542 561 560 +f 528 796 700 +f 808 785 671 +f 739 592 248 +f 895 905 896 +f 740 246 186 +f 272 137 979 +f 770 769 744 +f 712 742 720 +f 1213 2026 544 +f 1888 1235 2438 +f 555 554 2311 +f 737 313 1192 +f 1585 1612 1611 +f 695 721 685 +f 518 17 28 +f 769 770 562 +f 719 749 740 +f 648 669 679 +f 773 657 723 +f 606 637 619 +f 2072 2062 2042 +f 606 619 626 +f 549 569 553 +f 161 638 611 +f 910 917 942 +f 917 1103 942 +f 991 1026 992 +f 979 137 672 +f 785 163 657 +f 710 2488 2472 +f 611 581 119 +f 808 671 820 +f 1820 1900 1870 +f 759 700 591 +f 637 677 619 +f 2494 2490 2463 +f 671 765 816 +f 687 764 780 +f 1019 992 1026 +f 1726 1719 987 +f 713 771 694 +f 51 2355 78 +f 510 526 525 +f 525 526 1249 +f 526 33 1249 +f 2311 554 2335 +f 827 848 840 +f 603 591 649 +f 758 269 740 +f 1595 1612 1586 +f 1694 1048 1699 +f 682 740 186 +f 22 801 603 +f 555 570 554 +f 1053 110 97 +f 615 582 601 +f 814 668 188 +f 725 705 744 +f 528 700 759 +f 640 648 620 +f 703 701 562 +f 886 892 582 +f 631 731 576 +f 1087 1835 1747 +f 882 864 895 +f 956 950 1103 +f 1502 2500 2470 +f 205 190 200 +f 815 878 616 +f 616 878 995 +f 1183 878 815 +f 1601 1827 881 +f 527 535 526 +f 2184 2183 2175 +f 1142 1125 1133 +f 235 338 798 +f 160 339 792 +f 599 92 75 +f 598 1116 566 +f 631 558 731 +f 771 770 744 +f 730 528 642 +f 841 699 642 +f 668 401 188 +f 510 527 526 +f 749 758 740 +f 706 721 695 +f 694 726 705 +f 694 744 726 +f 906 911 905 +f 661 695 161 +f 708 815 616 +f 535 547 33 +f 794 759 591 +f 778 808 790 +f 269 758 783 +f 771 744 694 +f 800 808 820 +f 571 886 582 +f 854 948 1010 +f 906 905 887 +f 625 651 635 +f 2000 1226 534 +f 2140 1504 2016 +f 601 620 615 +f 620 601 640 +f 648 640 669 +f 698 452 439 +f 671 785 657 +f 1561 2356 545 +f 685 653 667 +f 685 727 684 +f 568 616 797 +f 708 732 815 +f 93 229 339 +f 865 851 839 +f 942 1103 950 +f 589 614 125 +f 606 610 627 +f 951 834 873 +f 92 599 625 +f 1878 830 1902 +f 2482 2098 1899 +f 568 708 616 +f 708 551 732 +f 2434 2487 2483 +f 160 964 665 +f 2316 2391 2309 +f 762 758 749 +f 570 614 589 +f 888 897 883 +f 2000 1517 1388 +f 685 721 727 +f 588 610 606 +f 653 685 684 +f 651 650 635 +f 760 1151 6 +f 793 622 150 +f 651 676 650 +f 744 769 750 +f 541 542 560 +f 476 500 690 +f 473 1064 1057 +f 561 578 560 +f 636 625 599 +f 876 995 949 +f 829 856 846 +f 682 704 740 +f 791 790 770 +f 2466 2500 2460 +f 579 587 586 +f 1352 1208 1095 +f 1684 1479 1916 +f 604 609 636 +f 751 721 706 +f 810 608 782 +f 672 603 649 +f 475 447 476 +f 794 591 801 +f 682 186 650 +f 808 800 790 +f 644 598 813 +f 704 719 740 +f 1011 608 810 +f 1192 584 737 +f 687 780 796 +f 2337 474 2312 +f 638 667 646 +f 706 1186 728 +f 733 575 568 +f 595 551 708 +f 595 540 551 +f 1308 501 1852 +f 665 339 160 +f 527 2447 535 +f 558 9 731 +f 723 793 773 +f 660 713 694 +f 693 725 666 +f 562 767 529 +f 550 538 531 +f 2267 2287 2233 +f 996 964 160 +f 2068 2470 2466 +f 704 711 719 +f 741 762 749 +f 605 606 626 +f 548 542 530 +f 995 878 709 +f 1898 1684 1916 +f 778 791 771 +f 782 163 785 +f 789 758 762 +f 857 883 847 +f 733 970 1028 +f 838 829 825 +f 2447 511 535 +f 22 603 137 +f 705 726 744 +f 605 587 580 +f 512 548 530 +f 743 784 742 +f 790 800 770 +f 778 810 808 +f 1014 998 355 +f 708 568 595 +f 656 697 551 +f 540 656 551 +f 143 125 614 +f 1000 1020 983 +f 778 178 1011 +f 676 704 682 +f 637 627 660 +f 606 627 637 +f 701 552 481 +f 808 810 785 +f 590 570 555 +f 716 595 568 +f 2355 2335 554 +f 912 1729 911 +f 1076 1456 1546 +f 697 68 492 +f 676 711 704 +f 839 851 838 +f 1028 575 733 +f 1020 844 982 +f 716 568 575 +f 844 781 982 +f 1238 2156 2034 +f 553 580 561 +f 580 579 561 +f 452 461 453 +f 560 578 48 +f 564 540 595 +f 632 656 540 +f 564 632 540 +f 75 578 599 +f 518 27 535 +f 511 518 535 +f 783 798 788 +f 642 759 663 +f 720 742 741 +f 605 626 587 +f 580 587 579 +f 725 712 666 +f 562 701 767 +f 1729 923 911 +f 712 743 742 +f 619 677 658 +f 161 695 654 +f 770 800 562 +f 2084 2489 2472 +f 575 559 716 +f 716 564 595 +f 654 695 685 +f 843 855 2064 +f 34 731 9 +f 527 510 1973 +f 723 622 793 +f 992 1726 987 +f 693 666 652 +f 2472 853 573 +f 624 159 148 +f 671 657 773 +f 681 188 498 +f 797 970 733 +f 565 656 632 +f 565 697 656 +f 565 731 697 +f 1949 951 920 +f 85 111 84 +f 662 200 190 +f 44 324 754 +f 33 547 40 +f 658 693 652 +f 658 652 645 +f 664 794 801 +f 666 712 692 +f 639 648 662 +f 611 646 610 +f 850 559 575 +f 1447 2490 1106 +f 1972 1955 1935 +f 582 615 590 +f 66 581 539 +f 780 641 631 +f 796 780 631 +f 1049 1192 83 +f 1348 13 1519 +f 799 807 562 +f 581 611 588 +f 687 795 644 +f 663 8 642 +f 1936 1972 1935 +f 650 676 682 +f 615 630 590 +f 730 795 687 +f 742 762 741 +f 548 553 542 +f 1048 1692 1074 +f 658 659 693 +f 37 52 30 +f 611 610 588 +f 649 632 564 +f 565 576 731 +f 2138 922 1058 +f 1204 854 965 +f 725 743 712 +f 644 813 641 +f 660 653 684 +f 771 791 770 +f 644 795 1074 +f 469 480 681 +f 559 672 564 +f 716 559 564 +f 672 649 564 +f 2161 1378 2171 +f 474 475 476 +f 816 765 701 +f 765 552 701 +f 513 538 548 +f 754 324 107 +f 609 586 618 +f 25 523 19 +f 677 659 658 +f 689 452 698 +f 1334 1115 1353 +f 700 565 632 +f 700 576 565 +f 481 552 793 +f 763 901 2458 +f 550 549 538 +f 781 964 996 +f 1596 1634 1595 +f 198 916 1124 +f 198 1124 341 +f 842 973 1025 +f 842 1025 836 +f 1009 1024 934 +f 573 710 2472 +f 1100 971 1002 +f 1501 1081 1557 +f 1225 1219 955 +f 413 2138 284 +f 955 1630 522 +f 341 1124 301 +f 2333 2376 2350 +f 1107 218 284 +f 398 925 1513 +f 1513 1442 1495 +f 1935 1455 1744 +f 1723 1935 1744 +f 825 1872 838 +f 1495 1442 1496 +f 963 1024 1009 +f 1511 1514 966 +f 1775 1729 912 +f 688 262 1067 +f 714 1007 1512 +f 919 1732 914 +f 2319 2331 2304 +f 2400 2407 2391 +f 1674 2164 1780 +f 843 927 899 +f 1660 988 1188 +f 1067 262 1640 +f 1381 1109 1483 +f 1437 1381 1483 +f 2495 1010 948 +f 1514 1289 1313 +f 899 374 961 +f 1438 1430 1422 +f 1634 1095 1632 +f 2487 973 2461 +f 1003 499 874 +f 849 848 827 +f 1430 1462 1453 +f 2496 2084 2471 +f 909 10 980 +f 730 927 835 +f 2031 1540 1536 +f 831 849 2178 +f 881 834 951 +f 1841 1722 1803 +f 1005 670 1020 +f 1021 670 1005 +f 1869 2059 2467 +f 903 902 1939 +f 2476 2502 1651 +f 853 8 573 +f 1850 831 2178 +f 934 746 247 +f 934 65 746 +f 301 285 1077 +f 968 944 977 +f 970 2495 1028 +f 974 2465 374 +f 899 927 374 +f 1882 1898 1916 +f 1613 1634 1596 +f 909 833 1396 +f 2492 247 1003 +f 919 914 1931 +f 1459 1299 1458 +f 1634 1632 1633 +f 844 670 228 +f 2494 2497 2467 +f 901 973 2487 +f 228 1772 734 +f 1701 1709 1666 +f 963 574 1024 +f 847 864 856 +f 1730 1736 2239 +f 870 859 848 +f 2074 2111 2103 +f 1140 1590 1483 +f 927 730 974 +f 2103 978 2074 +f 756 1745 1718 +f 848 859 840 +f 1296 1482 1320 +f 2331 51 66 +f 1067 988 962 +f 1396 833 1445 +f 1001 1005 1000 +f 901 1009 973 +f 1099 1077 817 +f 933 944 936 +f 952 958 1828 +f 988 1660 986 +f 833 1067 1445 +f 1067 1640 988 +f 218 413 284 +f 1843 180 347 +f 1846 1708 1798 +f 2469 2477 855 +f 1006 1021 1005 +f 381 382 250 +f 2369 828 531 +f 968 977 1001 +f 2460 1949 779 +f 1194 1441 1115 +f 1001 1000 968 +f 756 678 1745 +f 963 1009 901 +f 2471 2084 2472 +f 841 642 8 +f 982 991 1027 +f 670 844 1020 +f 1289 1514 945 +f 869 904 890 +f 1161 1115 1639 +f 823 2178 849 +f 746 12 499 +f 263 428 2366 +f 1685 1075 1692 +f 1002 926 806 +f 1799 1755 216 +f 944 968 993 +f 943 944 993 +f 31 38 19 +f 531 828 550 +f 1501 1078 1081 +f 1921 1149 431 +f 936 943 932 +f 1660 1489 1412 +f 301 980 285 +f 903 918 902 +f 869 890 868 +f 890 903 867 +f 1003 746 499 +f 951 1949 2500 +f 990 841 853 +f 1595 1634 1611 +f 374 927 974 +f 836 1025 247 +f 1653 1652 1638 +f 1303 1545 1142 +f 1616 1631 1638 +f 1629 1546 1628 +f 936 932 913 +f 513 506 531 +f 868 890 867 +f 2330 2369 2353 +f 924 918 914 +f 907 914 904 +f 1258 1421 1267 +f 301 939 980 +f 1472 1482 1296 +f 868 867 859 +f 472 491 313 +f 272 519 2488 +f 1471 1472 1296 +f 1025 934 247 +f 1634 1633 1611 +f 2176 1847 2177 +f 1310 1289 806 +f 924 933 918 +f 1969 1968 902 +f 2107 2128 2118 +f 1428 1436 1287 +f 1139 1564 1617 +f 2378 572 2384 +f 853 841 8 +f 2501 961 2465 +f 1221 1240 1408 +f 1069 1578 1627 +f 1006 1005 1001 +f 1617 1564 1578 +f 828 539 550 +f 1791 2168 2160 +f 1829 1718 1739 +f 1968 1939 902 +f 756 1718 665 +f 1998 2000 1388 +f 2451 545 2356 +f 178 997 1011 +f 1275 325 1270 +f 1709 872 1666 +f 2176 1959 1847 +f 944 943 936 +f 2424 518 511 +f 1445 1067 962 +f 2007 952 1828 +f 2052 2061 2081 +f 828 2303 539 +f 835 1699 1048 +f 1709 1706 872 +f 885 574 963 +f 1318 1296 1320 +f 859 867 1902 +f 1452 1448 1421 +f 943 993 976 +f 993 1000 983 +f 854 1010 876 +f 988 986 962 +f 2031 2052 2081 +f 924 1732 1828 +f 965 949 1060 +f 781 228 734 +f 1718 1765 665 +f 943 976 932 +f 1680 1794 1783 +f 1448 1471 1276 +f 1276 1267 1421 +f 1931 914 907 +f 991 781 996 +f 1276 1421 1448 +f 10 909 1396 +f 831 860 849 +f 1523 1762 1774 +f 924 1828 937 +f 307 994 1014 +f 946 963 901 +f 978 2103 977 +f 977 1006 1001 +f 1007 1161 1639 +f 1639 1294 1437 +f 885 1032 574 +f 1294 1381 1437 +f 733 568 797 +f 792 229 1112 +f 119 581 108 +f 843 835 927 +f 1889 860 831 +f 2211 2216 2204 +f 2400 2431 2422 +f 2103 1006 977 +f 840 1902 830 +f 827 840 830 +f 827 830 822 +f 1003 874 2492 +f 1432 1439 1431 +f 781 734 964 +f 1937 1936 1723 +f 918 913 902 +f 958 977 944 +f 1850 2178 2177 +f 1005 1020 1000 +f 991 996 307 +f 1396 1445 340 +f 2179 1763 889 +f 939 909 980 +f 1828 958 937 +f 978 977 958 +f 1590 1571 1563 +f 779 1949 920 +f 1551 1362 1573 +f 2103 2142 1006 +f 920 885 963 +f 946 920 963 +f 1584 1616 1583 +f 1453 1472 1452 +f 1647 1617 1578 +f 1578 1564 1627 +f 1628 938 1069 +f 869 868 859 +f 993 983 976 +f 912 1762 1775 +f 752 751 706 +f 1628 1546 938 +f 844 228 781 +f 840 859 1902 +f 898 907 904 +f 1025 973 1009 +f 663 664 573 +f 763 946 901 +f 898 904 869 +f 2172 889 1763 +f 1128 926 971 +f 860 848 849 +f 904 903 890 +f 2486 2459 2479 +f 577 782 608 +f 933 936 918 +f 2177 1847 1851 +f 665 1765 339 +f 937 958 944 +f 894 981 724 +f 968 1000 993 +f 2192 2195 2205 +f 1652 1099 817 +f 997 608 1011 +f 997 577 608 +f 577 163 782 +f 1112 998 792 +f 2177 1851 1850 +f 1257 1421 1258 +f 951 873 920 +f 822 830 2215 +f 1899 2496 2471 +f 1773 1668 1558 +f 904 914 903 +f 932 1671 913 +f 873 885 920 +f 1013 1617 1647 +f 873 1032 885 +f 894 862 981 +f 2469 855 961 +f 913 1671 1969 +f 2477 2064 855 +f 918 936 913 +f 860 870 848 +f 937 944 933 +f 1501 1013 1647 +f 824 178 751 +f 824 997 178 +f 824 577 997 +f 643 163 577 +f 863 856 882 +f 2128 2153 2134 +f 722 774 880 +f 722 894 774 +f 864 905 895 +f 850 575 583 +f 914 918 903 +f 924 937 933 +f 1501 717 1013 +f 1587 1324 928 +f 717 1512 1013 +f 602 577 824 +f 766 643 577 +f 894 709 862 +f 709 878 862 +f 976 975 932 +f 1324 1596 928 +f 880 524 1060 +f 2434 2459 2499 +f 1324 1613 1596 +f 752 824 751 +f 602 766 577 +f 1014 602 594 +f 1387 1226 2152 +f 2153 1387 2152 +f 669 930 950 +f 1710 1694 1699 +f 768 326 762 +f 582 892 601 +f 974 990 2465 +f 624 116 625 +f 835 795 730 +f 2458 2484 763 +f 989 602 824 +f 2064 2477 1710 +f 976 983 975 +f 949 722 880 +f 996 160 994 +f 2305 863 556 +f 556 863 886 +f 601 910 640 +f 2264 825 829 +f 989 824 752 +f 856 864 882 +f 1595 1586 2381 +f 1627 1629 1628 +f 2174 2180 2173 +f 2128 2134 2118 +f 137 272 22 +f 949 880 1060 +f 995 894 722 +f 894 995 709 +f 894 724 774 +f 886 895 892 +f 640 910 930 +f 871 870 860 +f 846 856 863 +f 1026 875 1019 +f 838 851 829 +f 1024 1171 934 +f 36 189 205 +f 863 882 886 +f 886 882 895 +f 875 1026 594 +f 52 1459 1269 +f 896 917 910 +f 1025 1009 934 +f 949 995 722 +f 2152 1226 1636 +f 895 896 892 +f 892 910 601 +f 942 950 930 +f 875 989 752 +f 594 602 989 +f 766 355 643 +f 355 260 643 +f 905 917 896 +f 965 1060 1162 +f 892 896 910 +f 1101 1052 1042 +f 1029 1031 834 +f 1101 1133 1118 +f 342 357 376 +f 516 515 2454 +f 1656 2494 2467 +f 1056 1303 1133 +f 1120 1130 862 +f 69 342 376 +f 1055 1056 1133 +f 499 69 165 +f 85 101 111 +f 1031 1032 834 +f 200 679 1166 +f 1031 1042 1032 +f 1171 65 934 +f 1822 1204 1177 +f 1096 956 1103 +f 514 96 97 +f 956 1145 1144 +f 1185 1166 1144 +f 1145 1185 1144 +f 1185 200 1166 +f 375 132 1041 +f 1153 1202 305 +f 32 1244 1249 +f 1096 1087 956 +f 554 78 2355 +f 1191 138 110 +f 65 35 432 +f 1087 1110 956 +f 1110 1146 956 +f 956 1146 1145 +f 1146 1156 1145 +f 1145 1156 1185 +f 950 956 1144 +f 2481 2495 948 +f 1156 1193 1185 +f 1050 1047 1051 +f 239 151 107 +f 1185 1193 36 +f 1747 1110 1087 +f 1134 1146 1110 +f 1146 1157 1156 +f 1156 1157 1193 +f 1041 1045 1034 +f 1397 1134 1110 +f 1157 1146 1134 +f 1157 1175 1193 +f 1193 199 36 +f 1090 1035 1196 +f 1456 1150 1051 +f 1175 199 1193 +f 1186 695 199 +f 1186 199 1175 +f 1175 1157 1134 +f 728 1186 1175 +f 197 760 6 +f 1130 593 862 +f 1167 1109 182 +f 1194 1115 1161 +f 2140 1928 1504 +f 921 922 2138 +f 1147 1134 1397 +f 1719 1147 1397 +f 1147 1175 1134 +f 1175 1147 728 +f 341 1654 1208 +f 754 151 9 +f 284 2138 1058 +f 1188 1557 1660 +f 1191 110 1053 +f 916 284 1189 +f 284 1058 1189 +f 2094 1465 2127 +f 1726 1019 1147 +f 1147 1019 728 +f 593 1130 96 +f 239 305 1038 +f 1036 1131 315 +f 397 1131 1120 +f 1053 96 1130 +f 2467 2485 1869 +f 517 1089 421 +f 834 1827 1029 +f 419 1047 1117 +f 1034 433 1306 +f 2239 1862 1730 +f 1453 1462 1472 +f 1408 1422 1399 +f 471 23 1111 +f 1205 1150 1456 +f 1205 1040 1150 +f 1131 1036 293 +f 293 1068 1044 +f 375 1041 138 +f 1205 1140 1046 +f 1040 1205 1046 +f 1140 1167 1046 +f 1104 1049 83 +f 1052 1085 1032 +f 1044 1068 1191 +f 1167 1483 1109 +f 208 1084 1035 +f 1040 132 375 +f 1834 20 3 +f 1050 1051 1070 +f 1133 1125 1174 +f 11 1440 1401 +f 420 208 1071 +f 1135 1079 1094 +f 1086 1101 1118 +f 1029 1030 1031 +f 1200 1061 294 +f 1191 1068 138 +f 1171 1141 65 +f 1141 1172 65 +f 1172 35 65 +f 1172 404 35 +f 404 99 35 +f 221 1104 1063 +f 802 398 1083 +f 20 1089 3 +f 2064 1699 835 +f 1042 1052 1032 +f 1433 1261 1432 +f 1323 2338 155 +f 1076 1205 1456 +f 1088 1402 1056 +f 1150 348 1070 +f 1200 1089 20 +f 1097 1162 100 +f 1032 873 834 +f 21 471 1111 +f 294 1097 1104 +f 1072 100 584 +f 1151 760 622 +f 132 1045 1041 +f 1050 1070 1135 +f 1088 1039 940 +f 650 159 635 +f 100 1170 729 +f 729 584 100 +f 1103 931 1096 +f 925 1443 1513 +f 138 1102 110 +f 1034 1306 1152 +f 1071 1035 1090 +f 100 1072 1097 +f 23 1158 315 +f 1068 375 138 +f 1586 1612 1585 +f 1819 1030 1029 +f 1041 1034 1102 +f 232 375 1068 +f 348 1079 1070 +f 1061 1097 294 +f 1513 1443 1442 +f 1200 294 1119 +f 376 1050 1062 +f 1094 1036 315 +f 1200 1119 1089 +f 1111 1183 21 +f 1044 1191 1053 +f 698 295 689 +f 1079 232 1036 +f 404 1117 99 +f 1495 1496 717 +f 1119 294 98 +f 3 1089 517 +f 1132 1063 83 +f 1132 83 175 +f 132 1046 182 +f 1111 1195 1183 +f 1131 1044 1037 +f 127 402 1804 +f 219 1272 1047 +f 1697 1135 1094 +f 2140 1854 2117 +f 1111 397 1195 +f 1177 1162 1097 +f 1061 1177 1097 +f 717 1509 714 +f 2 1300 433 +f 462 290 461 +f 98 294 221 +f 294 1104 221 +f 714 1161 1007 +f 1073 1152 1143 +f 1697 1094 1360 +f 1223 1423 1218 +f 836 2479 842 +f 1097 1072 1049 +f 348 1040 375 +f 3 517 316 +f 180 1061 1201 +f 348 375 232 +f 1432 1431 1415 +f 220 1513 1495 +f 1104 1097 1049 +f 306 674 596 +f 777 455 723 +f 2170 2151 1641 +f 1047 419 219 +f 1102 1034 1073 +f 1073 1034 1152 +f 1035 1054 1196 +f 1177 1204 1162 +f 746 65 12 +f 751 178 721 +f 1054 517 421 +f 1051 1150 1070 +f 1102 1073 110 +f 998 1136 355 +f 567 566 1163 +f 1111 315 397 +f 1048 1074 835 +f 1158 1094 315 +f 1374 1107 1252 +f 1112 1136 998 +f 472 629 500 +f 355 1136 260 +f 260 118 43 +f 1104 83 1063 +f 376 357 1050 +f 1463 1142 1545 +f 1036 232 293 +f 1030 1042 1031 +f 1079 348 232 +f 221 1063 1132 +f 1094 1079 1036 +f 1076 1629 1205 +f 1136 1197 260 +f 260 1197 118 +f 1204 965 1162 +f 293 232 1068 +f 1590 1205 1629 +f 1205 1590 1140 +f 250 382 392 +f 1296 1318 1311 +f 347 1201 20 +f 1201 1200 20 +f 132 182 1045 +f 1101 1086 1052 +f 1033 1039 1055 +f 138 1041 1102 +f 970 1010 2495 +f 455 777 43 +f 1992 1948 2023 +f 20 1834 347 +f 1072 584 1049 +f 584 1192 1049 +f 182 2 1045 +f 1163 324 44 +f 1360 1094 1158 +f 1450 1360 1158 +f 1091 1112 229 +f 509 723 455 +f 207 509 455 +f 1251 1257 1266 +f 1488 1489 1547 +f 2157 1541 1875 +f 305 107 324 +f 1045 2 433 +f 1070 1079 1135 +f 1136 1168 1197 +f 1197 359 118 +f 118 359 43 +f 359 356 43 +f 356 455 43 +f 356 207 455 +f 1240 1422 1408 +f 1163 1153 324 +f 1201 1061 1200 +f 1052 1086 1085 +f 1024 1141 1171 +f 1112 1105 1136 +f 1050 1135 1062 +f 1105 1168 1136 +f 1168 1178 1197 +f 1197 1178 359 +f 1173 404 1172 +f 465 356 359 +f 1174 1125 240 +f 1240 1431 1422 +f 1098 1113 1105 +f 1112 1098 1105 +f 1105 1178 1168 +f 1178 465 359 +f 1091 1098 1112 +f 1133 1174 1118 +f 98 221 1059 +f 487 1132 175 +f 980 1017 285 +f 465 207 356 +f 180 1201 347 +f 1060 524 1170 +f 445 127 316 +f 1431 1438 1422 +f 498 469 681 +f 940 1807 1759 +f 381 250 1290 +f 1113 1122 1105 +f 1105 1122 1178 +f 1151 509 207 +f 1236 2035 525 +f 1131 293 1044 +f 346 207 465 +f 346 1151 207 +f 1822 1796 1204 +f 1143 204 97 +f 123 1128 971 +f 2153 2152 2134 +f 126 1151 346 +f 517 445 316 +f 1450 1158 23 +f 1458 1462 1430 +f 1129 152 1182 +f 1122 1159 1178 +f 1178 1198 465 +f 79 346 465 +f 126 1155 1151 +f 1151 1155 6 +f 295 1129 689 +f 1073 1143 97 +f 1098 1123 1113 +f 1113 1123 1122 +f 1123 1169 1122 +f 1178 1159 1198 +f 1198 79 465 +f 392 383 152 +f 1822 1061 180 +f 116 92 625 +f 421 1089 1119 +f 1129 295 152 +f 110 1073 97 +f 1173 1172 1141 +f 1122 1169 1159 +f 79 126 346 +f 1155 181 6 +f 971 926 1002 +f 295 1043 152 +f 1039 1088 1056 +f 1428 1266 1436 +f 404 419 1117 +f 836 879 2479 +f 2464 2476 2458 +f 1198 317 79 +f 1124 939 301 +f 44 754 567 +f 1039 1056 1055 +f 1439 1459 1458 +f 1660 1412 986 +f 1169 1160 1159 +f 179 1155 126 +f 1155 131 181 +f 1061 1822 1177 +f 1153 305 324 +f 175 314 327 +f 1160 1187 1159 +f 1159 1187 1198 +f 1198 1187 317 +f 79 179 126 +f 1043 250 392 +f 152 1043 392 +f 96 819 593 +f 1123 1127 1169 +f 317 179 79 +f 1057 1155 179 +f 1155 391 131 +f 131 391 668 +f 2381 1586 1585 +f 12 69 499 +f 262 398 1640 +f 2107 2118 2060 +f 2130 2094 2002 +f 1187 249 317 +f 1155 1057 391 +f 1290 439 1265 +f 305 239 107 +f 1127 1160 1169 +f 317 473 179 +f 473 1057 179 +f 83 1192 314 +f 1043 1290 250 +f 1807 940 1030 +f 517 1084 445 +f 1057 1164 391 +f 2492 2480 2493 +f 163 643 43 +f 1056 1545 1303 +f 1069 1655 1023 +f 249 473 317 +f 1162 1060 1170 +f 1086 1118 1154 +f 82 68 16 +f 1989 1990 1536 +f 1633 1632 1611 +f 1487 2372 1305 +f 1494 1069 1023 +f 1137 1160 1127 +f 669 1166 679 +f 390 1285 426 +f 1955 1972 1971 +f 1219 1223 2437 +f 1254 1261 1223 +f 1319 1545 1056 +f 1320 1328 2443 +f 1261 1433 1223 +f 1219 1254 1223 +f 254 222 2428 +f 1237 1290 1265 +f 1284 1273 1263 +f 1277 1291 1301 +f 1314 102 1301 +f 1280 363 377 +f 1313 1353 1514 +f 468 451 439 +f 1918 1964 1956 +f 2026 29 2140 +f 1354 381 1279 +f 1224 30 1254 +f 147 158 173 +f 1247 1253 274 +f 1271 380 334 +f 2043 2072 2042 +f 274 300 267 +f 1356 1392 211 +f 13 240 1142 +f 1382 1330 1392 +f 1312 1323 155 +f 240 1125 1142 +f 2358 1573 1362 +f 1236 1249 1244 +f 1272 219 1348 +f 1271 1274 380 +f 191 2034 1982 +f 1992 2052 1990 +f 462 452 689 +f 2262 2286 2261 +f 183 489 1642 +f 2485 2480 1869 +f 84 111 1323 +f 1190 353 1354 +f 446 434 435 +f 1336 171 1341 +f 2021 430 2059 +f 862 878 1120 +f 1263 1273 1248 +f 1966 1921 2144 +f 1312 84 1323 +f 240 13 1348 +f 1359 1274 1271 +f 1392 1330 1247 +f 1520 1333 11 +f 1368 1253 1247 +f 1279 1285 1190 +f 2465 990 2489 +f 1272 1519 805 +f 1369 1272 805 +f 1317 95 1344 +f 1242 1248 1234 +f 1368 242 1363 +f 274 1262 1386 +f 532 597 1886 +f 2117 2026 2140 +f 1392 1247 274 +f 2162 508 985 +f 1964 1469 1965 +f 1315 104 1331 +f 1392 1356 1382 +f 128 1342 1336 +f 1285 427 426 +f 1219 1224 1254 +f 1320 1322 1321 +f 1320 1321 1328 +f 153 2443 1328 +f 1321 153 1328 +f 1235 1244 1243 +f 1225 1224 1219 +f 1359 353 1190 +f 1312 1473 1458 +f 1336 1342 147 +f 305 1333 1038 +f 1336 147 171 +f 516 31 19 +f 2479 2461 842 +f 1237 1265 427 +f 1263 1278 1284 +f 881 1827 834 +f 1237 427 1285 +f 1299 1312 1458 +f 1190 1285 1274 +f 1363 286 1253 +f 2330 2303 828 +f 427 442 426 +f 2493 2463 2492 +f 1285 380 1274 +f 522 18 1225 +f 2471 2472 2488 +f 2338 154 1321 +f 1423 1415 1218 +f 1225 18 1224 +f 1253 286 1262 +f 286 353 1359 +f 171 1368 1383 +f 1273 54 1234 +f 1973 2447 527 +f 1322 155 1321 +f 1203 1369 1413 +f 1307 363 1298 +f 1364 1375 1329 +f 1329 227 1306 +f 296 1298 1343 +f 947 2499 1447 +f 1203 1047 1272 +f 1098 1748 1123 +f 1519 1272 1348 +f 1277 70 1273 +f 1282 1337 1361 +f 286 302 353 +f 103 104 1315 +f 1377 435 434 +f 1449 1261 1345 +f 926 1310 806 +f 1263 1248 1242 +f 985 508 597 +f 1415 1222 1218 +f 88 1325 104 +f 170 111 156 +f 1384 1282 1361 +f 274 1253 1262 +f 1371 1317 1344 +f 1371 1366 1337 +f 1345 1459 1449 +f 171 1383 1341 +f 2438 1235 1227 +f 2134 1582 2118 +f 428 1260 1379 +f 1336 1341 1325 +f 1235 1242 1227 +f 1228 1687 2284 +f 1854 2140 2016 +f 1866 1887 1873 +f 1343 1298 1370 +f 1384 1361 2440 +f 171 242 1368 +f 1344 1309 1366 +f 1371 1344 1366 +f 1280 1377 1293 +f 200 1185 205 +f 1330 1383 1368 +f 1255 1264 1263 +f 543 1367 1876 +f 1343 1370 1260 +f 1293 1326 1370 +f 2440 1361 1302 +f 1282 1384 2406 +f 271 1337 1282 +f 170 2338 1323 +f 1528 1503 2470 +f 515 1347 2453 +f 1997 1705 1998 +f 2285 1228 2284 +f 1229 1250 1228 +f 1330 1368 1247 +f 1919 1619 2045 +f 1344 1364 1335 +f 1222 1240 1221 +f 1212 858 1741 +f 2388 1222 1221 +f 1528 2470 2068 +f 501 1308 2171 +f 1295 1311 1487 +f 2116 1619 1655 +f 1220 1229 1228 +f 8 663 573 +f 1343 1260 428 +f 1337 1366 1361 +f 1298 1280 1293 +f 1269 1345 1261 +f 1279 381 1290 +f 1230 1229 1220 +f 1230 1245 1229 +f 1245 1250 1229 +f 1227 1234 31 +f 1302 1361 1350 +f 1245 1266 1428 +f 1992 2023 2052 +f 2482 2471 2475 +f 452 462 461 +f 271 1282 1275 +f 1991 1989 1934 +f 1366 1309 1350 +f 1344 1335 1309 +f 730 699 974 +f 1374 1252 1208 +f 597 508 1912 +f 1363 1253 1368 +f 1386 1271 300 +f 1211 1218 1222 +f 1376 1377 434 +f 2399 2437 1211 +f 1284 1291 1277 +f 1230 1251 1245 +f 1251 1266 1245 +f 1317 1371 1337 +f 1288 1286 1095 +f 1095 1286 1352 +f 1241 1208 1352 +f 1241 1374 1208 +f 1284 1278 1291 +f 211 1392 267 +f 1344 1375 1364 +f 929 583 1028 +f 1361 1366 1350 +f 1115 1294 1639 +f 1291 103 1301 +f 1220 1231 1230 +f 1231 1251 1230 +f 1234 1248 1273 +f 1255 55 1264 +f 1360 1450 1702 +f 363 1280 1298 +f 1369 1203 1272 +f 1415 1240 1222 +f 1216 1231 1220 +f 1243 1263 1235 +f 1375 227 1329 +f 1264 1278 1263 +f 855 899 961 +f 1286 1241 1352 +f 2081 2128 2107 +f 1223 1433 1423 +f 1473 1312 155 +f 154 153 1321 +f 1377 1376 1293 +f 1392 274 267 +f 334 300 1271 +f 1955 1991 1934 +f 1613 1327 1288 +f 1327 1286 1288 +f 1349 1374 1241 +f 2370 2025 2367 +f 1315 1331 133 +f 434 446 1256 +f 1232 1251 1231 +f 1243 1244 1255 +f 1286 1304 1241 +f 1349 1107 1374 +f 1359 1271 1386 +f 1227 516 2431 +f 219 240 1348 +f 1270 271 1275 +f 1255 1263 1243 +f 2026 1926 29 +f 1683 2157 1212 +f 1326 1293 1376 +f 1255 32 55 +f 104 1325 1341 +f 519 2462 2475 +f 2154 2161 2137 +f 1376 434 1246 +f 1246 434 1256 +f 1257 1251 1232 +f 1262 1359 1386 +f 2195 2192 2186 +f 1308 534 1226 +f 2026 2117 544 +f 1327 1613 1324 +f 1327 1326 1286 +f 1286 1326 1304 +f 104 1341 1331 +f 774 524 880 +f 837 1517 534 +f 1127 1123 1567 +f 1279 1237 1285 +f 1297 1381 1294 +f 1217 1232 1216 +f 1142 1519 13 +f 1436 1267 1287 +f 1324 1372 1327 +f 1304 1246 1241 +f 1246 1349 1241 +f 1246 1373 1349 +f 286 1359 1262 +f 1382 1383 1330 +f 1284 1277 1273 +f 489 1998 1799 +f 1675 1116 1075 +f 106 1317 1337 +f 1311 1295 1281 +f 1292 1364 1329 +f 1335 1364 1292 +f 1334 1294 1115 +f 1334 1297 1294 +f 1300 1381 1297 +f 973 842 2461 +f 1217 1239 1232 +f 1232 1239 1257 +f 1258 1267 1436 +f 1359 1190 1274 +f 1862 1405 1877 +f 1372 1339 1327 +f 1339 1326 1327 +f 1373 1351 1349 +f 1276 1311 1281 +f 1256 2386 1351 +f 2 1109 1300 +f 482 1731 520 +f 803 1604 2022 +f 1223 1218 1211 +f 1341 1383 1382 +f 1298 1293 1370 +f 1190 1354 1279 +f 1324 2398 1372 +f 1714 1700 2173 +f 183 2000 489 +f 1701 1666 192 +f 1227 1242 1234 +f 1332 1289 1310 +f 1517 2005 2130 +f 1331 1341 1382 +f 525 1249 1236 +f 23 1268 1450 +f 1264 1291 1278 +f 1281 1287 1267 +f 1295 1305 1287 +f 1281 1295 1287 +f 1487 1305 1295 +f 1605 2097 2058 +f 1326 1376 1304 +f 1304 1376 1246 +f 1316 1919 1984 +f 2500 1949 2460 +f 1332 1313 1289 +f 2189 2181 2177 +f 1335 1334 1353 +f 1292 1297 1334 +f 1428 1250 1245 +f 969 958 952 +f 1217 1233 1239 +f 1233 1257 1239 +f 1876 1367 1338 +f 1379 1260 1372 +f 1372 1260 1339 +f 1128 1302 1310 +f 1310 1302 1332 +f 1335 1353 1313 +f 1292 1334 1335 +f 1297 1329 1300 +f 1279 1290 1237 +f 1301 103 1314 +f 70 1301 102 +f 23 1333 1268 +f 380 1285 390 +f 772 325 1275 +f 1314 103 1315 +f 2473 2458 2487 +f 1276 1281 1267 +f 1344 95 1375 +f 2053 1771 1572 +f 1246 1256 1373 +f 1373 1256 1351 +f 1340 1302 1128 +f 1350 1313 1332 +f 1329 1297 1292 +f 2434 2473 2487 +f 106 1337 271 +f 23 471 1333 +f 622 723 509 +f 1388 1517 2127 +f 1991 1990 1989 +f 183 1636 1226 +f 2133 1605 2151 +f 1260 1370 1339 +f 1339 1370 1326 +f 867 1894 1902 +f 390 426 412 +f 1235 1263 1242 +f 1399 1422 1233 +f 305 11 1333 +f 1300 1329 1306 +f 1302 1350 1332 +f 1350 1309 1313 +f 1309 1335 1313 +f 2470 2102 1502 +f 1787 1531 1599 +f 1724 1725 1691 +f 1827 1601 1927 +f 1678 1358 1476 +f 1823 1812 1846 +f 1805 1824 1708 +f 1746 1676 1797 +f 325 2395 429 +f 1835 1677 1826 +f 1507 1790 1722 +f 1526 1672 858 +f 158 147 1342 +f 1462 1473 1322 +f 1474 1414 1565 +f 1761 1900 1877 +f 940 1759 1008 +f 1565 1015 1008 +f 1924 1533 1933 +f 1878 826 830 +f 1565 1414 1015 +f 1402 1088 1008 +f 1538 1532 1651 +f 1015 1552 1008 +f 1538 1591 1474 +f 1532 1538 1474 +f 1474 1591 1414 +f 1484 1402 1008 +f 1552 1484 1008 +f 1414 1460 1015 +f 1015 1460 1552 +f 806 1289 945 +f 1597 1538 1659 +f 1484 1319 1402 +f 1056 1402 1319 +f 1538 1597 1591 +f 1591 960 1414 +f 1414 960 1460 +f 1925 1466 1455 +f 1552 1400 1484 +f 1484 1400 1319 +f 1400 113 1319 +f 1597 1580 1591 +f 1460 1400 1552 +f 1514 1441 966 +f 1597 1659 1409 +f 1657 113 1400 +f 1460 1657 1400 +f 1288 1095 1634 +f 1551 1597 1409 +f 1580 1598 1591 +f 1591 1598 960 +f 1536 1990 2031 +f 960 1657 1460 +f 1809 1746 1797 +f 1423 1433 1432 +f 2478 1362 1409 +f 1463 1545 113 +f 1657 1463 113 +f 1457 1287 1305 +f 1682 1716 1746 +f 1434 1761 1885 +f 1013 1139 1617 +f 2379 1362 2478 +f 1420 1597 1551 +f 1420 1580 1597 +f 1664 1808 1712 +f 2256 2250 2231 +f 1362 1551 1409 +f 2196 2214 2213 +f 1691 1725 1777 +f 1626 192 1666 +f 1534 1574 2058 +f 1574 1600 1605 +f 1600 1606 1605 +f 1606 1641 1605 +f 1573 1420 1551 +f 1657 1485 1463 +f 678 1806 1742 +f 1534 1553 1574 +f 1574 1575 1600 +f 1810 2170 585 +f 1623 1641 1606 +f 1407 1657 960 +f 1598 1407 960 +f 1485 1142 1463 +f 1716 1581 1676 +f 1738 1743 1733 +f 843 2064 835 +f 1539 1575 1574 +f 1553 1539 1574 +f 1575 1592 1600 +f 1592 1624 1606 +f 1600 1592 1606 +f 1642 585 1641 +f 1623 1642 1641 +f 1485 164 1142 +f 1738 1516 1743 +f 1809 1720 1798 +f 1533 1535 1534 +f 1592 1607 1624 +f 1624 1623 1606 +f 1163 566 1116 +f 1407 1485 1657 +f 1432 1449 1439 +f 1100 802 2382 +f 1743 1516 1722 +f 1746 1716 1676 +f 1535 1539 1534 +f 1534 1539 1553 +f 1642 1623 1624 +f 1095 1208 1654 +f 967 1407 1598 +f 1580 967 1598 +f 1809 1797 1720 +f 1924 1524 1535 +f 1533 1924 1535 +f 1539 1576 1575 +f 1642 216 585 +f 1407 1529 1485 +f 1485 1529 164 +f 1472 1462 1482 +f 1415 1431 1240 +f 966 1194 714 +f 383 1182 152 +f 474 2337 446 +f 1743 1841 1757 +f 1486 1524 1924 +f 1535 1525 1539 +f 1575 1576 1592 +f 1420 967 1580 +f 1288 1634 1613 +f 459 427 1265 +f 1404 2179 1393 +f 1404 1403 1800 +f 1404 1410 1403 +f 1410 1749 1403 +f 1349 1351 218 +f 1486 1498 1524 +f 1535 1524 1525 +f 1607 1636 1624 +f 183 1642 1624 +f 1636 183 1624 +f 1107 1349 218 +f 1351 845 218 +f 164 1519 1142 +f 845 413 218 +f 1525 1576 1539 +f 1576 1582 1592 +f 1592 2134 1607 +f 2134 1636 1607 +f 2147 1491 1401 +f 1407 1589 1529 +f 1529 1519 164 +f 1693 1763 1444 +f 1924 1479 1486 +f 1592 1582 2134 +f 499 165 874 +f 2176 1857 1959 +f 2327 2368 2326 +f 2358 821 953 +f 953 821 1573 +f 1824 1704 1464 +f 1731 1358 1678 +f 1394 1410 1404 +f 1394 1418 1410 +f 1466 1479 1839 +f 1486 1479 1498 +f 1498 1525 1524 +f 1576 2080 1582 +f 1785 1684 1898 +f 804 398 802 +f 804 925 398 +f 1447 1562 2358 +f 2358 1562 821 +f 821 1620 1573 +f 1620 1420 1573 +f 1420 1556 967 +f 1393 1394 1404 +f 1525 2080 1576 +f 1621 1420 1620 +f 1621 1556 1420 +f 967 1589 1407 +f 1505 5 1357 +f 1266 1258 1436 +f 1393 1395 1394 +f 2176 2175 1848 +f 1455 1466 1839 +f 1525 1540 2080 +f 1582 2080 2118 +f 1100 804 802 +f 1556 1589 967 +f 1589 1082 1529 +f 1093 1685 1357 +f 1504 1093 1357 +f 1425 1418 1394 +f 1475 1479 1466 +f 1479 1506 1498 +f 1789 1784 1730 +f 2501 2465 2489 +f 1438 1458 1430 +f 1462 1458 1473 +f 1454 805 1529 +f 1082 1454 1529 +f 1529 805 1519 +f 1425 1394 1395 +f 1425 1744 1418 +f 1479 1475 1506 +f 1540 2060 2080 +f 1556 1082 1589 +f 1443 945 1511 +f 1506 1536 1498 +f 1498 1536 1525 +f 1525 1536 1540 +f 1670 852 1672 +f 1998 1388 1389 +f 1511 966 1509 +f 1509 966 714 +f 1442 1443 1496 +f 1562 1635 821 +f 155 1322 1473 +f 1439 1458 1438 +f 1426 1425 1395 +f 1475 1499 1506 +f 1735 1588 1776 +f 2422 2454 2421 +f 1423 1432 1415 +f 1559 2101 2073 +f 845 866 413 +f 1429 1620 821 +f 1620 1429 1621 +f 1228 1250 1687 +f 1002 945 1443 +f 2382 802 1083 +f 1859 1411 1395 +f 1411 1426 1395 +f 1426 1744 1425 +f 1590 1437 1483 +f 1480 1475 1466 +f 1480 1499 1475 +f 1510 1733 1743 +f 1663 1696 1658 +f 1430 1453 1452 +f 1452 1472 1471 +f 1452 1471 1448 +f 1430 1452 1421 +f 1430 1421 1422 +f 1429 1082 1556 +f 1621 1429 1556 +f 1351 2386 845 +f 1126 1059 487 +f 1639 1437 1563 +f 1504 1928 1093 +f 1499 1536 1506 +f 1588 1770 1727 +f 1110 1747 1397 +f 1776 1588 1531 +f 1322 1320 1482 +f 1590 1629 1571 +f 1730 1877 1838 +f 1429 935 1082 +f 1082 935 1454 +f 804 1443 925 +f 1139 1007 1639 +f 1925 1480 1466 +f 1934 1989 1480 +f 1499 1989 1536 +f 1727 1526 1531 +f 1593 1614 502 +f 2455 2431 2400 +f 1755 1680 908 +f 1563 1571 1564 +f 1647 1078 1501 +f 2490 1635 1106 +f 1496 1511 717 +f 2454 2431 516 +f 1478 1153 1093 +f 1870 1426 1411 +f 1426 1723 1744 +f 962 986 1412 +f 717 1511 1509 +f 1825 1704 1824 +f 2225 2234 2253 +f 1490 1557 1188 +f 1635 80 821 +f 805 1454 935 +f 1186 706 695 +f 1194 1161 714 +f 1512 1007 1013 +f 592 97 204 +f 1258 1266 1257 +f 82 1333 471 +f 1694 1710 1505 +f 1643 490 1661 +f 1661 490 1114 +f 1518 2068 2484 +f 1750 1808 1664 +f 1656 1635 2490 +f 935 1521 805 +f 1546 1629 1076 +f 1301 70 1277 +f 966 1441 1194 +f 1148 1825 1824 +f 1614 1609 1643 +f 1114 1092 1921 +f 1770 1739 1670 +f 1631 1632 1646 +f 821 1016 1429 +f 1429 1016 935 +f 1632 1095 1654 +f 1083 262 688 +f 1724 1686 1725 +f 1644 490 1643 +f 1092 1149 1921 +f 3 893 1832 +f 988 1640 1188 +f 916 1107 284 +f 1656 80 1635 +f 1016 821 80 +f 1016 1521 935 +f 1478 1202 1153 +f 1401 1928 29 +f 1440 1478 1928 +f 1849 1700 1865 +f 1595 1611 1612 +f 1208 198 341 +f 1464 1704 1746 +f 2143 984 1721 +f 1848 1849 1868 +f 1662 1114 490 +f 1669 1787 1682 +f 1656 1618 80 +f 198 1208 916 +f 1440 1928 1401 +f 1521 1369 805 +f 1252 1107 916 +f 1745 678 1672 +f 1703 1779 1721 +f 1750 1465 1808 +f 1609 1644 1643 +f 1092 1114 1662 +f 1826 1523 1793 +f 2262 2261 2224 +f 1696 2166 1767 +f 1016 1648 1521 +f 1208 1252 916 +f 833 688 1067 +f 1794 1803 1558 +f 28 17 512 +f 1750 861 1566 +f 1594 1644 1609 +f 1644 1645 490 +f 490 1645 1662 +f 2229 2262 2224 +f 1602 861 1760 +f 1530 1777 1760 +f 872 1706 1673 +f 1696 1668 2166 +f 1708 1809 1798 +f 1581 1716 1814 +f 1709 1794 1680 +f 1233 1421 1257 +f 1724 1476 1686 +f 1469 1481 1965 +f 1965 1481 1492 +f 2073 1549 1559 +f 1594 1615 1644 +f 1799 1706 1755 +f 1725 1686 1837 +f 1720 1797 1572 +f 1618 2467 2022 +f 1618 1579 80 +f 1648 1016 80 +f 2134 2152 1636 +f 1611 1632 1631 +f 1761 1434 1470 +f 1559 1577 1594 +f 1603 1615 1594 +f 1615 1645 1644 +f 1637 1662 1645 +f 1662 1199 1092 +f 1199 1149 1092 +f 1451 1108 1149 +f 665 734 756 +f 1865 1700 1714 +f 1709 1841 1794 +f 1618 2022 1579 +f 1648 1413 1369 +f 1521 1648 1369 +f 1520 11 1401 +f 1446 1470 1434 +f 1798 1691 1754 +f 2063 1544 2073 +f 2073 1544 1549 +f 1594 1577 1603 +f 1615 1637 1645 +f 1637 1199 1662 +f 1427 1149 1199 +f 2167 1108 1451 +f 1997 1673 1705 +f 1706 1799 1705 +f 1841 1709 1757 +f 1604 1579 2022 +f 1579 707 80 +f 80 707 1648 +f 1520 1401 1491 +f 1649 1520 1491 +f 1435 1434 1885 +f 1470 1469 1461 +f 1481 1508 2024 +f 2370 1544 2063 +f 1549 1568 1559 +f 1559 1568 1577 +f 1603 1610 1615 +f 1615 1610 1637 +f 999 1199 1637 +f 1451 1149 1427 +f 1137 1825 1148 +f 1706 1705 1673 +f 1138 1604 2116 +f 1138 1579 1604 +f 1413 1648 707 +f 2360 2024 1508 +f 598 1075 1116 +f 229 93 1468 +f 1839 1479 1684 +f 2216 2229 2224 +f 1610 1625 1637 +f 329 999 1637 +f 1199 1017 1427 +f 1017 303 1427 +f 303 1451 1427 +f 1792 1754 1777 +f 2309 2391 2301 +f 1655 1138 2116 +f 1138 707 1579 +f 1649 1491 206 +f 1406 1885 1398 +f 1406 1419 1885 +f 1419 1435 1885 +f 1434 1435 1446 +f 1470 1481 1469 +f 1577 1583 1603 +f 999 1017 1199 +f 81 67 941 +f 67 1650 941 +f 1259 1815 2164 +f 1619 2116 2045 +f 1424 707 1138 +f 1702 1649 206 +f 1687 1406 1398 +f 1477 1481 1470 +f 1568 1569 1577 +f 1577 1569 1583 +f 1603 1583 1610 +f 1625 329 1637 +f 2167 340 273 +f 81 273 340 +f 81 962 67 +f 1547 1619 1488 +f 1830 1739 1770 +f 938 1424 1138 +f 1424 1413 707 +f 1527 1649 1702 +f 1527 1520 1649 +f 1527 1268 1520 +f 1250 1406 1687 +f 1441 1353 1115 +f 1203 1413 1051 +f 1250 1419 1406 +f 1477 2372 1481 +f 1481 2372 1508 +f 2449 1560 1568 +f 1549 2449 1568 +f 1568 1560 1569 +f 1569 1584 1583 +f 1652 329 1625 +f 329 817 999 +f 285 1017 999 +f 303 10 1451 +f 10 2167 1451 +f 1412 1650 67 +f 1412 1488 1650 +f 1547 1023 1619 +f 1023 1655 1619 +f 1655 938 1138 +f 1456 1413 1424 +f 1457 1470 1446 +f 1457 1477 1470 +f 329 1652 817 +f 10 340 2167 +f 938 1546 1424 +f 1546 1456 1424 +f 1259 1548 1779 +f 2052 2031 1990 +f 1440 1202 1478 +f 1428 1419 1250 +f 1428 1435 1419 +f 1428 1446 1435 +f 1934 1935 1955 +f 1560 1584 1569 +f 1610 1638 1625 +f 1638 1652 1625 +f 817 1077 999 +f 1077 285 999 +f 980 303 1017 +f 962 1412 67 +f 1494 1023 1547 +f 325 271 1270 +f 1443 1511 1496 +f 1450 1268 1527 +f 1514 1353 1441 +f 1287 1446 1428 +f 1446 1287 1457 +f 1305 2372 1477 +f 1992 1990 1991 +f 1992 1991 1971 +f 1971 1991 1955 +f 2449 1549 2418 +f 1583 1616 1610 +f 1610 1616 1638 +f 10 1396 340 +f 340 1445 81 +f 1445 962 81 +f 1790 984 1753 +f 984 2148 1753 +f 1588 1713 1770 +f 969 978 958 +f 1741 1779 1703 +f 1758 1846 1754 +f 1827 1819 1029 +f 1818 1530 1712 +f 1750 1566 2127 +f 2459 2434 2483 +f 1798 1720 1771 +f 1794 1841 1803 +f 216 1755 1810 +f 1098 1735 1748 +f 1735 1497 1748 +f 1502 2102 1601 +f 881 1502 1601 +f 1455 1839 1744 +f 1706 1709 1680 +f 1212 1741 1703 +f 1788 1969 1671 +f 1075 1074 1692 +f 951 2500 881 +f 2490 2486 2463 +f 1748 1497 1781 +f 1721 984 1840 +f 1815 1259 1741 +f 1626 1756 1837 +f 975 987 1542 +f 2230 2236 2235 +f 1772 678 734 +f 1542 1671 975 +f 1806 1772 1780 +f 678 1772 1806 +f 2218 2225 2268 +f 1828 1732 2007 +f 1526 1688 1531 +f 1752 1526 1554 +f 1844 1818 1712 +f 1823 1846 1804 +f 1781 1669 1704 +f 1721 1779 2143 +f 1770 1670 1526 +f 1497 1669 1781 +f 1098 1713 1735 +f 1742 1815 1741 +f 1526 858 1875 +f 1599 1531 1688 +f 1803 1790 1558 +f 1703 1721 1683 +f 1832 1766 957 +f 1542 1679 1671 +f 1679 1788 1671 +f 1927 1819 1827 +f 1718 1745 1739 +f 1684 1022 1839 +f 1459 1283 1299 +f 1022 1410 1418 +f 2368 2393 2326 +f 1669 1497 1776 +f 1875 858 1212 +f 1739 1745 852 +f 1964 1918 1461 +f 1356 133 1331 +f 1765 1829 1468 +f 858 1742 1741 +f 1006 1674 1021 +f 1723 1936 1935 +f 1468 1713 1098 +f 1724 1678 1476 +f 1680 1783 908 +f 1731 1543 520 +f 1683 1721 1840 +f 1467 1679 1542 +f 1812 1708 1846 +f 1679 1975 1788 +f 1713 1830 1770 +f 1803 1722 1790 +f 2301 2391 2349 +f 1713 1588 1735 +f 1836 1530 1818 +f 1837 1756 861 +f 886 571 556 +f 1181 1805 1812 +f 1706 1680 1755 +f 1677 1729 1775 +f 1776 1787 1669 +f 1526 1670 1672 +f 1727 1770 1526 +f 987 1467 1542 +f 1567 1704 1137 +f 1693 1865 1714 +f 897 1762 912 +f 1135 1697 1062 +f 1697 376 1062 +f 1543 1731 1678 +f 1793 1679 1467 +f 1777 1602 1760 +f 1846 1798 1754 +f 1835 1096 1677 +f 1033 1030 940 +f 1450 1527 1702 +f 1717 376 1697 +f 1711 1717 1697 +f 1717 165 376 +f 1840 984 1790 +f 1669 1746 1704 +f 1669 1682 1746 +f 2301 2349 2308 +f 1882 1444 1898 +f 1820 1789 1730 +f 861 1380 1566 +f 2301 2308 2266 +f 1771 1543 1691 +f 1958 1659 1651 +f 1697 1360 1711 +f 1711 1737 1717 +f 1717 1737 165 +f 1790 1753 1558 +f 1668 1696 1663 +f 1360 1702 1711 +f 1702 1707 1711 +f 1707 1737 1711 +f 1737 1751 165 +f 1444 1782 1693 +f 1716 1787 1599 +f 1744 1839 1022 +f 1898 1444 1785 +f 206 1707 1702 +f 1764 2468 1751 +f 316 1844 893 +f 893 1844 915 +f 1845 1804 1758 +f 1380 861 1756 +f 1780 670 1021 +f 1714 2172 1763 +f 1783 1558 1663 +f 1750 2127 1465 +f 1798 1771 1691 +f 1691 1543 1724 +f 1872 1910 839 +f 1737 2044 1751 +f 1751 2044 1764 +f 1757 1701 482 +f 1725 1602 1777 +f 1836 1845 1530 +f 2102 2470 1503 +f 2496 1899 544 +f 763 2484 946 +f 987 1719 1467 +f 1845 1758 1792 +f 1725 1837 1602 +f 1872 1866 1873 +f 1712 1530 1760 +f 489 1799 216 +f 1760 861 1750 +f 2068 2466 2460 +f 1696 2159 2168 +f 377 1377 1280 +f 1797 1676 1572 +f 1581 2053 1572 +f 1676 1581 1572 +f 1764 2498 2468 +f 2468 2498 1994 +f 1861 1695 1860 +f 2481 2004 2495 +f 1826 1677 1523 +f 1670 1739 852 +f 2234 2269 2253 +f 1724 1543 1678 +f 1658 2168 1791 +f 1397 1747 1719 +f 1696 2168 1658 +f 979 519 272 +f 1774 1975 1679 +f 975 1671 932 +f 1787 1716 1682 +f 1835 1826 1747 +f 2501 2469 961 +f 1810 908 1791 +f 1982 1768 191 +f 1137 1704 1825 +f 1804 1846 1758 +f 2004 2044 1737 +f 913 1969 902 +f 2498 1795 1801 +f 915 1844 1712 +f 1689 915 1712 +f 1740 1752 1541 +f 695 661 199 +f 1865 1693 1782 +f 1824 1464 1809 +f 1829 1765 1718 +f 1816 1768 1982 +f 1816 1622 1768 +f 1622 2165 1681 +f 1768 1622 1681 +f 670 1772 228 +f 1283 1459 52 +f 1785 1444 1749 +f 1675 1075 1685 +f 1567 1781 1704 +f 1858 1857 1848 +f 1526 1752 1688 +f 1791 2160 1810 +f 908 1658 1791 +f 1813 1773 1558 +f 1845 1792 1530 +f 69 376 165 +f 3 1832 1834 +f 1722 1516 1507 +f 1801 1821 1994 +f 1833 1982 2046 +f 1821 1833 2046 +f 1833 1816 1982 +f 1022 1785 1749 +f 2160 2170 1810 +f 1147 1719 1726 +f 1683 1840 1507 +f 1467 1719 1793 +f 1795 1802 1801 +f 1802 1811 1801 +f 1801 1811 1821 +f 1690 2165 1622 +f 1934 1480 1925 +f 229 1468 1091 +f 1780 2164 1742 +f 1672 1742 858 +f 1833 1417 1816 +f 1417 1622 1816 +f 1831 2165 1690 +f 1668 1663 1558 +f 1719 1747 1826 +f 1760 1750 1664 +f 1817 1690 1622 +f 1530 1792 1777 +f 948 1796 1802 +f 1796 1811 1802 +f 1515 1817 1622 +f 1695 1861 1831 +f 1783 1663 1658 +f 1749 1410 1022 +f 854 1796 948 +f 1811 1842 1833 +f 1821 1811 1833 +f 1833 1842 1417 +f 1622 1417 1515 +f 127 1804 1845 +f 1686 1626 1837 +f 1608 1690 1817 +f 1523 1775 1762 +f 127 1845 1836 +f 1812 1805 1708 +f 1523 1677 1775 +f 1780 1772 670 +f 1758 1754 1792 +f 1204 1796 854 +f 1822 1842 1811 +f 1608 1831 1690 +f 1822 1811 1796 +f 1842 1416 1417 +f 1417 1416 1515 +f 1515 1608 1817 +f 1728 1831 1608 +f 908 1783 1658 +f 127 1836 316 +f 1805 1148 1824 +f 852 1745 1672 +f 1478 1093 1928 +f 1822 1843 1842 +f 1843 959 1842 +f 1842 959 1416 +f 1728 1695 1831 +f 1728 1860 1695 +f 2346 446 2337 +f 1602 1837 861 +f 1087 1096 1835 +f 1708 1824 1809 +f 2004 1737 505 +f 1567 1748 1781 +f 520 1543 1883 +f 1760 1664 1712 +f 128 1336 72 +f 2053 1883 1543 +f 1822 180 1843 +f 1786 1608 1515 +f 929 2462 519 +f 512 2402 506 +f 1212 1703 1683 +f 1830 1829 1739 +f 2053 1543 1771 +f 1416 1769 1515 +f 1769 1786 1515 +f 1786 1728 1608 +f 1712 1808 1689 +f 1794 1558 1783 +f 1497 1735 1776 +f 1127 1567 1137 +f 1123 1748 1567 +f 36 205 1185 +f 959 1734 1416 +f 1738 1733 1541 +f 1774 1762 1974 +f 1752 1554 1541 +f 1752 1740 1688 +f 1526 1875 1554 +f 1468 1829 1830 +f 1755 908 1810 +f 1716 1599 1814 +f 1806 1780 1742 +f 2308 2349 2340 +f 1832 915 1689 +f 1713 1468 1830 +f 1814 1599 1346 +f 1832 1689 1766 +f 1022 1684 1785 +f 1093 1153 1116 +f 1672 678 1742 +f 1675 1685 1093 +f 1841 1743 1722 +f 1814 2053 1581 +f 1464 1746 1809 +f 2485 2497 2493 +f 1416 1734 1769 +f 1665 1728 1786 +f 1665 1951 1728 +f 1951 1860 1728 +f 1951 2094 1860 +f 1844 1836 1818 +f 316 1836 1844 +f 1776 1531 1787 +f 1719 1826 1793 +f 2147 1401 29 +f 2111 2121 1548 +f 1741 1259 1779 +f 1843 347 1834 +f 1843 1734 959 +f 1766 1769 1734 +f 957 1766 1734 +f 1766 1786 1769 +f 1766 1689 1786 +f 1689 1665 1786 +f 1754 1691 1777 +f 1507 1840 1790 +f 1761 1470 1461 +f 1523 1679 1793 +f 1091 1468 1098 +f 1820 1730 1838 +f 1843 1834 1734 +f 1808 1951 1665 +f 1588 1727 1531 +f 893 915 1832 +f 1523 1774 1679 +f 272 2488 710 +f 1093 1116 1675 +f 2340 2349 2348 +f 1832 1734 1834 +f 1832 957 1734 +f 1951 1808 2094 +f 1685 1692 1505 +f 1043 295 698 +f 2143 1779 2121 +f 1689 1808 1665 +f 1693 1714 1763 +f 1738 2157 1516 +f 1114 1921 236 +f 1268 1333 1520 +f 1149 1108 431 +f 508 2144 1912 +f 1957 1108 1537 +f 431 1108 1957 +f 1018 1108 2167 +f 1338 1957 1681 +f 2163 1957 1338 +f 1983 1390 2093 +f 30 557 37 +f 1714 2173 2172 +f 1983 1984 1390 +f 1984 2065 1390 +f 884 1762 897 +f 2065 1984 1214 +f 1950 1974 1762 +f 884 1950 1762 +f 2012 1698 1861 +f 1214 2116 803 +f 1950 1938 1974 +f 1938 1967 1974 +f 1900 1761 1461 +f 865 1929 884 +f 884 1929 1950 +f 2062 2071 2042 +f 919 1985 1732 +f 1593 502 2146 +f 1995 1213 2098 +f 1522 2476 1651 +f 2174 1849 2175 +f 1480 1989 1499 +f 1929 1938 1950 +f 1605 2058 1574 +f 2097 1605 2133 +f 1912 2014 1886 +f 2092 2082 2083 +f 206 1930 505 +f 2101 2100 2092 +f 2073 2101 2092 +f 839 1910 865 +f 1910 1901 1929 +f 865 1910 1929 +f 1967 1788 1975 +f 2073 2092 2063 +f 2101 1593 2100 +f 2015 1876 1698 +f 1853 1884 2014 +f 1831 1698 2165 +f 1316 273 81 +f 1901 1920 1929 +f 1929 1920 1938 +f 1920 1968 1967 +f 1938 1920 1967 +f 1849 2174 1700 +f 2173 1700 2174 +f 2062 2072 2091 +f 803 2467 2059 +f 2239 1736 2240 +f 1505 1357 1685 +f 1358 1686 1476 +f 1967 1968 1788 +f 1968 1969 1788 +f 2065 2110 2156 +f 2065 1214 2110 +f 2110 1214 503 +f 273 2093 1018 +f 273 1983 2093 +f 532 1886 2155 +f 2034 2021 1947 +f 216 1810 585 +f 1912 543 2014 +f 1390 2051 1537 +f 1872 1873 1910 +f 1984 2045 1214 +f 597 1912 1886 +f 1593 2146 2100 +f 2071 2062 2090 +f 2034 2046 1982 +f 2034 1947 2046 +f 1214 2045 2116 +f 1873 1887 1910 +f 1887 1901 1910 +f 1562 1447 1106 +f 2163 431 1957 +f 1948 1972 1936 +f 1972 1948 1992 +f 2014 2015 2013 +f 1853 2014 2013 +f 1550 1884 1853 +f 1947 2468 1994 +f 1355 1550 2154 +f 1355 1884 1550 +f 2081 2108 2128 +f 2024 1965 1492 +f 2024 2032 1965 +f 2116 1604 803 +f 1901 1911 1920 +f 1939 1968 1920 +f 1911 1939 1920 +f 872 1626 1666 +f 2062 2091 2120 +f 1819 1927 1759 +f 1021 1674 1780 +f 872 1673 1756 +f 1550 501 2171 +f 1378 1550 2171 +f 2146 2162 2145 +f 1358 482 192 +f 2109 2120 2119 +f 1866 1872 2227 +f 1391 2012 1860 +f 2136 2137 2161 +f 2162 1661 236 +f 1887 1894 1901 +f 1901 1894 1911 +f 505 1707 206 +f 2120 2137 2136 +f 2142 2164 1674 +f 1860 2012 1861 +f 1894 1939 1911 +f 2080 2060 2118 +f 2162 236 508 +f 2164 1815 1742 +f 1018 2093 1537 +f 2154 1378 2161 +f 2041 2098 2491 +f 2043 2042 2032 +f 1108 1018 1537 +f 1465 2094 1808 +f 502 1643 1661 +f 2467 1618 1656 +f 2119 2136 2135 +f 2119 2108 2071 +f 878 1183 1195 +f 2101 1594 1593 +f 2033 2370 2063 +f 2482 2491 2098 +f 1282 2406 1275 +f 2003 1948 1956 +f 2043 2032 2024 +f 2025 2043 2024 +f 2154 1550 1378 +f 1795 2498 1764 +f 2142 1548 2164 +f 2431 2454 2422 +f 1981 2011 1993 +f 2349 2391 2362 +f 502 2162 2146 +f 2025 2024 2360 +f 2129 2120 2091 +f 1732 1985 2007 +f 2171 1308 209 +f 1930 1995 2041 +f 1390 1238 2051 +f 1866 1878 1887 +f 1878 1894 1887 +f 1965 2032 2011 +f 874 2480 2492 +f 2071 2108 2069 +f 1358 1731 482 +f 430 2021 2034 +f 1965 2003 1964 +f 1855 1889 831 +f 1668 1773 2150 +f 1390 2156 1238 +f 898 869 1903 +f 2391 2407 2362 +f 2121 2111 2074 +f 1548 1259 2164 +f 2099 2129 2091 +f 1550 1853 501 +f 1853 1852 501 +f 952 2017 969 +f 2085 2121 2074 +f 2130 2006 1391 +f 2144 1367 543 +f 2100 2146 2099 +f 1545 1319 113 +f 1903 1922 898 +f 1922 1931 898 +f 585 2170 1641 +f 2007 2017 952 +f 2017 2074 969 +f 1558 1753 1813 +f 837 2005 1517 +f 2005 2006 2130 +f 1532 1474 1528 +f 2003 1981 1948 +f 2070 2071 2069 +f 1922 919 1931 +f 2017 2085 2074 +f 2085 2104 2121 +f 2100 2099 2082 +f 2156 2110 2034 +f 505 2474 2004 +f 1903 871 1922 +f 1922 1952 919 +f 919 1952 1985 +f 1985 2001 2007 +f 2001 2036 2017 +f 2007 2001 2017 +f 2017 2036 2085 +f 2036 2047 2085 +f 2047 2075 2085 +f 2075 2104 2085 +f 1948 1993 2023 +f 2400 2422 2407 +f 2011 2070 1993 +f 2033 2043 2025 +f 2012 2015 1698 +f 1876 1338 2165 +f 871 1940 1922 +f 1985 1976 2001 +f 2121 2104 2143 +f 1051 1413 1456 +f 2358 1362 2379 +f 1859 1789 1870 +f 2090 2109 2071 +f 1405 1398 1885 +f 1886 1884 1355 +f 1922 1960 1952 +f 1952 1960 1985 +f 1960 1976 1985 +f 1956 1948 1936 +f 2135 209 2128 +f 2157 1875 1212 +f 2160 2168 2169 +f 1900 1461 1918 +f 2001 2018 2036 +f 2075 2086 2104 +f 2111 2142 2103 +f 1937 1956 1936 +f 2023 2070 2061 +f 2135 2128 2108 +f 2042 2071 2011 +f 2138 413 2383 +f 2033 2072 2043 +f 1922 1940 1960 +f 2070 2069 2061 +f 2069 2108 2061 +f 2108 2119 2135 +f 1855 1904 1889 +f 1889 1904 871 +f 871 1904 1940 +f 1976 2018 2001 +f 2036 2018 2047 +f 2122 2143 2104 +f 216 1642 489 +f 2148 984 2143 +f 1975 1974 1967 +f 2157 1683 1516 +f 1614 1593 1594 +f 2269 2270 2276 +f 1926 2147 29 +f 2082 2091 2072 +f 430 503 2059 +f 1904 1905 1940 +f 1940 1961 1960 +f 1961 1976 1960 +f 2087 2086 2075 +f 2065 2156 1390 +f 1820 1838 1900 +f 534 1308 837 +f 2167 273 1018 +f 831 1850 1855 +f 2019 2037 2018 +f 2018 2037 2047 +f 2037 2075 2047 +f 2086 2095 2104 +f 2095 2122 2104 +f 2122 2148 2143 +f 1926 1213 1995 +f 1405 1885 1761 +f 2006 2013 2012 +f 2211 2233 2216 +f 1855 1890 1904 +f 1904 1895 1905 +f 1905 1932 1940 +f 1961 1977 1976 +f 1976 1986 2018 +f 2484 2476 1518 +f 1870 1411 1859 +f 1548 2142 2111 +f 1904 1890 1895 +f 1895 1932 1905 +f 1940 1932 1961 +f 1976 1977 1986 +f 1986 2008 2018 +f 2018 2008 2019 +f 2087 2075 2037 +f 2087 2095 2086 +f 2094 1391 1860 +f 1852 1853 2006 +f 1853 2013 2006 +f 929 979 850 +f 1855 1874 1890 +f 2008 2028 2019 +f 1993 2070 2023 +f 1705 1799 1998 +f 1491 2147 206 +f 1851 1856 1855 +f 1895 1890 1874 +f 2038 2019 2028 +f 2038 2048 2037 +f 2019 2038 2037 +f 2048 2067 2087 +f 2037 2048 2087 +f 2087 2067 2095 +f 2095 2149 2122 +f 2149 2148 2122 +f 1308 2005 837 +f 209 1308 1387 +f 1601 2102 1927 +f 254 170 201 +f 1800 1403 1763 +f 1510 1346 1740 +f 870 871 1903 +f 1919 1650 1619 +f 2148 1667 1753 +f 1932 1923 1961 +f 1977 1953 1986 +f 2067 2112 2095 +f 2112 2149 2095 +f 2148 2149 1667 +f 2422 2421 2407 +f 1926 2026 1213 +f 1912 2144 543 +f 2128 1387 2153 +f 1733 1510 1740 +f 990 853 2489 +f 503 1214 803 +f 1921 431 2163 +f 2146 2145 2129 +f 2144 1921 2163 +f 1855 1856 1874 +f 1895 1923 1932 +f 1923 1941 1961 +f 1961 1941 1977 +f 2048 2076 2067 +f 2076 2113 2067 +f 2067 2113 2112 +f 1723 1900 1937 +f 1870 1900 1723 +f 1367 2163 1338 +f 520 1346 1510 +f 1698 1831 1861 +f 1984 1919 2045 +f 1895 1891 1923 +f 2008 1986 2028 +f 1948 1981 1993 +f 1883 1346 520 +f 1883 1814 1346 +f 1930 206 2147 +f 2499 2486 1447 +f 1891 1906 1923 +f 1923 1953 1941 +f 1953 1977 1941 +f 1953 1987 1986 +f 2113 2123 2112 +f 2123 2149 2112 +f 1387 1308 1226 +f 1599 1688 1346 +f 2093 1390 1537 +f 2003 2011 1981 +f 1987 2028 1986 +f 2038 2049 2048 +f 2048 2049 2076 +f 1813 1667 2149 +f 2123 1813 2149 +f 1461 1469 1964 +f 1757 1510 1743 +f 505 1930 1999 +f 2223 1784 1789 +f 1532 1522 1651 +f 1906 1913 1923 +f 1913 1943 1923 +f 1943 1942 1923 +f 1923 1942 1953 +f 1942 1987 1953 +f 1308 1852 2005 +f 2053 1814 1883 +f 1733 1740 1541 +f 2154 1886 1355 +f 1503 1528 1474 +f 1874 1879 1895 +f 1895 1879 1891 +f 2076 2124 2113 +f 2113 2124 2123 +f 1896 1891 1879 +f 1891 1896 1906 +f 1942 1962 1987 +f 1962 2009 2028 +f 1987 1962 2028 +f 2009 2038 2028 +f 2109 2119 2071 +f 1918 1956 1937 +f 1851 1864 1856 +f 1896 1897 1906 +f 1906 1897 1913 +f 1943 1962 1942 +f 2049 2077 2076 +f 2124 2125 2123 +f 1930 2147 1926 +f 1902 1894 1878 +f 482 1510 1757 +f 2129 2137 2120 +f 503 803 2059 +f 1847 1857 1851 +f 1851 1857 1864 +f 2039 2038 2009 +f 2038 2039 2049 +f 2076 2077 2124 +f 2150 1813 2123 +f 482 520 1510 +f 1994 1821 2046 +f 2044 2004 1764 +f 1864 1867 1856 +f 1867 1874 1856 +f 1897 1944 1913 +f 1943 1944 1962 +f 2124 2126 2125 +f 2150 2123 2125 +f 2099 2146 2129 +f 2041 1995 2098 +f 1605 1641 2151 +f 1847 1959 1857 +f 1874 1867 1879 +f 1913 1944 1943 +f 1944 1963 1962 +f 2077 2096 2124 +f 2096 2126 2124 +f 2126 2150 2125 +f 941 1650 1919 +f 2135 2136 209 +f 1884 1886 2014 +f 2049 2029 2077 +f 1388 2127 1389 +f 1389 2127 1566 +f 1930 1926 1995 +f 941 1919 1316 +f 2110 503 430 +f 1867 1880 1879 +f 1879 1880 1896 +f 1897 1907 1944 +f 1963 1978 1962 +f 1962 1978 2009 +f 2039 2029 2049 +f 2077 2078 2096 +f 822 823 827 +f 2166 1668 2150 +f 81 941 1316 +f 2204 2216 2203 +f 2011 2071 2070 +f 1880 1892 1896 +f 1892 1907 1897 +f 1896 1892 1897 +f 1907 1914 1944 +f 1978 2010 2009 +f 2010 2039 2009 +f 1688 1740 1346 +f 1789 1820 1870 +f 2130 1391 2094 +f 1944 1945 1963 +f 2029 2078 2077 +f 1767 2150 2126 +f 1767 2166 2150 +f 803 2022 2467 +f 1503 1927 2102 +f 1914 1954 1944 +f 1944 1954 1945 +f 1963 1970 1978 +f 2078 2105 2096 +f 2105 2126 2096 +f 1965 2011 2003 +f 192 1626 1358 +f 2101 1559 1594 +f 1930 2041 1999 +f 1698 1876 2165 +f 1398 1871 891 +f 2165 1338 1681 +f 1970 2010 1978 +f 2010 2030 2029 +f 2039 2010 2029 +f 2030 2055 2078 +f 2029 2030 2078 +f 1849 1848 2175 +f 1871 1862 891 +f 543 2015 2014 +f 1857 1858 1864 +f 1864 1858 1867 +f 1963 1945 1970 +f 2055 2088 2078 +f 2078 2088 2105 +f 2105 2131 2126 +f 2126 2131 1767 +f 2063 2083 2033 +f 2161 2171 209 +f 2032 2042 2011 +f 1813 2150 1773 +f 1914 1908 1954 +f 1970 1979 2010 +f 2088 2131 2105 +f 2015 543 1876 +f 1694 1692 1048 +f 1395 2207 1859 +f 1395 1393 2207 +f 1730 1784 1736 +f 2500 2466 2470 +f 1709 1701 1757 +f 1945 1979 1970 +f 2030 2050 2055 +f 2350 2317 2286 +f 2154 2155 1886 +f 871 860 1889 +f 2161 209 2136 +f 2497 2463 2493 +f 2190 2204 2203 +f 1800 2179 1404 +f 2477 2469 1385 +f 1385 1715 2477 +f 2128 209 1387 +f 1858 1868 1867 +f 1867 1881 1880 +f 1893 1892 1880 +f 1881 1893 1880 +f 1893 1907 1892 +f 1907 1908 1914 +f 1954 1979 1945 +f 1979 1980 2010 +f 2131 2159 1767 +f 1765 93 339 +f 1761 1877 1405 +f 523 1347 515 +f 1541 2157 1738 +f 2144 2163 1367 +f 1380 1389 1566 +f 2317 2392 2316 +f 1994 2498 1801 +f 1867 1868 1881 +f 1980 2050 2030 +f 2010 1980 2030 +f 2050 2089 2055 +f 2055 2089 2088 +f 2088 2114 2131 +f 1538 1651 1659 +f 2145 2155 2129 +f 2140 29 1928 +f 2370 2033 2025 +f 2252 2239 2240 +f 2239 2252 1862 +f 2392 2391 2316 +f 2469 2501 1385 +f 2477 1715 1710 +f 502 1614 1643 +f 2438 1227 2431 +f 1915 1907 1893 +f 1915 1908 1907 +f 1954 1908 1979 +f 1908 1988 1979 +f 1979 1988 1980 +f 2114 2159 2131 +f 2155 2154 2129 +f 508 1966 2144 +f 872 1756 1626 +f 1710 1715 1505 +f 236 1966 508 +f 2272 2284 1398 +f 2325 2355 2319 +f 1548 2121 1779 +f 1532 1528 1522 +f 1980 2056 2050 +f 2050 2056 2089 +f 2013 2015 2012 +f 1964 2003 1956 +f 2006 2012 1391 +f 1565 1927 1503 +f 2244 2243 2226 +f 5 1715 1385 +f 1858 1848 1868 +f 1915 1946 1908 +f 1946 1988 1908 +f 1980 2020 2056 +f 2115 2159 2114 +f 2092 2083 2063 +f 1398 2284 1687 +f 2162 2155 2145 +f 519 2475 2488 +f 2158 5 1385 +f 5 1505 1715 +f 1692 1694 1505 +f 1988 2020 1980 +f 2115 2169 2159 +f 2169 2168 2159 +f 2083 2082 2072 +f 1316 1984 1983 +f 1488 1619 1650 +f 2083 2072 2033 +f 2361 1210 1233 +f 1933 1946 1915 +f 2056 2079 2089 +f 2088 2115 2114 +f 2099 2091 2082 +f 2162 532 2155 +f 1852 2006 2005 +f 2023 2061 2052 +f 2176 2184 2175 +f 2162 985 532 +f 1909 1893 1881 +f 1909 1915 1893 +f 1988 2040 2020 +f 2040 2056 2020 +f 2089 2079 2088 +f 2088 2079 2115 +f 1782 1444 1882 +f 1216 1215 2320 +f 867 1939 1894 +f 867 903 1939 +f 1372 2398 1379 +f 1863 504 2027 +f 2158 1385 504 +f 1868 1782 1881 +f 1909 1933 1915 +f 2040 1988 1946 +f 1481 2024 1492 +f 2120 2136 2119 +f 1522 1528 1518 +f 1871 1398 1405 +f 1221 1408 1399 +f 1357 5 2158 +f 2179 1800 1763 +f 1868 1865 1782 +f 1882 1881 1782 +f 1882 1909 1881 +f 2040 2057 2056 +f 2106 2079 2056 +f 2057 2106 2056 +f 2106 2132 2079 +f 2132 2115 2079 +f 2115 2132 2169 +f 532 985 597 +f 2092 2100 2082 +f 1210 1221 1399 +f 1399 1233 1210 +f 2130 2002 1517 +f 1849 1865 1868 +f 1933 2040 1946 +f 52 1269 30 +f 1667 1813 1753 +f 1997 1380 1673 +f 940 1008 1088 +f 1947 1994 2046 +f 1882 1916 1909 +f 1924 1933 1909 +f 1533 2040 1933 +f 1533 1534 2040 +f 2058 2040 1534 +f 2058 2057 2040 +f 1238 191 1768 +f 1997 1389 1380 +f 1875 1541 1554 +f 1854 504 1863 +f 1854 2158 504 +f 2396 1275 2406 +f 2426 2443 153 +f 1916 1924 1909 +f 1925 1935 1934 +f 1870 1723 1426 +f 2058 2097 2057 +f 2097 2106 2057 +f 2132 2151 2169 +f 2151 2160 2169 +f 1106 1635 1562 +f 1957 1768 1681 +f 1957 2051 1768 +f 526 535 33 +f 1614 1594 1609 +f 2233 2229 2216 +f 2496 2027 2084 +f 2496 1863 2027 +f 2117 1854 1863 +f 2016 2158 1854 +f 2016 1504 1357 +f 2158 2016 1357 +f 1114 236 1661 +f 2129 2154 2137 +f 2133 2106 2097 +f 2491 1999 2041 +f 2051 1238 1768 +f 2061 2108 2081 +f 2189 2195 2186 +f 2348 2349 2362 +f 1701 192 482 +f 505 1737 1707 +f 2133 2132 2106 +f 2132 2133 2151 +f 2151 2170 2160 +f 502 1661 2162 +f 1998 1389 1997 +f 2297 2352 2329 +f 2352 2364 2329 +f 2394 2414 2364 +f 2352 2394 2364 +f 2402 512 2415 +f 2255 2254 2243 +f 2446 1365 2456 +f 2271 2282 2298 +f 846 2283 2264 +f 2293 2310 2318 +f 2254 2295 2294 +f 2283 2290 2278 +f 2270 2294 2293 +f 2423 2455 2400 +f 2281 2287 2267 +f 2190 2191 2204 +f 2271 2263 2282 +f 2334 2329 2364 +f 2424 2432 2409 +f 2282 2263 2298 +f 1409 1659 1958 +f 2263 2302 2298 +f 2297 2329 2296 +f 1256 446 2346 +f 1958 2502 2478 +f 2437 2399 2444 +f 263 2366 2359 +f 849 827 823 +f 2311 2325 2290 +f 2499 2379 2434 +f 2446 2456 2423 +f 947 2358 2379 +f 2499 947 2379 +f 2205 2195 2212 +f 2245 2237 2227 +f 2245 2256 2237 +f 2256 2263 2271 +f 556 571 2305 +f 1528 2068 1518 +f 2424 2439 2432 +f 2302 2352 2297 +f 1866 2237 826 +f 2248 2242 2211 +f 2334 2364 2363 +f 2235 2244 2226 +f 2255 2295 2254 +f 2329 2324 2296 +f 2439 2447 1973 +f 2329 2334 2324 +f 2409 2432 2414 +f 2293 2318 2276 +f 866 2425 2416 +f 1487 1493 2372 +f 2237 2231 2230 +f 2415 512 17 +f 2035 1236 26 +f 921 2138 688 +f 2491 2482 2462 +f 6 181 197 +f 2481 948 1795 +f 2138 2383 2382 +f 2377 2394 2352 +f 2377 506 2394 +f 2394 506 2402 +f 2401 2402 2415 +f 2394 2402 2401 +f 2318 2326 2276 +f 2439 2457 2432 +f 2298 2302 2297 +f 2244 2249 2243 +f 2404 1100 2382 +f 2238 2245 2227 +f 2245 2257 2256 +f 2257 2263 2256 +f 2324 2334 2328 +f 2257 2289 2263 +f 2289 2302 2263 +f 2236 2231 2250 +f 2138 2382 688 +f 2383 2404 2382 +f 1100 2404 2343 +f 2353 2352 2302 +f 2353 2377 2352 +f 2237 2230 2220 +f 2335 2355 2325 +f 2308 2340 2315 +f 2253 2269 2276 +f 2311 2335 2325 +f 2439 2424 511 +f 2268 2267 2248 +f 2383 413 2404 +f 123 971 832 +f 2234 2243 2269 +f 2225 2213 2234 +f 2219 2213 2225 +f 2195 2196 2212 +f 1544 2418 1549 +f 413 866 2404 +f 2404 866 2416 +f 2416 2417 2404 +f 2404 2417 2343 +f 2415 2409 2401 +f 2196 2219 2212 +f 2268 2248 2218 +f 2206 2214 2197 +f 2417 2332 2343 +f 2343 2332 832 +f 2330 2302 2289 +f 2330 2353 2302 +f 2453 2454 515 +f 2218 2248 2217 +f 2218 2217 2205 +f 2276 2281 2268 +f 2178 2197 2177 +f 2197 2189 2177 +f 2332 2066 832 +f 832 2066 123 +f 2231 2236 2230 +f 669 950 1144 +f 2217 2211 2199 +f 1216 1209 1217 +f 2066 2365 123 +f 2230 2226 2214 +f 2290 2325 2304 +f 2325 2319 2304 +f 2217 2248 2211 +f 2191 2192 2199 +f 510 525 2035 +f 2417 1917 2332 +f 2332 1917 2066 +f 2408 2413 2341 +f 2248 2267 2242 +f 2326 2333 2281 +f 1340 2365 2066 +f 2440 1302 1340 +f 2226 2230 2235 +f 1153 1163 1116 +f 2431 2455 2438 +f 2416 2425 2417 +f 2495 2474 2462 +f 2290 2304 2277 +f 825 2227 1872 +f 151 239 1038 +f 9 151 1038 +f 545 928 2381 +f 2440 2406 1384 +f 928 1596 2381 +f 2186 2188 2185 +f 2456 26 1888 +f 2287 2333 2262 +f 2425 2342 2417 +f 2342 1917 2417 +f 1917 877 2066 +f 2336 1340 2066 +f 2336 2440 1340 +f 2328 2351 2327 +f 825 2238 2227 +f 2351 2368 2327 +f 1222 2388 1211 +f 678 756 734 +f 428 263 1343 +f 2188 2191 2190 +f 2341 2376 2333 +f 2066 877 2336 +f 2290 2277 2278 +f 739 634 592 +f 675 304 14 +f 2384 675 14 +f 2199 2211 2204 +f 2191 2199 2204 +f 2322 2318 2310 +f 2287 2262 2233 +f 2185 2188 2184 +f 2386 2425 845 +f 2384 572 675 +f 1128 123 2365 +f 832 971 2343 +f 2188 2186 2191 +f 2185 2184 2176 +f 2345 1917 2342 +f 2345 877 1917 +f 2336 2406 2440 +f 971 1100 2343 +f 2299 2289 2257 +f 2299 2303 2289 +f 2249 2255 2243 +f 506 513 512 +f 2437 955 1219 +f 1587 2398 1324 +f 877 2396 2336 +f 2336 2396 2406 +f 2463 2479 879 +f 2376 2412 2350 +f 2281 2267 2268 +f 2303 2330 2289 +f 624 635 159 +f 1996 2356 1561 +f 2449 2436 1996 +f 2356 2054 2451 +f 928 2398 1587 +f 2333 2350 2262 +f 2035 26 2456 +f 2346 2342 2425 +f 2346 2345 2342 +f 1544 2380 2418 +f 2412 2392 2350 +f 622 509 1151 +f 2436 2054 1996 +f 545 2451 928 +f 2326 2341 2333 +f 2346 2425 2386 +f 1365 2035 2456 +f 2369 2377 2353 +f 2369 506 2377 +f 2451 900 928 +f 900 2398 928 +f 1235 1888 1244 +f 2337 2345 2346 +f 877 772 2396 +f 772 1275 2396 +f 2432 2446 2414 +f 2294 2295 2310 +f 2369 2330 828 +f 2418 2419 2436 +f 2450 2429 2436 +f 2436 2429 2054 +f 2490 2494 1656 +f 1321 155 2338 +f 1256 2346 2386 +f 2448 877 2345 +f 877 2448 772 +f 2446 2423 2414 +f 2351 2334 2363 +f 2243 2254 2269 +f 2380 2419 2418 +f 2419 2450 2436 +f 2283 2278 2264 +f 822 2197 823 +f 1008 1759 1565 +f 2448 2345 2337 +f 2270 2293 2276 +f 2323 2324 2328 +f 2429 1012 2054 +f 2226 2243 2213 +f 2395 325 772 +f 2370 2367 2380 +f 2054 2435 2451 +f 2435 2397 2451 +f 2451 2397 900 +f 1774 1974 1975 +f 2305 2290 2283 +f 846 2305 2283 +f 2320 1215 2285 +f 2139 2448 2337 +f 2448 2395 772 +f 1232 1231 1216 +f 2272 2285 2284 +f 2367 2371 2380 +f 2371 2405 2380 +f 2380 2405 2419 +f 2419 2429 2450 +f 2429 176 1012 +f 2397 2373 900 +f 2373 2398 900 +f 2373 1379 2398 +f 2372 1500 1508 +f 1133 1303 1142 +f 2252 2273 2272 +f 891 2252 2272 +f 2419 2405 2429 +f 2405 2430 2429 +f 2429 2430 176 +f 2189 2186 2181 +f 2212 2219 2218 +f 2312 2139 2337 +f 2139 2384 2448 +f 2448 2384 2395 +f 899 855 843 +f 2272 2273 2285 +f 2331 2303 2299 +f 176 2435 2054 +f 1012 176 2054 +f 2177 2185 2176 +f 2218 2219 2225 +f 1216 1220 1215 +f 2378 2139 2312 +f 2384 14 2395 +f 2324 2295 2255 +f 2240 2273 2252 +f 2371 2387 2405 +f 2410 2430 2405 +f 2430 2442 176 +f 2435 2344 2397 +f 2397 2344 2373 +f 2456 1888 2455 +f 2242 2267 2233 +f 2233 2262 2229 +f 2378 2384 2139 +f 2323 2310 2295 +f 2323 2322 2310 +f 2240 2274 2273 +f 974 841 990 +f 2490 1447 2486 +f 2387 2410 2405 +f 2442 2141 176 +f 2344 1778 2373 +f 972 1379 2373 +f 1778 972 2373 +f 1379 972 428 +f 1211 2437 1223 +f 1228 1215 1220 +f 702 2378 2312 +f 17 518 2415 +f 1888 26 1244 +f 2324 2323 2295 +f 2305 2311 2290 +f 2307 2285 2273 +f 2274 2307 2273 +f 2307 2320 2285 +f 2369 531 506 +f 2435 2258 2344 +f 2296 2324 2288 +f 1233 1217 2361 +f 2360 2371 2367 +f 2410 2442 2430 +f 176 2141 2258 +f 176 2258 2435 +f 539 2331 66 +f 2350 2392 2317 +f 2268 2225 2253 +f 1508 1500 2371 +f 2360 1508 2371 +f 2371 1500 2387 +f 972 2366 428 +f 1626 1686 1358 +f 1759 1807 1819 +f 2277 2257 2245 +f 2277 2299 2257 +f 1784 2228 1736 +f 2265 2240 1736 +f 2228 2265 1736 +f 2265 2274 2240 +f 1209 2320 2307 +f 2320 1209 1216 +f 1555 1584 1560 +f 2387 1500 2372 +f 2410 2420 2442 +f 2433 972 1778 +f 2433 2366 972 +f 955 522 1225 +f 2339 2307 2274 +f 2372 1493 2387 +f 2411 2420 2410 +f 2420 954 2442 +f 2442 954 2141 +f 2344 2433 1778 +f 2205 2212 2218 +f 2328 2334 2351 +f 2394 2401 2414 +f 2250 2256 2271 +f 2339 1209 2307 +f 2328 2322 2323 +f 866 845 2425 +f 3 316 893 +f 2387 2411 2410 +f 2441 2141 954 +f 2141 2441 2258 +f 2354 2433 2344 +f 2254 2294 2270 +f 2269 2254 2270 +f 863 2305 846 +f 2441 2354 2258 +f 2258 2354 2344 +f 2319 2355 51 +f 2223 2228 1784 +f 1493 2411 2387 +f 1560 2449 1555 +f 2288 2324 2255 +f 825 2251 2238 +f 2251 2245 2238 +f 1299 84 1312 +f 2246 2265 2228 +f 2313 2274 2265 +f 2313 2339 2274 +f 2251 2277 2245 +f 2319 51 2331 +f 891 1862 2252 +f 2443 954 2420 +f 2443 2441 954 +f 511 2447 2439 +f 2242 2233 2211 +f 188 15 814 +f 2443 2426 2441 +f 2426 2354 2441 +f 2306 2403 2433 +f 2433 2403 2366 +f 539 2303 2331 +f 2246 2228 2223 +f 1030 1819 1807 +f 2354 2306 2433 +f 2413 2412 2376 +f 2438 2455 1888 +f 1848 1857 2176 +f 2207 2208 2223 +f 2208 2246 2223 +f 1209 2339 1217 +f 2339 2361 1217 +f 1221 1210 2388 +f 554 109 78 +f 386 1375 95 +f 2327 2326 2318 +f 2179 2182 1393 +f 2182 2208 1393 +f 1393 2208 2207 +f 2361 2399 2388 +f 2388 2399 1211 +f 2306 2354 2426 +f 2403 2359 2366 +f 2214 2226 2213 +f 2268 2253 2276 +f 889 2200 2179 +f 2200 2182 2179 +f 2200 2221 2182 +f 2221 2208 2182 +f 2314 2265 2246 +f 2314 2313 2265 +f 2339 2374 2361 +f 2478 2434 2379 +f 2205 2217 2199 +f 2208 2259 2246 +f 2259 2275 2246 +f 2314 2321 2313 +f 2313 2347 2339 +f 2347 2374 2339 +f 2374 2399 2361 +f 153 154 2426 +f 154 2306 2426 +f 2385 2359 2403 +f 2221 2259 2208 +f 2306 2357 2403 +f 2357 2385 2403 +f 2237 2256 2231 +f 2172 2180 889 +f 2180 2200 889 +f 2200 2201 2221 +f 2246 2291 2314 +f 2374 2444 2399 +f 571 555 2311 +f 2192 2205 2199 +f 2173 2180 2172 +f 2279 2246 2275 +f 2279 2291 2246 +f 2292 2314 2291 +f 2321 2362 2313 +f 2362 2347 2313 +f 2347 2389 2374 +f 2444 955 2437 +f 2292 2291 2279 +f 2452 2444 2374 +f 2054 2356 1996 +f 2338 2306 154 +f 2186 2192 2191 +f 2193 2201 2200 +f 2259 2221 2201 +f 2247 2259 2201 +f 2452 955 2444 +f 2278 2277 2251 +f 2338 2357 2306 +f 2181 2186 2185 +f 2276 2326 2281 +f 2432 2457 2446 +f 2198 2201 2193 +f 2198 2232 2201 +f 2232 2247 2201 +f 2389 2452 2374 +f 2452 1630 955 +f 1403 1749 1444 +f 1555 1996 1561 +f 2357 2427 2385 +f 2385 2428 230 +f 2409 2415 2424 +f 2304 2331 2299 +f 2193 2200 2180 +f 2445 2452 2389 +f 1565 1759 1927 +f 2380 1544 2370 +f 2338 2427 2357 +f 2427 2428 2385 +f 230 222 253 +f 2202 2198 2193 +f 2202 2209 2198 +f 2209 2241 2198 +f 2241 2232 2198 +f 2266 2275 2259 +f 2365 1340 1128 +f 2415 518 2424 +f 2338 170 2427 +f 170 2428 2427 +f 2181 2185 2177 +f 2196 2195 2189 +f 2183 2193 2180 +f 2453 1630 2452 +f 2197 2214 2189 +f 2401 2409 2414 +f 822 2220 2197 +f 1210 2361 2388 +f 2187 2193 2183 +f 2187 2202 2193 +f 2266 2279 2275 +f 2279 2300 2292 +f 2375 2347 2362 +f 2375 2390 2347 +f 2390 2389 2347 +f 2453 2452 2445 +f 1347 1630 2453 +f 1630 1347 522 +f 2220 2206 2197 +f 2262 2350 2286 +f 170 254 2428 +f 2457 1973 2446 +f 1973 1365 2446 +f 2174 2183 2180 +f 2194 2202 2187 +f 2222 2241 2209 +f 2222 2260 2241 +f 2266 2259 2247 +f 2390 2445 2389 +f 2264 2251 825 +f 2363 2368 2351 +f 2326 2393 2341 +f 1855 1850 1851 +f 2210 2209 2202 +f 2210 2222 2209 +f 2261 2260 2222 +f 2280 2279 2266 +f 2280 2300 2279 +f 251 263 2359 +f 2277 2304 2299 +f 2220 2230 2206 +f 2202 2194 2210 +f 2213 2243 2234 +f 2328 2327 2322 +f 2294 2310 2293 +f 2214 2196 2189 +f 2196 2213 2219 +f 2224 2222 2210 +f 2421 2390 2375 +f 2206 2230 2214 +f 2194 2203 2210 +f 2224 2261 2222 +f 2421 2445 2390 +f 2322 2327 2318 +f 2393 2408 2341 +f 1365 1973 510 +f 2216 2210 2203 +f 2216 2224 2210 +f 2266 2308 2280 +f 2280 2308 2300 +f 2407 2421 2375 +f 2175 2183 2174 +f 2194 2190 2203 +f 2454 2445 2421 +f 522 1347 523 +f 2456 2455 2423 +f 823 2197 2178 +f 2281 2333 2287 +f 2188 2187 2183 +f 2188 2190 2194 +f 2187 2188 2194 +f 2308 2315 2300 +f 2407 2375 2362 +f 2443 2420 2503 +f 2420 2411 2503 +f 2411 1493 2503 +f 1493 1487 2503 +f 1487 1318 2503 +f 1318 1320 2503 +f 1320 2443 2503 diff --git a/code/threed/bunny.py b/code/threed/bunny.py new file mode 100644 index 0000000..1632660 --- /dev/null +++ b/code/threed/bunny.py @@ -0,0 +1,135 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4)) + M[0, 0] = +2.0 * znear / (right - left) + M[2, 0] = (right + left) / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[3, 2] = -2.0 * znear * zfar / (zfar - znear) + M[2, 3] = -1.0 + return M.T + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(fovy / 360.0 * np.pi) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def scale(x, y, z): + return np.array( + [[x, 0, 0, 0], [0, y, 0, 0], [0, 0, z, 0], [0, 0, 0, 1]], dtype=float + ) + + +def zoom(z): + return scale(z, z, z) + + +def translate(x, y, z): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=float + ) + + +def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=float + ) + + +def obj_load(filename): + V, Vi = [], [] + with open(filename) as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + Vi.append([int(x) for x in values[1:4]]) + return np.array(V), np.array(Vi) - 1 + + +# ----------------------------------------------------------------------------- + +# Loading and centering +V, Vi = obj_load("bunny.obj") +V = (V - (V.max(axis=0) + V.min(axis=0)) / 2) / max(V.max(axis=0) - V.min(axis=0)) + +# Computing model-view-projection matrix +model = zoom(1.5) @ xrotate(20) @ yrotate(45) +view = translate(0, 0, -4.5) +proj = perspective(25, 1, 1, 100) +MVP = proj @ view @ model + +# Applying MVP +VH = np.c_[V, np.ones(len(V))] # Homogenous coordinates +VT = VH @ MVP.T # Transformed coordinates +VN = VT / VT[:, 3].reshape(-1, 1) # Normalization +VS = VN[:, :3] # Normalized device coordinates + +# Actual faces +V = VS[Vi] + +# Backface culling +CW = ( + (V[:, 1, 0] - V[:, 0, 0]) * (V[:, 1, 1] + V[:, 0, 1]) + + (V[:, 2, 0] - V[:, 1, 0]) * (V[:, 2, 1] + V[:, 1, 1]) + + (V[:, 0, 0] - V[:, 2, 0]) * (V[:, 0, 1] + V[:, 2, 1]) +) +V = V[CW < 0] + +# Rendering as a collection of polygons (triangles) +segments = V[:, :, :2] +zbuffer = -V[:, :, 2].mean(axis=1) + +# Color according to depth +zmin, zmax = zbuffer.min(), zbuffer.max() +zbuffer = (zbuffer - zmin) / (zmax - zmin) +colors = plt.get_cmap("magma")(zbuffer) + +# Sort triangles according to z buffer +I = np.argsort(zbuffer) +segments, colors = segments[I, :], colors[I, :] + +# Actual rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1) +ax.axis("off") + +for fc, ec, lw in [ + ("None", "black", 6.0), + ("None", "white", 3.0), + (colors, "black", 0.25), +]: + collection = PolyCollection( + segments, closed=True, linewidth=lw, facecolor=fc, edgecolor=ec + ) + ax.add_collection(collection) + +plt.savefig("../../figures/threed/bunny.pdf", transparent=True) +plt.show() diff --git a/code/typography/projection-3d-gaussian.py b/code/typography/projection-3d-gaussian.py new file mode 100644 index 0000000..b660b32 --- /dev/null +++ b/code/typography/projection-3d-gaussian.py @@ -0,0 +1,331 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.text import TextPath +from matplotlib.patches import PathPatch +from matplotlib.colors import LightSource +from matplotlib.transforms import Affine2D +from mpl_toolkits.mplot3d import Axes3D, art3d +from matplotlib.font_manager import FontProperties + + +def text3d(ax, xyz, s, zdir="z", size=0.1, angle=0, **kwargs): + x, y, z = xyz + if zdir == "y": + xy, z = (x, z), y + elif zdir == "x": + xy, z = (y, z), x + else: + xy, z = (x, y), z + path = TextPath((0, 0), s, size=size, prop=FontProperties(family="Roboto")) + V = path.vertices + V[:, 0] -= (V[:, 0].max() - V[:, 0].min()) / 2 + trans = Affine2D().rotate(angle).translate(xy[0], xy[1]) + path = PathPatch(trans.transform_path(path), **kwargs) + ax.add_patch(path) + art3d.pathpatch_2d_to_3d(path, z=z, zdir=zdir) + + +fig = plt.figure(figsize=(6, 6)) +ax = plt.subplot(1, 1, 1, projection="3d") +ax.view_init(elev=30, azim=-55) +ax.set_axis_off() +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +ax.set_zlim(0, 1) + +xrange = 0, 5 +yrange = 0, 5 +zrange = 0, 2 +xticks = (1.0, 0.2) +yticks = (1.0, 0.2) +zticks = (1.0, 0.2) + +maxrange = max(xrange[1] - xrange[0], yrange[1] - yrange[0], zrange[1] - zrange[0]) +xtransform = lambda x: (x - xrange[0]) / maxrange +ytransform = lambda y: (y - yrange[0]) / maxrange +ztransform = lambda z: (z - zrange[0]) / maxrange +xmin, xmax = xtransform(xrange[0]), xtransform(xrange[1]) +ymin, ymax = ytransform(yrange[0]), ytransform(yrange[1]) +zmin, zmax = ztransform(zrange[0]), ztransform(zrange[1]) + + +# Minor gridlines +linewidth, color = 0.25, "0.75" +for x in np.arange(xrange[0], xrange[1] + xticks[1], xticks[1]): + x = xtransform(x) + ax.plot( + [x, x, x], + [ymin, ymax, ymax], + [zmin, zmin, zmax], + linewidth=linewidth, + color=color, + zorder=-50, + ) +for y in np.arange(yrange[0], yrange[1] + yticks[1], yticks[1]): + y = ytransform(y) + ax.plot( + [xmax, xmin, xmin], + [y, y, y], + [zmin, zmin, zmax], + linewidth=linewidth, + color=color, + zorder=-50, + ) +for z in np.arange(zrange[0], zrange[1] + zticks[1], zticks[1]): + z = ztransform(z) + ax.plot( + [xmax, xmin, xmin], + [ymax, ymax, ymin], + [z, z, z], + linewidth=linewidth, + color=color, + zorder=-50, + ) + +# Major gridlines +linewidth, color = 0.50, "0.50" +for x in np.arange(xrange[0], xrange[1] + xticks[0], xticks[0]): + x = xtransform(x) + ax.plot( + [x, x, x], + [ymin, ymax, ymax], + [zmin, zmin, zmax], + linewidth=linewidth, + color=color, + zorder=-25, + ) +for y in np.arange(yrange[0], yrange[1] + yticks[0], yticks[0]): + y = ytransform(y) + ax.plot( + [xmax, xmin, xmin], + [y, y, y], + [zmin, zmin, zmax], + linewidth=linewidth, + color=color, + zorder=-25, + ) +for z in np.arange(zrange[0], zrange[1] + zticks[0], zticks[0]): + z = ztransform(z) + ax.plot( + [xmax, xmin, xmin], + [ymax, ymax, ymin], + [z, z, z], + linewidth=linewidth, + color=color, + zorder=-25, + ) + +# Frame +linewidth, color = 1.00, "0.00" +ax.plot( + [xmin, xmin, xmax, xmax, xmin], + [ymin, ymax, ymax, ymin, ymin], + [zmin, zmin, zmin, zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-10, +) +ax.plot( + [xmin, xmin, xmax, xmax, xmin], + [ymax, ymax, ymax, ymax, ymax], + [zmin, zmax, zmax, zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-10, +) +ax.plot( + [xmin, xmin, xmin, xmin, xmin], + [ymin, ymin, ymax, ymax, ymin], + [zmin, zmax, zmax, zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-10, +) + +# Minor ticks +linewidth, length, color = 0.25, 0.02, "0.5" +for x in np.arange(xrange[0], xrange[1] + xticks[1], xticks[1]): + x = xtransform(x) + ax.plot( + [x, x], + [ymin, ymin - length], + [zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-50, + ) +for y in np.arange(yrange[0], yrange[1] + yticks[1], yticks[1]): + y = ytransform(y) + ax.plot( + [xmax, xmax + length], + [y, y], + [zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-50, + ) +for z in np.arange(zrange[0], zrange[1] + zticks[1], zticks[1]): + z = ztransform(z) + ax.plot( + [xmin, xmin - length], + [ymin, ymin], + [z, z], + linewidth=linewidth, + color=color, + zorder=-50, + ) + +# Major ticks & labels +linewidth, length, color = 0.75, 0.04, "0.0" +for x in np.arange(xrange[0], xrange[1] + xticks[0], xticks[0]): + x_ = xtransform(x) + ax.plot( + [x_, x_], + [ymin, ymin - length], + [zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-50, + ) + text3d( + ax, + (x_, ymin - 2 * length, zmin), + "%.1f" % x, + size=0.04, + facecolor="black", + edgecolor="None", + ) +for y in np.arange(yrange[0], yrange[1] + yticks[0], yticks[0]): + y_ = ytransform(y) + ax.plot( + [xmax, xmax + length], + [y_, y_], + [zmin, zmin], + linewidth=linewidth, + color=color, + zorder=-50, + ) + text3d( + ax, + (xmax + 2 * length, y_, zmin), + "%.1f" % y, + angle=np.pi / 2, + size=0.04, + facecolor="black", + edgecolor="None", + ) +for z in np.arange(zrange[0], zrange[1] + zticks[1], zticks[0]): + z_ = ztransform(z) + ax.plot( + [xmin, xmin - length], + [ymin, ymin], + [z_, z_], + linewidth=linewidth, + color=color, + zorder=-50, + ) + text3d( + ax, + (xmin - 2 * length, ymin, z_), + "%.1f" % z, + zdir="y", + size=0.04, + facecolor="black", + edgecolor="None", + ) + + +# Data +n = 101 +X = Y = np.linspace(-1, 1, n) +X, Y = np.meshgrid(X, Y) +R = np.sqrt(5 * (X ** 2 + Y ** 2)) +Z = 0.5 * np.exp(-0.5 * R * R) +X, Y = (1 + X) / 2, (1 + Y) / 2 + +# Plot +C = np.ones((n, n, 3)) +I1 = LightSource(azdeg=0, altdeg=25).hillshade(Z).reshape(n, n, 1) +I2 = LightSource(azdeg=120, altdeg=25).hillshade(Z).reshape(n, n, 1) +I3 = LightSource(azdeg=240, altdeg=25).hillshade(Z).reshape(n, n, 1) +C = 0.5 * C + (I1 * (1, 0, 0) + I2 * (0, 1, 0) + I3 * (0, 0, 1)) +C = np.minimum(C, (1, 1, 1)) +C[::2, ::2] = C[1::2, 1::2] = 0.0, 0.0, 0.0 + + +# ax.plot_wireframe(X, Y, Z, rstride=n-1, cstride=n-1, color="black", +# linewidth=3, antialiased=True, zorder=25) +ax.plot_wireframe( + X, + Y, + Z, + rstride=1, + cstride=1, + color="black", + linewidth=5, + antialiased=True, + zorder=10, +) +ax.plot_wireframe( + X, + Y, + Z, + rstride=1, + cstride=1, + color="white", + linewidth=2, + antialiased=True, + zorder=20, +) +ax.plot_surface( + X, + Y, + Z, + rstride=5, + cstride=5, + facecolors=C, + shade=False, + edgecolor="None", + linewidth=0, + antialiased=True, + zorder=30, +) + +text3d( + ax, + ((xmin + xmax) / 2, ymax, 1.05 * zmax), + "X axis", + zdir="y", + size=0.05, + facecolor="black", + edgecolor="None", +) +text3d( + ax, + (xmin, (ymin + ymax) / 2, 1.05 * zmax), + "Y axis", + zdir="x", + size=0.05, + facecolor="black", + edgecolor="None", +) +text3d( + ax, + (1.05 * xmax, ymax, (zmin + zmax) / 2), + "Z axis", + angle=np.pi / 2, + zdir="y", + size=0.05, + facecolor="black", + edgecolor="None", +) + + +plt.tight_layout() +plt.savefig("../../figures/typography/projection-3d-gaussian.pdf") +plt.show() diff --git a/code/typography/text-outline.py b/code/typography/text-outline.py new file mode 100644 index 0000000..7fc8f21 --- /dev/null +++ b/code/typography/text-outline.py @@ -0,0 +1,58 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patheffects import Stroke, Normal + +fig = plt.figure(figsize=(8, 3)) +ax = fig.add_axes([0, 0, 1, 1], frameon=False) +family = "Pacifico" +size = 100 +cmap = plt.cm.magma +text = "Matplotlib" + +for x in np.linspace(0, 1, 20): + lw, color = x * 225, cmap(1 - x) + t = ax.text( + 0.5, + 0.45, + text, + size=size, + color="none", + weight="bold", + va="center", + ha="center", + family=family, + zorder=-lw, + ) + t.set_path_effects([Stroke(linewidth=lw + 1, foreground="black")]) + t = ax.text( + 0.5, + 0.45, + text, + size=size, + color="black", + weight="bold", + va="center", + ha="center", + family=family, + zorder=-lw + 1, + ) + t.set_path_effects([Stroke(linewidth=lw, foreground=color)]) +t = ax.text( + 1.0, + 0.01, + "https://matplotlib.org ", + va="bottom", + ha="right", + size=10, + color="white", + family="Roboto", + alpha=0.50, +) + +plt.savefig("../../figures/typography/text-outline.pdf") +plt.show() diff --git a/code/typography/text-starwars.py b/code/typography/text-starwars.py new file mode 100644 index 0000000..f40ba0d --- /dev/null +++ b/code/typography/text-starwars.py @@ -0,0 +1,57 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate the use of TextPath and access to raw vertices +# ---------------------------------------------------------------------------- +import matplotlib.pyplot as plt +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch + +fig = plt.figure(figsize=(4.25, 2)) +ax = fig.add_axes([0, 0, 1, 1], aspect=1, xlim=[-40, 40], ylim=[-1, 25]) +ax.axis("off") + +text = [ + "Beautiful is better than ugly.", + "Explicit is better than implicit.", + "Simple is better than complex.", + "Complex is better than complicated.", + "Flat is better than nested.", + "Sparse is better than dense.", + "Readability counts.", +] + +y = 0 +size = 6 +xfactor = 1 / 50 +yfactor = 1 / 120 + +for i, line in enumerate(text[::-1]): + path = TextPath((0, 0), line, size=size) + V = path.vertices + xmin, xmax = V[:, 0].min(), V[:, 0].max() + ymin, ymax = V[:, 1].min(), V[:, 1].max() + + # X centering + V[:, 0] -= (xmax + xmin) / 2 + + # Moving whole text at y coordinates + V[:, 1] += y + + # Rescaling along y + V[:, 1] *= 1 - (V[:, 1] * yfactor) + + # Rescaling along x + V[:, 0] *= 1 - (V[:, 1] * xfactor) + + # Update interlines + y += size * (1 - ymin * yfactor) + + # Display + patch = PathPatch(path, facecolor="%.2f" % (i / 10), linewidth=0, clip_on=False) + ax.add_artist(patch) + +plt.savefig("../../figures/typography/text-starwars.pdf") +plt.show() diff --git a/code/typography/tick-labels-variation.py b/code/typography/tick-labels-variation.py new file mode 100644 index 0000000..672d154 --- /dev/null +++ b/code/typography/tick-labels-variation.py @@ -0,0 +1,105 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + + +# Style +# ----------------------------------------------------------------------------- +plt.rc("font", family="Roboto Condensed") +plt.rc("xtick", labelsize="small") +plt.rc("ytick", labelsize="small") +plt.rc("axes", labelsize="medium", titlesize="medium") + +fig = plt.figure(figsize=(8, 2)) + +# +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 1, xlim=[0, 1], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.05)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.01)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) +ax.tick_params(axis="both", which="major") +labels = ax.get_xticklabels() +for i, label in enumerate(labels): + if i in [1, 6, 11, 16, 21]: + label.set_font("Roboto") + label.set_size(8.5) + +# +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 2, xlim=[0, 1], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.05)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.01)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) +labels = ax.get_xticklabels() +for i, label in enumerate(labels): + if i not in [1, 6, 11, 16, 21]: + label.set_size(7) + +# +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 3, xlim=[0, 1], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.05)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.01)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) +labels = ax.get_xticklabels() +for i, label in enumerate(labels): + if i not in [1, 6, 11, 16, 21]: + label.set_weight(200) + + +# +# ----------------------------------------------------------------------------- +ax = plt.subplot(4, 1, 4, xlim=[0, 1], ylim=[0, 1]) +ax.patch.set_facecolor("none") +ax.set_yticks([]) +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) +ax.xaxis.set_major_locator(ticker.MultipleLocator(0.05)) +ax.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.01)) +ax.xaxis.set_minor_formatter(plt.NullFormatter()) + +labels = ax.get_xticklabels() +for i, label in enumerate(labels): + if i in [1, 6, 11, 16, 21]: + label.set_color("white") + label.set_weight(600) + label.set_bbox( + dict( + boxstyle="round,pad=.25", + linewidth=0.5, + facecolor="black", + edgecolor="black", + ) + ) + +# Show +# ----------------------------------------------------------------------------- +plt.tight_layout() +plt.savefig("../../figures/typography/tick-labels-variation.pdf") +plt.show() diff --git a/code/typography/typography-font-stacks.py b/code/typography/typography-font-stacks.py new file mode 100644 index 0000000..5d76c3d --- /dev/null +++ b/code/typography/typography-font-stacks.py @@ -0,0 +1,70 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import os +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.font_manager import findfont, FontProperties + + +def font_text(x, y, text, family, dy=0): + + ax.text(x, y + dy, text, family=family, size=size, va="bottom") + font = findfont(FontProperties(family=family)) + filename = os.path.basename(font) + ax.text( + x, + y, + filename, + va="top", + family="Roboto Condensed", + weight=400, + size=10, + color="0.5", + ) + + +plt.figure(figsize=(8, 2), dpi=100) +size = 20 +dx = 15 +xmin, xmax = -1, 4 * dx - 5 + +ax = plt.subplot( + 3, 1, 1, frameon=False, xlim=(xmin, xmax), ylim=(-1, 1), xticks=[], yticks=[] +) +font_text(0 * dx, 0, "Serif", "Serif") +font_text(1 * dx, 0, "Sans", "Sans") +font_text(2 * dx, 0, "Monospace", "Monospace") +font_text(3 * dx, 0, "Cursive", "Cursive", -0.25) + + +ax = plt.subplot( + 3, 1, 2, frameon=False, xlim=(xmin, xmax), ylim=(-1, 1), xticks=[], yticks=[] +) +font_text(0 * dx, 0, "Serif", "Roboto Slab") +font_text(1 * dx, 0, "Sans", "Roboto Condensed") +font_text(2 * dx, 0, "Monospace", "Roboto Mono") +font_text(3 * dx, 0, "Cursive", "Merienda", -0.25) + + +ax = plt.subplot( + 3, 1, 3, frameon=False, xlim=(xmin, xmax), ylim=(-1, 1), xticks=[], yticks=[] +) +font_text(0 * dx, 0, "Serif", "Source Serif Pro") +font_text(1 * dx, 0, "Sans", "Source Sans Pro") +font_text(2 * dx, 0, "Monospace", "Source Code Pro") +font_text(3 * dx, 0, "Cursive", "ITC Zapf Chancery") + +# ax.text(0*dx, 0, "Serif", family="Roboto Slab", size=size) +# ax.text(0*dx, -0.5, find("Roboto Slab"), +# family="Roboto Slab", weight=300, size=8, color="0.25") + +# ax.text(1*dx, 0, "Sans-serif", family="Roboto Condensed", size=size) +# ax.text(2*dx, 0, "Monospace", family="Roboto Mono", size=size) +# ax.text(3*dx, 0, "Cursive", family="Pacifico", size=size) + +plt.tight_layout() +plt.savefig("../../figures/typography/typography-font-stacks.pdf", dpi=100) +plt.show() diff --git a/code/typography/typography-legibility.py b/code/typography/typography-legibility.py new file mode 100644 index 0000000..ea01adc --- /dev/null +++ b/code/typography/typography-legibility.py @@ -0,0 +1,74 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import os +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patheffects import Stroke, Normal + +dpi = 200 +figsize = (4.25, 3.0) + +shape = 20, 80 +I = np.random.uniform(0, 1, shape) +cmap = plt.cm.get_cmap("gray") +x, y = 0.5, 2 + +fig = plt.figure(figsize=figsize, dpi=dpi) + +# Regular +ax = fig.add_subplot(4, 2, 1, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text(x, y, "Read me", color="black", transform=ax.transData) + +ax = fig.add_subplot(4, 2, 2, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text(x, y, "Read me", color="white", transform=ax.transData) + +# Bold +ax = fig.add_subplot(4, 2, 3, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text(x, y, "Read me", color="black", weight="bold", transform=ax.transData) + +ax = fig.add_subplot(4, 2, 4, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text(x, y, "Read me", color="white", weight="bold", transform=ax.transData) + +# Boxes +ax = fig.add_subplot(4, 2, 5, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text( + x, + y, + "Read me", + color="black", + transform=ax.transData, + bbox={"facecolor": "white", "edgecolor": "None", "pad": 1, "alpha": 0.75}, +) + +ax = fig.add_subplot(4, 2, 6, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +ax.text( + x, + y, + "Read me", + color="white", + transform=ax.transData, + bbox={"facecolor": "black", "edgecolor": "None", "pad": 1, "alpha": 0.75}, +) + +ax = fig.add_subplot(4, 2, 7, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +text = ax.text(x, y, "Read me", color="black", transform=ax.transData) +text.set_path_effects([Stroke(linewidth=1.5, foreground="white"), Normal()]) + +ax = fig.add_subplot(4, 2, 8, xticks=[], yticks=[]) +ax.imshow(I, cmap=cmap, origin="lower") +text = ax.text(x, y, "Read me", color="white", transform=ax.transData) +text.set_path_effects([Stroke(linewidth=1.5, foreground="black"), Normal()]) + +plt.tight_layout() +plt.savefig("../../figures/typography/typography-legibility.pdf", dpi=600) +plt.show() diff --git a/code/typography/typography-math-stacks.py b/code/typography/typography-math-stacks.py new file mode 100644 index 0000000..c2e47a9 --- /dev/null +++ b/code/typography/typography-math-stacks.py @@ -0,0 +1,51 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import os +import numpy as np +import matplotlib.pyplot as plt + +plt.rcParams["text.usetex"] = False + + +def plot(family): + plt.rcParams["mathtext.fontset"] = family + fig = plt.figure(figsize=(3, 1.75), dpi=100) + ax = plt.subplot( + 1, 1, 1, frameon=False, xlim=(-1, 1), ylim=(-0.5, 1.5), xticks=[], yticks=[] + ) + ax.text( + 0, + 0.0, + r"$\frac{\pi}{4} = \sum_{k=0}^\infty\frac{(-1)^k}{2k+1}$", + size=32, + ha="center", + va="bottom", + ) + ax.text( + 0, + -0.1, + 'mathtext.fontset = "%s"' % family, + size=14, + ha="center", + va="top", + family="Roboto Condensed", + color="0.5", + ) + + # plt.tight_layout() + plt.savefig("../../figures/typography/typography-math-%s.pdf" % family, dpi=600) + plt.show() + + +plot("cm") +plot("stix") +plot("stixsans") +plot("dejavusans") +plot("dejavuserif") +plot("custom") + +# Process result with +# pdfjam --nup 3x2 ../figures/typography-math-cm.pdf ../figures/typography-math-custom.pdf ../figures/typography-math-dejavusans.pdf ../figures/typography-math-dejavuserif.pdf ../figures/typography-math-stix.pdf ../figures/typography-math-stixsans.pdf --outfile ../figures/typography-math-stacks.pdf; pdfcrop ../figures/typography-math-stacks.pdf ../figures/typography-math-stacks.pdf diff --git a/code/typography/typography-matters.py b/code/typography/typography-matters.py new file mode 100644 index 0000000..76ab14a --- /dev/null +++ b/code/typography/typography-matters.py @@ -0,0 +1,95 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib +from matplotlib import ticker +import matplotlib.pyplot as plt +import matplotlib.patheffects as path_effects + +plt.figure(figsize=(8, 5), dpi=100) + +ax = plt.subplot(2, 1, 1, aspect=0.25) +ax.text( + 0.5, + 0.5, + "Typography matters", + family="Times New Roman", + size=60, + color=".85", + horizontalalignment="center", + verticalalignment="center", +) +ax.set_title( + "Typography is an important dimension of a scientific figure", + x=0, + y=0.965, + horizontalalignment="left", + verticalalignment="baseline", +) +ax.set_xticks(np.linspace(0, 1, 21)) + + +ax = plt.subplot(2, 1, 2, aspect=0.25) +font = matplotlib.font_manager.FontProperties( + family="Roboto Condensed", weight="regular", size=9 +) +ax.set_xticks(np.linspace(0, 1, 21)) +ax.set_yticks(np.linspace(0, 1, 5)) + +for label in ax.get_xticklabels(): + label.set_fontproperties(font) +for label in ax.get_yticklabels(): + label.set_fontproperties(font) + +for label in ax.get_xticklabels()[1:-1:2]: + label.set_size(7.5) + label.set_weight("light") + +for label in ax.get_yticklabels()[1:-1:]: + label.set_size(7.5) + label.set_weight("light") + +ax.text( + 0.5, + 0.5, + "Typography", + family="Times New Roman", + size=80, + color=".85", + horizontalalignment="center", + verticalalignment="center", +) +ax.text( + 0.8375, + 0.725, + "matters", + family="Times New Roman", + size=30, + color="0.25", + horizontalalignment="center", + verticalalignment="center", +) + +font = matplotlib.font_manager.FontProperties( + family="Roboto Slab", weight="regular", size=15 +) +text = ax.set_title( + "Typography is an important dimension of a scientific figure", + fontproperties=font, + x=0, + y=0.975, + horizontalalignment="left", + verticalalignment="baseline", +) +text.set_path_effects( + [path_effects.Stroke(linewidth=2, foreground="white"), path_effects.Normal()] +) + + +plt.tight_layout() +plt.savefig("../../figures/typography/typography-matters.pdf", dpi=100) +plt.savefig("../../figures/typography/typography-matters.png", dpi=300) +plt.show() diff --git a/code/typography/typography-text-path.py b/code/typography/typography-text-path.py new file mode 100644 index 0000000..01300b5 --- /dev/null +++ b/code/typography/typography-text-path.py @@ -0,0 +1,113 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import scipy +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch +from matplotlib.font_manager import FontProperties + + +def interpolate(X, Y, T): + dR = (np.diff(X) ** 2 + np.diff(Y) ** 2) ** 0.5 + R = np.zeros_like(X) + R[1:] = np.cumsum(dR) + return np.interp(T, R, X), np.interp(T, R, Y), R[-1] + + +def contour(X, Y, text, offset=0): + + # Interpolate text along curve + # X0,Y0 for position + X1,Y1 for normal vectors + path = TextPath( + (0, -0.75), text, prop=FontProperties(size=2, family="Roboto", weight="bold") + ) + V = path.vertices + X0, Y0, D = interpolate(X, Y, offset + V[:, 0]) + X1, Y1, _ = interpolate(X, Y, offset + V[:, 0] + 0.1) + + # Here we interpolate the original path to get the "remainder" + # (path minus text) + X, Y, _ = interpolate(X, Y, np.linspace(V[:, 0].max() + 1, D - 1, 200)) + plt.plot( + X, Y, color="black", linewidth=0.5, markersize=1, marker="o", markevery=[0, -1] + ) + + # Transform text vertices + dX, dY = X1 - X0, Y1 - Y0 + norm = np.sqrt(dX ** 2 + dY ** 2) + dX, dY = dX / norm, dY / norm + X0 += -V[:, 1] * dY + Y0 += +V[:, 1] * dX + V[:, 0], V[:, 1] = X0, Y0 + + # Faint outline + patch = PathPatch( + path, + facecolor="white", + zorder=10, + alpha=0.25, + edgecolor="white", + linewidth=1.25, + ) + ax.add_artist(patch) + + # Actual text + patch = PathPatch( + path, facecolor="black", zorder=30, edgecolor="black", linewidth=0.0 + ) + ax.add_artist(patch) + + +# Some data +n = 64 +X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), +) +Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + +def f(x, y): + return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2) - y ** 2) + + +n = 100 +x = np.linspace(-3, 3, n) +y = np.linspace(-3, 3, n) +X, Y = np.meshgrid(x, y) +Z = 0.5 * f(X, Y) + + +fig = plt.figure(figsize=(10, 5), dpi=100) +levels = 10 + +# Regular contour with straight labels +ax = fig.add_subplot(1, 2, 1, aspect=1, xticks=[], yticks=[]) +CF = plt.contourf(Z, origin="lower", levels=levels) +CS = plt.contour(Z, origin="lower", levels=levels, colors="black", linewidths=0.5) +ax.clabel(CS, CS.levels) + +# Regular contour with curved labels +# ! aspect=1 is critical here, else text path would be deformed +ax = fig.add_subplot(1, 2, 2, aspect=1, xticks=[], yticks=[]) +CF = plt.contourf(Z, origin="lower", levels=levels) +CS = plt.contour( + Z, origin="lower", levels=levels, alpha=0, colors="black", linewidths=0.5 +) + +for level, collection in zip(CS.levels[:], CS.collections[:]): + for path in collection.get_paths(): + V = np.array(path.vertices) + text = "%.3f" % level + if level == 0.0: + text = " DO NOT CROSS •••" * 8 + contour(V[:, 0], V[:, 1], text) + +plt.tight_layout() +plt.savefig("../../figures/typography/typography-text-path.png", dpi=600) +plt.savefig("../../figures/typography/typography-text-path.pdf", dpi=600) +plt.show() diff --git a/code/unsorted/3d/README.md b/code/unsorted/3d/README.md new file mode 100644 index 0000000..01e73ff --- /dev/null +++ b/code/unsorted/3d/README.md @@ -0,0 +1,2 @@ + +Temporary non commented / documented /cleaned code !!! diff --git a/code/unsorted/3d/__pycache__/glm.cpython-37.pyc b/code/unsorted/3d/__pycache__/glm.cpython-37.pyc new file mode 100644 index 0000000..6f7bf00 Binary files /dev/null and b/code/unsorted/3d/__pycache__/glm.cpython-37.pyc differ diff --git a/code/unsorted/3d/__pycache__/plot.cpython-37.pyc b/code/unsorted/3d/__pycache__/plot.cpython-37.pyc new file mode 100644 index 0000000..bbc1570 Binary files /dev/null and b/code/unsorted/3d/__pycache__/plot.cpython-37.pyc differ diff --git a/code/unsorted/3d/axis.py b/code/unsorted/3d/axis.py new file mode 100644 index 0000000..16d6d69 --- /dev/null +++ b/code/unsorted/3d/axis.py @@ -0,0 +1,13 @@ +import glm +import plot +import matplotlib.pyplot as plt + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_axes([0, 0, 1, 1]) + camera = glm.camera(25, 45, 1, "perspective") + plot.axis(ax, camera) + plt.show() diff --git a/code/unsorted/3d/bar.py b/code/unsorted/3d/bar.py new file mode 100644 index 0000000..4d5cf23 --- /dev/null +++ b/code/unsorted/3d/bar.py @@ -0,0 +1,29 @@ +import glm +import plot +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + + n = 24 + X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + ) + Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + cmap = plt.get_cmap("magma") + norm = mpl.colors.Normalize(vmin=-0.25, vmax=0.75) + facecolors = cmap(norm(Y))[..., :3] + + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_axes([0, 0, 1, 1]) + + camera = glm.camera(25, 45, 1, "perspective") + plot.axis(ax, camera) + plot.bar(ax, camera, Y, facecolors=facecolors, edgecolors=facecolors * 0.25) + + plt.show() diff --git a/code/unsorted/3d/bunnies.py b/code/unsorted/3d/bunnies.py new file mode 100644 index 0000000..950b598 --- /dev/null +++ b/code/unsorted/3d/bunnies.py @@ -0,0 +1,60 @@ +import glm +import plot +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +# Wavefront loader (only vertices and faces) +def obj_load(filename): + V, Vi = [], [] + with open(filename) as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + Vi.append([int(x) for x in values[1:4]]) + return np.array(V), np.array(Vi) - 1 + + +# Model loading +V, F = obj_load("bunny.obj") +V = glm.fit_unit_cube(V) + + +fig = plt.figure(figsize=(8, 8)) + +ax = plt.subplot(221) +camera = glm.camera(20, 45, 1.25, "perspective") +plot.axis(ax, camera, ticks=False) +plot.mesh(ax, camera, V, F, facecolor="black", edgecolor="black", linewidth=2.5) +plot.mesh(ax, camera, V, F, cmap=plt.get_cmap("magma"), edgecolor="none") +ax.text(0.99, 0.99, "Perpective", transform=ax.transAxes, ha="right", va="top") + +white = (1.0, 1.0, 1.0, 0.75) +black = (0.0, 0.0, 0.0, 1.00) + +ax = plt.subplot(222) +camera = glm.camera(90, 0, 2, "ortho") +plot.axis(ax, camera, ticks=False) +plot.mesh(ax, camera, V, F, linewidth=0.25, facecolor=white, edgecolor=black) +ax.text(0.99, 0.99, "Orthographic (XZ)", transform=ax.transAxes, ha="right", va="top") + +ax = plt.subplot(223) +camera = glm.camera(0, 90, 2, "ortho") +plot.axis(ax, camera, ticks=False) +plot.mesh(ax, camera, V, F, linewidth=0.25, facecolor=white, edgecolor=black) +ax.text(0.99, 0.99, "Orthographic (XY)", transform=ax.transAxes, ha="right", va="top") + +ax = plt.subplot(224) +camera = glm.camera(0, 0, 2, "ortho") +plot.axis(ax, camera, ticks=False) +plot.mesh(ax, camera, V, F, linewidth=0.25, facecolor=white, edgecolor=black) +ax.text(0.99, 0.99, "Orthographic (ZY)", transform=ax.transAxes, ha="right", va="top") + +plt.tight_layout() +plt.show() diff --git a/code/unsorted/3d/bunny.obj b/code/unsorted/3d/bunny.obj new file mode 100644 index 0000000..18abc08 --- /dev/null +++ b/code/unsorted/3d/bunny.obj @@ -0,0 +1,7474 @@ +# OBJ file format with ext .obj +# vertex count = 2503 +# face count = 4968 +v -3.4101800e-003 1.3031957e-001 2.1754370e-002 +v -8.1719160e-002 1.5250145e-001 2.9656090e-002 +v -3.0543480e-002 1.2477885e-001 1.0983400e-003 +v -2.4901590e-002 1.1211138e-001 3.7560240e-002 +v -1.8405680e-002 1.7843055e-001 -2.4219580e-002 +v 1.9067940e-002 1.2144925e-001 3.1968440e-002 +v 6.0412000e-003 1.2494359e-001 3.2652890e-002 +v -1.3469030e-002 1.6299355e-001 -1.2000020e-002 +v -3.4393240e-002 1.7236688e-001 -9.8213000e-004 +v -8.4314160e-002 1.0957263e-001 3.7097300e-003 +v -4.2233540e-002 1.7211574e-001 -4.1799800e-003 +v -6.3308390e-002 1.5660615e-001 -1.3838790e-002 +v -7.6903950e-002 1.6708033e-001 -2.6931360e-002 +v -7.2253920e-002 1.1539550e-001 5.1670300e-002 +v 1.2981330e-002 1.1366375e-001 3.8302950e-002 +v -3.7857280e-002 1.7010102e-001 1.4236000e-003 +v 4.8689400e-003 3.7962370e-002 4.5867630e-002 +v -5.7180550e-002 4.0918830e-002 4.6301340e-002 +v -4.5209070e-002 3.8839100e-002 4.4503770e-002 +v -3.3761490e-002 1.2617876e-001 1.7132300e-003 +v -5.0242270e-002 1.5773747e-001 9.3944500e-003 +v -2.1216950e-002 1.5887938e-001 -4.6923700e-003 +v -5.6472950e-002 1.5778406e-001 8.1786500e-003 +v -5.2802060e-002 4.1319860e-002 4.6169800e-002 +v -4.9960340e-002 4.3101950e-002 4.4462650e-002 +v -2.9748750e-002 3.6539860e-002 5.2493310e-002 +v -3.5438900e-003 4.2659770e-002 4.7541530e-002 +v 4.9304900e-003 4.1982660e-002 4.5723390e-002 +v -3.9088180e-002 1.6872020e-001 -1.1924680e-002 +v -5.6901000e-002 4.5437000e-002 4.3236960e-002 +v -4.1244880e-002 4.3098890e-002 4.2129560e-002 +v -2.6471980e-002 4.5034530e-002 5.1219460e-002 +v -2.1866970e-002 4.4022930e-002 5.3243800e-002 +v -3.6996250e-002 1.6899301e-001 1.3256300e-003 +v -6.7216590e-002 1.6171340e-001 -1.3733710e-002 +v 4.9760060e-002 7.0235220e-002 2.3732020e-002 +v -4.9186640e-002 4.6411230e-002 4.1170040e-002 +v -4.4590380e-002 4.3797990e-002 4.2685460e-002 +v -4.3686470e-002 4.7154500e-002 4.0286310e-002 +v -2.2491950e-002 4.6513620e-002 5.1885310e-002 +v -6.5174200e-003 4.5036200e-002 4.7502780e-002 +v 3.7699000e-004 4.4935790e-002 4.6519930e-002 +v 3.4023920e-002 1.1353879e-001 2.4595280e-002 +v -2.6467900e-002 1.8104250e-001 -8.0811700e-003 +v -1.7533470e-002 4.7964250e-002 4.8829630e-002 +v -7.0012600e-003 4.6416520e-002 4.7485540e-002 +v 5.9862300e-003 4.6689140e-002 4.9073620e-002 +v 9.1007200e-003 4.8474490e-002 4.9353190e-002 +v -3.5453700e-002 1.1244769e-001 3.5055410e-002 +v -7.5983200e-002 1.3820800e-001 4.9216580e-002 +v 3.4838440e-002 4.3153410e-002 2.8954310e-002 +v -5.2655550e-002 4.8494220e-002 3.8731190e-002 +v -4.7378940e-002 4.8456670e-002 3.9126790e-002 +v -3.8933750e-002 4.6364270e-002 4.0364780e-002 +v -2.6468940e-002 4.7816430e-002 4.9322590e-002 +v -2.2365790e-002 4.8073650e-002 5.0126500e-002 +v -1.3373430e-002 4.7892410e-002 4.7883850e-002 +v -1.2193490e-002 4.9470300e-002 4.9484490e-002 +v -6.3364000e-004 4.7193060e-002 4.9136900e-002 +v 2.0656800e-003 5.0104680e-002 5.2290220e-002 +v -2.2749270e-002 4.9883880e-002 4.6605520e-002 +v -1.8002080e-002 4.9917850e-002 4.6947970e-002 +v -7.8036800e-003 5.0169310e-002 5.0988650e-002 +v -2.6843800e-003 5.1247420e-002 5.3186790e-002 +v -6.3875650e-002 1.6140094e-001 -2.0064210e-002 +v 3.2434000e-002 4.5333970e-002 3.0316760e-002 +v -8.8064570e-002 1.2496764e-001 5.7412000e-004 +v -4.1503710e-002 1.6748512e-001 3.2765900e-003 +v -6.4457010e-002 1.5342891e-001 -5.1180400e-003 +v -3.4303190e-002 5.0520150e-002 3.8286020e-002 +v -2.2949400e-002 5.1020650e-002 4.3926450e-002 +v -1.4354710e-002 5.4428200e-002 5.0710310e-002 +v 1.3773100e-003 5.2302710e-002 5.3149010e-002 +v 3.6285000e-003 5.3198640e-002 5.3422710e-002 +v 8.0723800e-003 5.1574140e-002 5.1773560e-002 +v -7.2665890e-002 1.3005582e-001 5.1668200e-002 +v 3.7992780e-002 4.9793200e-002 3.1902020e-002 +v 3.8497260e-002 4.8062400e-002 3.1737450e-002 +v 2.1503510e-002 1.2563988e-001 2.1252620e-002 +v -7.6481330e-002 1.4827412e-001 -8.9376200e-003 +v -8.7240410e-002 1.1967213e-001 -1.7813000e-004 +v -4.3719960e-002 1.6822738e-001 2.3425000e-003 +v -4.0652200e-002 1.2266506e-001 2.6290300e-002 +v -4.6686180e-002 5.4570720e-002 3.7587370e-002 +v -4.4071750e-002 5.1058250e-002 3.8977810e-002 +v -3.8144110e-002 5.0599600e-002 3.9302160e-002 +v -1.9875770e-002 5.1607710e-002 4.6142000e-002 +v -1.6911250e-002 5.1843550e-002 4.8459320e-002 +v -1.6249190e-002 5.4292110e-002 5.0306940e-002 +v -1.0446540e-002 5.3685970e-002 5.1958610e-002 +v -4.3090900e-003 5.4467500e-002 5.3908250e-002 +v 7.8152700e-003 5.5050680e-002 5.2750250e-002 +v 3.7955090e-002 1.0488710e-001 -3.2031800e-003 +v -7.9003790e-002 1.2850550e-001 5.3149340e-002 +v -7.9778990e-002 1.3448894e-001 5.0990290e-002 +v -5.9129700e-002 1.5039712e-001 3.4489540e-002 +v -6.5691790e-002 1.4961818e-001 3.8160980e-002 +v -3.1951660e-002 1.2518394e-001 1.9400580e-002 +v -6.9372590e-002 1.6061775e-001 -9.1905000e-003 +v -4.5225500e-002 1.2935459e-001 2.0377520e-002 +v -4.1879110e-002 5.6164390e-002 3.9796700e-002 +v -3.0614840e-002 5.4412650e-002 3.6694290e-002 +v -2.4787600e-002 5.2606220e-002 4.0839760e-002 +v -2.1588860e-002 5.6836920e-002 4.5467040e-002 +v -2.4264000e-004 5.4536020e-002 5.4641200e-002 +v -8.0900510e-002 1.2558713e-001 5.2155370e-002 +v -2.9996210e-002 1.7811137e-001 -5.2358200e-003 +v 3.5515390e-002 5.0449570e-002 3.1439830e-002 +v 4.3315550e-002 5.2145550e-002 3.2492110e-002 +v -6.3938540e-002 1.5262699e-001 3.4481070e-002 +v -4.4489440e-002 6.1077710e-002 3.9545320e-002 +v -3.8979900e-002 5.7996270e-002 4.0151390e-002 +v -7.9087730e-002 1.7044488e-001 -4.1373170e-002 +v -4.6247300e-003 5.7759650e-002 5.3990710e-002 +v -1.4985500e-003 5.5925480e-002 5.4630800e-002 +v 5.1981700e-003 5.7017990e-002 5.3423530e-002 +v 3.0920000e-005 1.2315746e-001 3.4749660e-002 +v 3.3568300e-002 1.1523716e-001 2.1798410e-002 +v 3.8686300e-002 5.6450590e-002 3.1188930e-002 +v -3.4385780e-002 5.4096000e-002 3.8060290e-002 +v -8.5308300e-003 6.0159420e-002 5.5308950e-002 +v -4.4024000e-004 5.8343410e-002 5.4483410e-002 +v -9.1078730e-002 1.1506037e-001 4.0141810e-002 +v 4.0775480e-002 5.4557490e-002 3.2014740e-002 +v 4.5636880e-002 5.7402620e-002 3.1992220e-002 +v 2.0358850e-002 1.2448747e-001 2.5906340e-002 +v -1.4169700e-002 1.2767892e-001 1.3080500e-003 +v -1.1987590e-002 5.7493210e-002 5.2752420e-002 +v 3.2514500e-003 5.9828640e-002 5.5464300e-002 +v -1.2395240e-002 1.2264726e-001 3.3588280e-002 +v 1.3813780e-002 1.2322188e-001 3.2502590e-002 +v -7.7004310e-002 1.5521281e-001 2.4534770e-002 +v -2.8001360e-002 6.1075420e-002 3.7471210e-002 +v -8.5480000e-004 6.0593520e-002 5.5824810e-002 +v -3.8050200e-002 1.1527068e-001 3.3178540e-002 +v -1.6231340e-002 1.2382942e-001 2.9576990e-002 +v -2.5373550e-002 1.5840012e-001 -1.4801300e-003 +v -6.7818590e-002 1.5454353e-001 3.0233720e-002 +v -4.3082600e-003 6.1418570e-002 5.5688490e-002 +v -3.1958900e-003 1.1912518e-001 3.8349580e-002 +v -6.4292400e-003 1.2201090e-001 3.5740890e-002 +v 4.2312960e-002 5.9099150e-002 3.0848420e-002 +v 4.8510010e-002 6.1780760e-002 3.0347250e-002 +v 5.0412290e-002 6.0312610e-002 3.0245060e-002 +v -3.9185590e-002 6.3074530e-002 4.1382890e-002 +v -3.4448660e-002 6.0780500e-002 3.9543990e-002 +v -1.4746030e-002 6.5583910e-002 5.3730860e-002 +v 2.6645200e-003 6.2700010e-002 5.6525210e-002 +v -1.3991610e-002 1.1962575e-001 3.6251540e-002 +v 1.9659170e-002 1.1236219e-001 3.7545270e-002 +v -3.2597160e-002 1.7498725e-001 -2.5953100e-003 +v -2.1513900e-003 9.9437380e-002 4.9849750e-002 +v -5.6001390e-002 6.1830670e-002 2.7931150e-002 +v -5.4707260e-002 6.3461570e-002 3.1670590e-002 +v -5.1307940e-002 6.0521660e-002 3.1434930e-002 +v -4.1979320e-002 6.9629980e-002 4.1824930e-002 +v -3.0272490e-002 6.2474660e-002 3.7982220e-002 +v -1.1387860e-002 6.4742460e-002 5.4918000e-002 +v 6.9544900e-003 6.4700130e-002 5.5599150e-002 +v 4.3015090e-002 9.7690960e-002 1.0258300e-003 +v 4.0635900e-002 6.1574860e-002 2.9841250e-002 +v 4.6183560e-002 6.1910110e-002 3.0223400e-002 +v 3.7552960e-002 1.0685291e-001 2.6303470e-002 +v -7.8640730e-002 1.6387238e-001 -2.8387790e-002 +v -6.1996240e-002 1.4761484e-001 -4.3256800e-003 +v -5.7499800e-003 6.5488980e-002 5.6173390e-002 +v 2.5369000e-004 6.5741170e-002 5.6569260e-002 +v -2.0542550e-002 1.1979518e-001 3.3003670e-002 +v 4.3155900e-003 1.2782561e-001 2.8646880e-002 +v -4.6549580e-002 6.7652130e-002 3.9635790e-002 +v -1.7420580e-002 6.9659490e-002 5.4089530e-002 +v -1.5242190e-002 7.0909900e-002 5.5004790e-002 +v -1.0282890e-002 6.8926360e-002 5.5289610e-002 +v -1.1289000e-004 6.9288200e-002 5.6579790e-002 +v -3.6309330e-002 1.1876943e-001 3.0674020e-002 +v -7.0325800e-002 6.3367770e-002 1.9809180e-002 +v 4.3023100e-002 6.3795810e-002 2.8039210e-002 +v 4.2831110e-002 8.5556040e-002 2.7873760e-002 +v 1.6981600e-002 1.2715003e-001 2.2931490e-002 +v -4.2121490e-002 1.2825104e-001 1.0751500e-003 +v 1.6329230e-002 1.2251895e-001 3.1375390e-002 +v -8.1264160e-002 1.5381172e-001 2.5897830e-002 +v -3.2257870e-002 8.8192600e-002 -2.5130960e-002 +v -1.3774950e-002 7.0887950e-002 5.4695630e-002 +v 5.2929600e-003 6.8006030e-002 5.5670490e-002 +v 7.6962500e-003 7.2375600e-002 5.6062150e-002 +v 3.4830600e-003 1.2002635e-001 3.6911950e-002 +v 6.6532500e-003 1.1673563e-001 3.8716340e-002 +v 4.6086570e-002 6.6473930e-002 2.6808990e-002 +v 5.2327290e-002 6.4327070e-002 2.8281890e-002 +v -6.1897630e-002 1.2297065e-001 -8.7725500e-003 +v -6.3934700e-003 1.0524472e-001 -2.2841900e-002 +v -3.5218330e-002 6.8559830e-002 4.1381470e-002 +v -3.2689880e-002 6.7729720e-002 4.0124390e-002 +v -2.9245440e-002 6.9551520e-002 3.9369010e-002 +v -5.0024500e-003 6.9655000e-002 5.6892510e-002 +v 1.6573960e-002 1.1890153e-001 3.5042300e-002 +v -8.9385100e-002 9.9024040e-002 1.7521830e-002 +v 4.5719230e-002 6.9489400e-002 2.3549340e-002 +v 5.4537210e-002 6.8796720e-002 2.4517690e-002 +v -4.4989450e-002 7.1577330e-002 4.1929250e-002 +v -4.2439400e-003 1.2914902e-001 2.5829230e-002 +v -7.3880090e-002 1.2091638e-001 5.3395800e-002 +v -7.4033870e-002 1.4406894e-001 4.4994970e-002 +v 5.0400010e-002 6.7292480e-002 2.6851470e-002 +v -5.4056890e-002 1.5671602e-001 -2.4865900e-003 +v 2.6148110e-002 1.2014725e-001 2.7308010e-002 +v -1.0736490e-002 1.2990285e-001 1.0993790e-002 +v -4.5078840e-002 8.7261130e-002 -2.1865520e-002 +v -3.8340900e-002 6.8843770e-002 4.1846470e-002 +v -2.9255580e-002 7.5169210e-002 4.1186430e-002 +v -4.7311210e-002 1.6296037e-001 6.0740300e-003 +v -1.1866030e-002 7.3183750e-002 5.6250050e-002 +v -6.3734600e-003 7.2184340e-002 5.7972980e-002 +v -2.9935300e-003 7.2186440e-002 5.8167190e-002 +v -2.5781060e-002 9.3778180e-002 -2.8388220e-002 +v -1.6692560e-002 1.1568553e-001 3.7853150e-002 +v -8.4123410e-002 1.0832050e-001 2.4730980e-002 +v -7.4294080e-002 1.6356850e-001 -1.5534220e-002 +v -9.4297150e-002 1.2617744e-001 1.9224650e-002 +v -3.5207090e-002 1.2505219e-001 2.1635690e-002 +v -4.9495940e-002 7.3436340e-002 4.1673570e-002 +v -3.3064160e-002 7.6654840e-002 4.1277900e-002 +v -7.3157300e-003 7.3919590e-002 5.7971690e-002 +v 2.1850000e-005 7.3496040e-002 5.7696650e-002 +v 4.1934400e-003 7.2915170e-002 5.6298730e-002 +v -7.7256080e-002 1.4565854e-001 4.3122930e-002 +v 4.1073260e-002 8.8724320e-002 -9.7879400e-003 +v 3.7418710e-002 1.0850822e-001 3.3973000e-004 +v -5.5111380e-002 7.4687840e-002 4.1939740e-002 +v -4.2740230e-002 7.6995340e-002 4.2804080e-002 +v -6.8531190e-002 1.5630045e-001 2.0997710e-002 +v -9.9440200e-003 7.6343100e-002 5.7388560e-002 +v -3.2479200e-003 7.5710690e-002 5.8714640e-002 +v 1.3414380e-002 9.3073740e-002 5.1467750e-002 +v -7.3504440e-002 9.3883340e-002 -1.4751720e-002 +v -7.4471830e-002 1.3507476e-001 5.0688900e-002 +v -2.5851310e-002 1.2182948e-001 2.6079670e-002 +v -3.4022940e-002 1.7597076e-001 -3.7271600e-003 +v -7.5405850e-002 1.6839072e-001 -2.6792980e-002 +v -3.6658410e-002 7.5087300e-002 4.2006940e-002 +v -1.7795480e-002 7.7486190e-002 5.6087240e-002 +v -1.1378660e-002 7.9877150e-002 5.7698880e-002 +v -1.0415000e-004 7.6881950e-002 5.8190740e-002 +v 2.7381400e-003 7.9105680e-002 5.6719190e-002 +v 5.5681200e-003 7.6397140e-002 5.6327220e-002 +v -6.1895860e-002 1.5424247e-001 -1.9018600e-002 +v -7.2646960e-002 1.4098943e-001 4.6976640e-002 +v 1.5799740e-002 1.2901416e-001 1.3236870e-002 +v -1.1703420e-002 9.7355720e-002 5.1592080e-002 +v -5.8922160e-002 7.7545490e-002 4.2961390e-002 +v -5.3121320e-002 7.7912430e-002 4.3334920e-002 +v -5.0745740e-002 7.6148400e-002 4.3137630e-002 +v -4.7401820e-002 7.5550340e-002 4.2630140e-002 +v -4.5055620e-002 7.8796280e-002 4.2341310e-002 +v -3.9517650e-002 7.8127780e-002 4.2918620e-002 +v -1.5245570e-002 8.2940770e-002 5.6934590e-002 +v -1.4557790e-002 7.6582160e-002 5.6493250e-002 +v -5.9406000e-003 7.9038240e-002 5.7969830e-002 +v 3.7176540e-002 1.1064404e-001 1.8811330e-002 +v 2.3929700e-003 1.3162713e-001 1.1955100e-002 +v -9.3644210e-002 1.1789378e-001 1.8662080e-002 +v -6.3939810e-002 7.8621830e-002 4.2083520e-002 +v -4.5376460e-002 8.2383550e-002 4.3282120e-002 +v -3.6505460e-002 8.1152260e-002 4.3162320e-002 +v -3.3244340e-002 8.2266590e-002 4.1852180e-002 +v -3.0800650e-002 8.0068420e-002 4.1798070e-002 +v -2.0578500e-003 8.0998290e-002 5.7553840e-002 +v 8.1848100e-003 8.0756170e-002 5.5374510e-002 +v -1.2953370e-002 1.1593580e-001 3.8920230e-002 +v -7.8081470e-002 1.2351940e-001 5.2136990e-002 +v -2.6580930e-002 1.5567694e-001 -4.1963400e-003 +v -8.2471600e-002 1.1624130e-001 -2.3236300e-003 +v -2.7538480e-002 7.9964780e-002 4.7697210e-002 +v 1.2556400e-003 8.3845570e-002 5.7446440e-002 +v 6.1508300e-003 8.3406240e-002 5.6463500e-002 +v -6.2433240e-002 8.4035270e-002 4.4203120e-002 +v -5.9867170e-002 8.0540510e-002 4.3277090e-002 +v -5.5238340e-002 8.1999450e-002 4.4984770e-002 +v -5.4000400e-002 8.0568410e-002 4.4601460e-002 +v -5.0027020e-002 8.1311330e-002 4.4264180e-002 +v -4.1996120e-002 8.1083670e-002 4.2456150e-002 +v -3.9357940e-002 8.3631380e-002 4.3502350e-002 +v -8.6161480e-002 1.0838594e-001 1.8244920e-002 +v -8.6723010e-002 9.9917250e-002 3.5537100e-003 +v -2.2413700e-002 8.3283520e-002 5.5590700e-002 +v -1.6993180e-002 8.2555820e-002 5.7523880e-002 +v -1.2406010e-002 8.5222570e-002 5.7267780e-002 +v -7.4442100e-003 1.1693417e-001 3.9283850e-002 +v -2.1452000e-003 1.1143287e-001 4.2436620e-002 +v -7.5718220e-002 1.2522734e-001 5.3087330e-002 +v -7.7056660e-002 1.3193469e-001 5.2462430e-002 +v -6.1121040e-002 1.5569660e-001 2.2517050e-002 +v -3.7538540e-002 1.2744127e-001 1.5320870e-002 +v -2.0516700e-003 1.0093469e-001 4.5625920e-002 +v -6.4992150e-002 8.4550900e-002 4.4120060e-002 +v -5.7861950e-002 8.3944360e-002 4.4186030e-002 +v -4.5681080e-002 8.4988010e-002 4.4159500e-002 +v -3.5022640e-002 8.2888160e-002 4.2912760e-002 +v -2.9982010e-002 8.5402300e-002 4.3745080e-002 +v -8.8892260e-002 9.9209100e-002 9.5703200e-003 +v -1.9135300e-002 8.3474800e-002 5.7217390e-002 +v -8.3489710e-002 1.0724729e-001 7.5790000e-004 +v -7.0112800e-002 1.1790350e-001 5.2714160e-002 +v -3.5526320e-002 1.7595563e-001 -4.8676200e-003 +v -7.0831390e-002 1.2254425e-001 5.3274880e-002 +v 4.5133810e-002 9.3630690e-002 6.2336800e-003 +v -5.3616700e-002 8.5346850e-002 4.5332470e-002 +v -4.9000840e-002 8.6221680e-002 4.5352040e-002 +v -3.6744880e-002 8.6083690e-002 4.3612890e-002 +v -1.0872600e-002 8.8826770e-002 5.6665490e-002 +v -3.8450200e-003 8.4787810e-002 5.7197980e-002 +v -4.9020070e-002 1.1771293e-001 3.1581430e-002 +v -4.2914400e-002 1.1835991e-001 3.0645040e-002 +v -5.7684530e-002 1.5561695e-001 1.2983110e-002 +v -2.5411730e-002 1.2472533e-001 1.2886000e-004 +v 1.9012230e-002 1.2736197e-001 1.7786580e-002 +v -5.9498600e-002 8.8845470e-002 4.5109290e-002 +v -5.6931050e-002 8.8101500e-002 4.4692930e-002 +v 3.5765600e-003 1.3138981e-001 7.2086000e-003 +v -1.6683350e-002 8.7266690e-002 5.6741190e-002 +v -8.4980800e-003 8.3990470e-002 5.7605220e-002 +v 3.5078200e-003 8.6339520e-002 5.7048320e-002 +v -2.8398700e-002 1.8070650e-001 -7.8469500e-003 +v -7.6565830e-002 1.1674037e-001 5.1489350e-002 +v 1.7869430e-002 9.0898610e-002 4.8712940e-002 +v -4.0342100e-002 1.1669551e-001 3.2460200e-002 +v 5.9105700e-003 1.3140929e-001 1.6823750e-002 +v -8.5777550e-002 9.1701370e-002 -4.6970000e-005 +v -5.0372230e-002 8.8844660e-002 4.5188000e-002 +v -4.4434130e-002 8.7654530e-002 4.3477620e-002 +v -4.2056390e-002 8.6711520e-002 4.2534630e-002 +v -3.3058460e-002 8.6185500e-002 4.2560350e-002 +v -2.9241910e-002 9.0453360e-002 4.4236610e-002 +v -6.8964100e-003 8.4432910e-002 5.7168580e-002 +v -6.6210600e-003 9.0415250e-002 5.6879750e-002 +v -1.2439100e-003 8.9093200e-002 5.6552120e-002 +v 9.4076000e-003 9.0328050e-002 5.4214140e-002 +v 4.0194810e-002 1.0231597e-001 -2.0048600e-003 +v -8.6227130e-002 1.1466841e-001 2.2102000e-003 +v -8.9495490e-002 9.5632430e-002 1.4234810e-002 +v -6.7132160e-002 1.5709447e-001 -6.2032000e-003 +v -5.2935640e-002 9.0913520e-002 4.4568870e-002 +v -3.6744910e-002 8.8886950e-002 4.3312050e-002 +v -1.3626110e-002 8.9787930e-002 5.6674380e-002 +v 2.3337130e-002 1.2353449e-001 2.4874140e-002 +v -3.7053790e-002 1.2715094e-001 3.5474000e-004 +v -7.3696690e-002 1.5613015e-001 1.4359790e-002 +v -6.5592380e-002 9.1042400e-002 4.4092080e-002 +v -5.8997380e-002 9.2030670e-002 4.5335270e-002 +v -3.3238910e-002 8.8573580e-002 4.3697040e-002 +v -3.1834990e-002 9.0722970e-002 4.4173460e-002 +v -2.0022170e-002 8.8032110e-002 5.5589350e-002 +v -1.1213830e-002 9.2366370e-002 5.6105260e-002 +v 3.9108440e-002 1.0829072e-001 1.3142330e-002 +v 2.8675700e-002 1.1959600e-001 2.4545910e-002 +v -6.8940210e-002 1.5652777e-001 -1.9716000e-003 +v -6.2615110e-002 9.1126880e-002 4.5090730e-002 +v 3.0444560e-002 1.1886441e-001 2.0821750e-002 +v -1.5241090e-002 9.1821720e-002 5.5817230e-002 +v -5.6221700e-003 9.3235010e-002 5.5893630e-002 +v 4.7989900e-003 9.1654840e-002 5.4715170e-002 +v -6.8282400e-002 9.2376840e-002 4.2388730e-002 +v -5.5623730e-002 9.2187420e-002 4.5054970e-002 +v -5.1901030e-002 9.5457620e-002 4.3937650e-002 +v -4.8809030e-002 9.1083890e-002 4.4456690e-002 +v -4.5411560e-002 9.1002130e-002 4.3252770e-002 +v -4.4514550e-002 9.4860420e-002 4.2972490e-002 +v -3.9430320e-002 8.9597620e-002 4.3177890e-002 +v -3.5642240e-002 9.2617410e-002 4.4238490e-002 +v -1.2246000e-004 9.3201160e-002 5.5398380e-002 +v 9.5104600e-003 9.5483870e-002 5.0910600e-002 +v 2.1441660e-002 9.1354960e-002 4.8043360e-002 +v -8.9830300e-003 1.6926449e-001 -2.2683480e-002 +v -7.3019050e-002 1.5602104e-001 2.2419340e-002 +v -6.4760430e-002 1.5311588e-001 -2.0371200e-003 +v -6.9368510e-002 9.5242790e-002 4.2129000e-002 +v -6.0117140e-002 9.5552910e-002 4.4183820e-002 +v -2.9241690e-002 9.4290440e-002 4.4821190e-002 +v -2.6561430e-002 9.3289510e-002 4.4975420e-002 +v -1.4394030e-002 9.4587640e-002 5.3993500e-002 +v -8.8691600e-003 9.5400260e-002 5.4445980e-002 +v -1.2188700e-003 9.6201750e-002 5.3815910e-002 +v 4.0479000e-003 9.5817360e-002 5.2936770e-002 +v -4.6019400e-003 1.2428544e-001 3.3471960e-002 +v -7.8436460e-002 1.3928013e-001 4.8329360e-002 +v 1.0774610e-002 1.3079162e-001 1.4341740e-002 +v -5.6623730e-002 9.6322170e-002 4.3667910e-002 +v -3.6298870e-002 9.5695620e-002 4.3580310e-002 +v -2.4379930e-002 9.5866450e-002 4.4434530e-002 +v 1.0915500e-002 1.2633629e-001 2.9857020e-002 +v -5.8622700e-003 9.7350210e-002 5.2743650e-002 +v 1.6973450e-002 9.7106620e-002 4.7440920e-002 +v -6.7231980e-002 9.9173950e-002 4.1593880e-002 +v -5.4994210e-002 9.9640820e-002 4.2955230e-002 +v -4.8617990e-002 9.6452700e-002 4.4183060e-002 +v -5.5369000e-002 1.5442476e-001 1.6160650e-002 +v -9.4243550e-002 1.2207432e-001 2.3568470e-002 +v 1.3242990e-002 9.6738240e-002 4.8750160e-002 +v 2.0639290e-002 9.6602480e-002 4.6971000e-002 +v 7.3429700e-003 1.2098188e-001 3.5973430e-002 +v -1.3493870e-002 1.2882438e-001 5.9690700e-003 +v -2.0110640e-002 1.2504545e-001 2.3588310e-002 +v -6.9438450e-002 1.6479930e-001 -1.7218700e-002 +v -6.4028050e-002 9.7838670e-002 4.2565330e-002 +v -5.1996350e-002 9.9707850e-002 4.2716590e-002 +v -4.3990880e-002 9.9425460e-002 4.2383430e-002 +v -3.9738250e-002 1.0215357e-001 4.0574410e-002 +v -3.5931490e-002 9.9809950e-002 4.2335800e-002 +v -3.0867600e-002 9.6914680e-002 4.4651400e-002 +v -2.8342070e-002 9.7782680e-002 4.3761280e-002 +v -2.5622580e-002 9.8713420e-002 4.4210890e-002 +v -8.5236620e-002 1.1077356e-001 2.4537670e-002 +v 7.1936000e-003 9.8859470e-002 4.8419510e-002 +v 9.6509200e-003 1.0108782e-001 4.7373080e-002 +v 1.3487100e-002 1.0076420e-001 4.7454290e-002 +v 7.7389800e-003 1.3147500e-001 1.1682970e-002 +v 8.0905000e-004 1.1633319e-001 4.0167560e-002 +v -7.2652570e-002 1.6567918e-001 -1.8212480e-002 +v -5.6009400e-003 1.3076674e-001 1.0516060e-002 +v -2.6303720e-002 1.2518875e-001 1.7392980e-002 +v -4.7590430e-002 1.0081180e-001 4.2349150e-002 +v -4.1460830e-002 9.8544800e-002 4.1778620e-002 +v -3.3582070e-002 1.0383908e-001 4.0737990e-002 +v -2.2870240e-002 1.0284737e-001 4.3544750e-002 +v -2.2361970e-002 9.8207610e-002 4.4765940e-002 +v -1.8870510e-002 9.8973200e-002 4.4489280e-002 +v -7.1433690e-002 7.7573520e-002 3.8060760e-002 +v -7.3001150e-002 1.1826712e-001 5.3034590e-002 +v -6.8466430e-002 1.3498146e-001 -8.3359800e-003 +v -7.4683810e-002 1.0786100e-001 -9.0477100e-003 +v -6.4958960e-002 1.5852021e-001 -1.2595320e-002 +v -7.8931700e-002 1.5093057e-001 3.5151900e-002 +v -7.4113550e-002 9.9442520e-002 3.8337710e-002 +v -7.0456930e-002 1.0098777e-001 3.9794060e-002 +v -5.9058760e-002 1.0041260e-001 4.2725130e-002 +v -4.9187330e-002 1.0452012e-001 4.0301390e-002 +v -2.9151180e-002 1.0197369e-001 4.2633060e-002 +v -1.1599720e-002 1.0107813e-001 4.4191660e-002 +v 5.1450400e-003 1.0163906e-001 4.5423010e-002 +v -5.1495700e-002 1.0496738e-001 4.0347210e-002 +v -2.0218210e-002 1.0214391e-001 4.3701160e-002 +v 4.2515900e-003 1.0523743e-001 4.2563550e-002 +v 1.6832800e-002 1.0337487e-001 4.5287270e-002 +v -2.5661080e-002 1.2562669e-001 4.5537500e-003 +v -7.2141950e-002 1.0536685e-001 3.7523210e-002 +v -6.4984570e-002 1.0371550e-001 4.0647810e-002 +v -6.0652480e-002 1.0467197e-001 4.0906390e-002 +v -5.5308980e-002 1.0365394e-001 4.1516690e-002 +v -4.4243240e-002 1.0431726e-001 4.1339990e-002 +v -1.5513340e-002 1.0436131e-001 4.2919420e-002 +v -7.6323200e-003 1.0304531e-001 4.3710640e-002 +v -7.8046900e-003 1.0516619e-001 4.3825460e-002 +v 9.7163200e-003 1.0523506e-001 4.3603830e-002 +v 3.0300390e-002 1.1553645e-001 2.8685010e-002 +v -4.7496910e-002 1.0635662e-001 4.0165640e-002 +v -3.8978950e-002 1.0683037e-001 3.8247660e-002 +v -2.5869310e-002 1.0426705e-001 4.2207540e-002 +v -1.8057930e-002 1.0503919e-001 4.2802830e-002 +v -1.5180030e-002 1.0807750e-001 4.2350430e-002 +v -3.8981500e-003 1.0566175e-001 4.4047190e-002 +v 2.6820000e-005 1.0446731e-001 4.3775910e-002 +v 1.1978350e-002 1.0403629e-001 4.5396310e-002 +v 1.5004970e-002 1.0726898e-001 4.1811990e-002 +v 2.6488060e-002 1.2230287e-001 2.0398110e-002 +v -3.6225630e-002 1.0634244e-001 3.8644860e-002 +v -2.1126780e-002 1.0932290e-001 4.0715320e-002 +v -1.2819810e-002 1.0457100e-001 4.3465690e-002 +v 5.2847900e-003 1.0943666e-001 4.1674980e-002 +v 8.9403700e-003 1.0710645e-001 4.1243400e-002 +v -5.1839670e-002 1.6062039e-001 7.1421300e-003 +v -5.4201370e-002 1.1451730e-001 3.4843990e-002 +v 1.3226250e-002 1.2958070e-001 1.9689610e-002 +v -6.9382410e-002 1.0865787e-001 3.7507800e-002 +v -6.7691040e-002 1.0734145e-001 3.8018440e-002 +v -6.3782400e-002 1.1037270e-001 3.7579790e-002 +v -5.0749390e-002 1.0928682e-001 3.8297580e-002 +v -9.3936200e-003 1.0742813e-001 4.3454570e-002 +v 1.1760100e-003 1.0932531e-001 4.2662800e-002 +v 9.8020300e-003 1.1003994e-001 3.9945400e-002 +v 2.0131290e-002 1.0732778e-001 4.0323840e-002 +v -2.7872800e-003 1.0577531e-001 -2.2459030e-002 +v -5.4996890e-002 1.0774199e-001 3.9424590e-002 +v -4.5966740e-002 1.0905146e-001 3.8754110e-002 +v -4.2324540e-002 1.0737278e-001 3.9456440e-002 +v -3.2161240e-002 1.0896504e-001 3.8102720e-002 +v -3.0770180e-002 1.1597313e-001 3.2858800e-002 +v -1.1608610e-002 1.0983707e-001 4.2475330e-002 +v -2.9428320e-002 9.3166620e-002 -2.4931860e-002 +v -8.0043570e-002 9.2080160e-002 -9.4198200e-003 +v -4.9797430e-002 1.1342104e-001 3.5117920e-002 +v -4.3723850e-002 1.6191369e-001 5.7713400e-003 +v -5.7981740e-002 1.0943152e-001 3.7997640e-002 +v -4.1491180e-002 1.1224766e-001 3.5873450e-002 +v -2.4929830e-002 1.1592775e-001 3.4094730e-002 +v -2.0881690e-002 1.1409528e-001 3.7872990e-002 +v -7.5519700e-003 1.1183813e-001 4.2039690e-002 +v 3.7667200e-003 1.1240547e-001 4.1494710e-002 +v -6.2829620e-002 1.5189480e-001 -9.2373400e-003 +v -5.9195950e-002 1.1320797e-001 3.6234680e-002 +v -5.1079080e-002 9.3892810e-002 -2.1761690e-002 +v -7.3945370e-002 8.4374880e-002 -1.5154490e-002 +v -7.2146240e-002 1.3486431e-001 -7.7592200e-003 +v -1.9408870e-002 1.7041104e-001 -2.0994830e-002 +v -5.5530450e-002 1.4905531e-001 -1.9602100e-003 +v 1.6688460e-002 3.6976600e-002 4.3000600e-002 +v -5.2277330e-002 1.1775075e-001 3.3769460e-002 +v -6.9201380e-002 9.3039200e-002 -1.6486120e-002 +v 2.6579210e-002 1.1702438e-001 3.0867940e-002 +v -2.3574310e-002 3.7036910e-002 5.4144750e-002 +v -7.3775100e-003 3.8988430e-002 4.8929450e-002 +v 1.3234660e-002 3.8453060e-002 4.4501470e-002 +v 1.9487350e-002 4.0809290e-002 4.2641060e-002 +v -6.3953930e-002 1.4694729e-001 3.8484200e-002 +v -4.9579470e-002 3.6096540e-002 4.5955360e-002 +v -4.3323650e-002 3.6286400e-002 4.4042360e-002 +v -2.9047200e-002 1.2556338e-001 7.7617700e-003 +v -1.7343100e-003 3.9476800e-002 4.7262900e-002 +v -3.1358130e-002 1.5362199e-001 -4.6738900e-003 +v 2.5822000e-003 1.0747582e-001 -2.0606030e-002 +v -5.6802300e-002 1.4514674e-001 3.1740300e-002 +v -5.6464330e-002 3.7683110e-002 4.6819640e-002 +v -5.0964750e-002 3.8312290e-002 4.6286140e-002 +v -5.0980410e-002 1.3486613e-001 2.7585000e-002 +v -2.5647410e-002 3.8860730e-002 5.4161390e-002 +v -2.2542110e-002 4.0615780e-002 5.3986030e-002 +v -1.7618010e-002 3.8911170e-002 5.2403440e-002 +v -1.9711750e-002 1.6829145e-001 -1.3020960e-002 +v 2.3780070e-002 9.5222940e-002 4.6347330e-002 +v 1.4744290e-002 4.2716950e-002 4.4510310e-002 +v 2.1691360e-002 4.0161530e-002 4.0846450e-002 +v -6.4067240e-002 9.0172190e-002 -1.8855520e-002 +v 2.0319150e-002 1.0041961e-001 4.5760520e-002 +v -3.6425000e-002 9.3630690e-002 -2.3534630e-002 +v -1.4981170e-002 4.2571420e-002 5.1404530e-002 +v -5.7335340e-002 1.2340101e-001 4.0231470e-002 +v -5.4172560e-002 1.2337919e-001 3.7576440e-002 +v 2.2625210e-002 4.3621680e-002 4.0904580e-002 +v 2.8810520e-002 4.3352290e-002 3.2157720e-002 +v -4.2764160e-002 1.5727487e-001 5.2016200e-003 +v 9.2231900e-003 4.4125090e-002 4.5057440e-002 +v 1.5048210e-002 4.5755840e-002 4.3793870e-002 +v -6.3757290e-002 1.0251144e-001 -1.7484400e-002 +v -3.4070430e-002 1.6148975e-001 -1.3786960e-002 +v -8.2191500e-002 7.5610200e-002 1.6542620e-002 +v -6.6299420e-002 1.2337119e-001 5.0615920e-002 +v -1.5510100e-002 4.5283110e-002 5.0653040e-002 +v 1.8928020e-002 4.4249610e-002 4.3009830e-002 +v 2.5821800e-002 4.6326610e-002 3.8277230e-002 +v 2.7268700e-002 4.4547790e-002 3.6152520e-002 +v -4.5301340e-002 1.5695057e-001 7.2036900e-003 +v 2.3855760e-002 1.0616625e-001 3.9378080e-002 +v 2.1632670e-002 4.8127270e-002 4.0694430e-002 +v 4.3785360e-002 4.8803700e-002 3.1343420e-002 +v 4.8074790e-002 4.8969960e-002 2.8165490e-002 +v 5.2663090e-002 4.7673620e-002 2.1201270e-002 +v -5.2722450e-002 4.4722850e-002 4.4143250e-002 +v -3.0071610e-002 1.7258324e-001 -6.3597700e-003 +v -3.4508050e-002 1.5447469e-001 1.6504600e-003 +v 1.0629710e-002 4.6711810e-002 4.6472020e-002 +v 1.6743440e-002 4.8439000e-002 4.3678630e-002 +v 2.8827050e-002 9.2133370e-002 4.3920090e-002 +v -5.9937100e-002 1.2726188e-001 4.0771270e-002 +v -3.6752090e-002 1.5802075e-001 4.1862000e-003 +v -3.7885390e-002 1.6199719e-001 2.4686000e-004 +v -2.2047790e-002 1.8348586e-001 -1.2094990e-002 +v -2.4364620e-002 1.8096836e-001 -9.8312000e-003 +v -4.4882280e-002 1.5052959e-001 7.6451700e-003 +v 2.6996760e-002 5.1317780e-002 3.8752040e-002 +v 4.7735750e-002 5.2751040e-002 3.0797290e-002 +v 5.1703790e-002 4.8857380e-002 2.4147970e-002 +v -6.7504360e-002 1.1424088e-001 4.8036050e-002 +v -1.6257520e-002 1.6031250e-001 -9.6926000e-003 +v -6.3926300e-002 1.6792441e-001 -4.0730420e-002 +v -4.1665290e-002 1.4996141e-001 4.5405000e-003 +v -3.5203230e-002 1.6493551e-001 -2.6810000e-003 +v 4.1318770e-002 9.9496740e-002 2.4275750e-002 +v 1.4055220e-002 5.2523910e-002 4.8593880e-002 +v 1.9421220e-002 5.1321300e-002 4.4798910e-002 +v 2.3677990e-002 5.1474390e-002 4.1053270e-002 +v 3.4258130e-002 5.1930810e-002 3.2757880e-002 +v 5.5957340e-002 5.3147410e-002 2.3197720e-002 +v -3.9937960e-002 1.4922850e-001 1.6017200e-003 +v -4.6988800e-002 1.2600802e-001 2.6985500e-002 +v -2.7708370e-002 9.0081290e-002 -3.1911460e-002 +v 1.9204630e-002 5.5166510e-002 4.7722150e-002 +v 2.1886000e-002 5.3927560e-002 4.5102460e-002 +v 3.1286270e-002 5.2863840e-002 3.6913620e-002 +v 4.6661160e-002 5.4719230e-002 3.1976810e-002 +v 5.1823730e-002 5.3276700e-002 2.7927010e-002 +v -2.9264880e-002 1.6140418e-001 -2.1039500e-003 +v -6.8700770e-002 1.4463537e-001 4.3041630e-002 +v -5.6070060e-002 1.5000706e-001 2.9867640e-002 +v 4.4717850e-002 9.4802660e-002 1.2024710e-002 +v -4.1804090e-002 1.5582081e-001 6.4548200e-003 +v -6.8369340e-002 1.2289287e-001 5.2437860e-002 +v -6.4114810e-002 9.5509880e-002 -1.8114610e-002 +v -1.8383130e-002 1.8543664e-001 -1.7136370e-002 +v 1.1745400e-002 5.6678340e-002 5.1914060e-002 +v -5.9375360e-002 1.1998238e-001 4.0548240e-002 +v 5.9092080e-002 5.7956980e-002 2.0270120e-002 +v 4.3547740e-002 9.7389400e-002 1.7314650e-002 +v -2.6291780e-002 1.5963381e-001 -5.1845000e-004 +v 1.4904780e-002 5.6350380e-002 4.9522780e-002 +v 2.4286200e-002 5.4958580e-002 4.3086850e-002 +v 2.8952610e-002 5.6125250e-002 4.0388970e-002 +v -4.9507770e-002 1.2949500e-001 3.0259270e-002 +v 4.0824790e-002 9.5170220e-002 2.8657920e-002 +v 1.7774800e-002 5.8243780e-002 4.8864720e-002 +v 3.3573840e-002 5.8515260e-002 3.8310990e-002 +v 3.6385040e-002 5.6996480e-002 3.3601460e-002 +v -6.4205010e-002 1.2243894e-001 4.8008340e-002 +v -6.5424500e-002 1.4011279e-001 4.1308960e-002 +v 5.0801340e-002 5.7308080e-002 3.0001390e-002 +v 5.6671750e-002 5.6970820e-002 2.4291920e-002 +v -4.9349930e-002 1.4913519e-001 1.1274060e-002 +v -6.9760570e-002 1.3442855e-001 4.8265220e-002 +v 1.9537060e-002 6.0003780e-002 4.8576140e-002 +v 2.7013910e-002 5.9952790e-002 4.3454420e-002 +v 5.7679430e-002 6.1392970e-002 2.4201790e-002 +v -5.6916540e-002 1.2623512e-001 3.9426610e-002 +v 2.3469280e-002 1.1656262e-001 3.3537270e-002 +v -5.8298640e-002 1.3885500e-001 3.2937460e-002 +v 6.4598400e-003 6.0297430e-002 5.4780030e-002 +v 1.0406020e-002 5.9162400e-002 5.2484370e-002 +v 2.3183950e-002 5.8654360e-002 4.5871060e-002 +v 3.3040360e-002 6.1773840e-002 3.9781440e-002 +v -6.4348220e-002 1.2628088e-001 4.6650200e-002 +v -5.7031440e-002 1.1562007e-001 3.6494880e-002 +v 5.4451560e-002 5.8342890e-002 2.7653010e-002 +v -3.0134400e-002 1.7011322e-001 -7.3591600e-003 +v -3.7077100e-002 1.5986369e-001 1.6096500e-003 +v -5.6032760e-002 1.3731083e-001 3.1970590e-002 +v -6.7676470e-002 1.4150325e-001 4.3868140e-002 +v 9.9911700e-003 6.2735270e-002 5.4009240e-002 +v 1.4521510e-002 6.1382890e-002 5.0500900e-002 +v 3.0051740e-002 6.2169610e-002 4.1545810e-002 +v 3.7519170e-002 6.1062710e-002 3.4366020e-002 +v 5.3944010e-002 6.1391550e-002 2.8268530e-002 +v 5.9119900e-002 6.3128810e-002 2.1561830e-002 +v -2.4366390e-002 1.7693266e-001 -1.1719630e-002 +v -1.3253420e-002 1.6627152e-001 -1.4120370e-002 +v 3.9218740e-002 1.0669250e-001 2.0450190e-002 +v -1.7968980e-002 1.8078031e-001 -1.8103430e-002 +v 2.1902390e-002 6.0875970e-002 4.7282360e-002 +v 3.5341750e-002 6.1630030e-002 3.7606020e-002 +v -6.2145620e-002 1.3599775e-001 3.6700970e-002 +v 5.6820620e-002 6.3691150e-002 2.5286090e-002 +v -3.2800040e-002 1.5948699e-001 2.1962800e-003 +v 1.1212140e-002 6.6584120e-002 5.3982180e-002 +v 1.2919590e-002 6.4203580e-002 5.2441150e-002 +v 2.0126950e-002 6.3851330e-002 4.7919660e-002 +v 3.5971760e-002 6.6669610e-002 3.7781400e-002 +v 3.9906940e-002 6.4361260e-002 3.1686660e-002 +v -6.6702350e-002 1.3210600e-001 4.5480940e-002 +v -4.1601430e-002 1.5978000e-001 3.5374700e-003 +v 3.3044580e-002 1.0766252e-001 3.1916150e-002 +v 2.4672100e-002 6.3694500e-002 4.5204640e-002 +v 2.6108660e-002 6.8007640e-002 4.3902690e-002 +v 3.3363940e-002 6.7054760e-002 3.9729480e-002 +v 4.2915790e-002 6.6707700e-002 2.6994720e-002 +v 5.4714960e-002 6.4697160e-002 2.6979680e-002 +v -1.6530940e-002 1.6325000e-001 -9.2475200e-003 +v -1.7891600e-002 1.6113800e-001 -6.7072700e-003 +v 4.1118120e-002 9.7491260e-002 -3.9756700e-003 +v 2.3386770e-002 7.0075990e-002 4.7012620e-002 +v 3.8102900e-002 6.5678440e-002 3.5132520e-002 +v 1.0145240e-002 1.2221678e-001 3.4718950e-002 +v 5.8392410e-002 6.6741240e-002 2.1979460e-002 +v 3.8302050e-002 8.4549140e-002 -1.4478830e-002 +v 3.4126440e-002 9.7053980e-002 3.7590390e-002 +v -3.1355740e-002 1.5809888e-001 1.9128800e-003 +v -5.8259510e-002 1.4099493e-001 3.2440640e-002 +v -6.6817230e-002 1.1951525e-001 5.1490220e-002 +v -6.8090040e-002 1.1647050e-001 5.1151230e-002 +v 1.6568300e-002 6.6269890e-002 5.1009890e-002 +v 2.9362870e-002 6.6509780e-002 4.2289380e-002 +v 3.7027180e-002 9.3949630e-002 -1.1674040e-002 +v 5.6412730e-002 6.7659930e-002 2.3969320e-002 +v -6.1295740e-002 1.4519988e-001 3.7137830e-002 +v 8.3873000e-003 1.1336223e-001 3.9792610e-002 +v 1.1807030e-002 7.0920980e-002 5.4240490e-002 +v 2.9741730e-002 7.0647100e-002 4.1653890e-002 +v 3.6294410e-002 7.1220700e-002 3.7114610e-002 +v 3.9899680e-002 7.0294820e-002 3.2720020e-002 +v -6.2763130e-002 1.3778012e-001 3.6678590e-002 +v -1.5815440e-002 1.7504938e-001 -1.8654160e-002 +v -9.2268990e-002 1.1475156e-001 1.7017380e-002 +v -9.4964000e-004 1.0141111e-001 4.4290070e-002 +v -6.3712920e-002 1.1274250e-001 3.8006760e-002 +v -6.1096020e-002 1.1701650e-001 3.9654020e-002 +v 2.0991870e-002 6.9335450e-002 4.9003540e-002 +v 2.5658530e-002 7.0550460e-002 4.4539930e-002 +v 3.2978560e-002 7.3500690e-002 4.0486510e-002 +v 4.2156130e-002 6.9717580e-002 2.8318230e-002 +v -5.5516860e-002 1.2956070e-001 3.6598450e-002 +v -4.0802290e-002 1.6436059e-001 3.7448800e-003 +v -6.2546500e-003 1.0121650e-001 4.4322030e-002 +v -1.0986820e-002 1.6621199e-001 -1.6047550e-002 +v -3.0351420e-002 1.6448158e-001 -5.3291400e-003 +v 2.6110920e-002 1.0088990e-001 4.1733260e-002 +v -6.5599940e-002 1.1329504e-001 4.2318710e-002 +v 2.8814660e-002 9.6712680e-002 4.2257700e-002 +v 1.5263280e-002 7.1571940e-002 5.2717390e-002 +v 2.8982400e-002 7.4088480e-002 4.3447240e-002 +v 4.4872540e-002 7.5516710e-002 2.3155250e-002 +v -7.8225230e-002 1.4962481e-001 -2.5019400e-003 +v -4.6094940e-002 1.5296850e-001 9.0029700e-003 +v -5.2369030e-002 1.4682913e-001 1.8934650e-002 +v -2.1592100e-002 1.5763440e-001 -6.8623600e-003 +v 1.7176770e-002 7.3066230e-002 5.1826600e-002 +v 2.2687500e-002 7.5149180e-002 4.9312500e-002 +v 3.5472040e-002 7.3076670e-002 3.8482270e-002 +v -8.9480840e-002 1.3839976e-001 2.5061450e-002 +v -5.3216730e-002 1.3221978e-001 3.2978380e-002 +v -3.7776780e-002 1.5551947e-001 4.3700800e-003 +v -9.0549380e-002 1.3511875e-001 2.1680550e-002 +v -6.3366580e-002 1.3037076e-001 4.1669940e-002 +v 1.4074270e-002 7.6651720e-002 5.4221350e-002 +v 1.8109790e-002 7.5806590e-002 5.2488260e-002 +v 4.2209940e-002 7.8861480e-002 2.9187200e-002 +v -5.2115930e-002 1.4179906e-001 2.0510310e-002 +v 2.9063090e-002 1.1149602e-001 3.3805790e-002 +v -5.4731460e-002 1.4267229e-001 2.8980480e-002 +v 2.5903640e-002 7.5536040e-002 4.6416650e-002 +v 3.1298760e-002 7.5907440e-002 4.2699060e-002 +v 3.8446170e-002 7.5649430e-002 3.5050640e-002 +v 4.6351670e-002 7.4079520e-002 1.8354320e-002 +v -4.7656560e-002 1.3077525e-001 2.5523570e-002 +v -1.1447430e-002 1.7131059e-001 -1.9602980e-002 +v -3.6647240e-002 1.6640131e-001 -2.8167000e-004 +v -4.6653530e-002 1.5917824e-001 7.8019000e-003 +v -4.5569890e-002 1.4663612e-001 5.6514200e-003 +v 4.1438880e-002 9.2365100e-002 -7.4587000e-003 +v -6.4287420e-002 1.3463625e-001 3.9945640e-002 +v -6.1128890e-002 1.3178328e-001 3.8915910e-002 +v -4.7843540e-002 1.2215063e-001 2.8833160e-002 +v -4.9536830e-002 1.2491344e-001 3.1778440e-002 +v -7.1135380e-002 1.3817656e-001 4.7853960e-002 +v 1.0113870e-002 7.6468110e-002 5.5256790e-002 +v 1.7897450e-002 7.9516550e-002 5.2759530e-002 +v 2.1740850e-002 8.0250650e-002 5.0425390e-002 +v 2.5271590e-002 7.8724920e-002 4.8026570e-002 +v 3.0885040e-002 7.8999480e-002 4.3388770e-002 +v -6.2441930e-002 1.4084781e-001 3.6965840e-002 +v -6.2165060e-002 1.5666850e-001 -1.7837760e-002 +v 2.0657260e-002 1.0416830e-001 4.3004680e-002 +v -6.3602800e-002 1.1571453e-001 4.2572290e-002 +v 1.4424020e-002 8.0085500e-002 5.3755600e-002 +v 2.8779340e-002 8.2553250e-002 4.4527350e-002 +v 4.4450130e-002 8.1846900e-002 2.4552920e-002 +v 4.5541990e-002 8.3338380e-002 1.9700850e-002 +v -4.9665810e-002 1.2063801e-001 3.2163270e-002 +v -2.9177290e-002 1.7619959e-001 -5.6241100e-003 +v -5.8203130e-002 1.3270975e-001 3.6918680e-002 +v 3.8997050e-002 9.7088220e-002 -7.7799300e-003 +v -5.4725800e-002 1.2071262e-001 3.7451450e-002 +v 1.3189120e-002 8.4211180e-002 5.3065830e-002 +v -1.9926300e-002 1.6489742e-001 -9.9900200e-003 +v 2.0153130e-002 1.1849719e-001 3.4271250e-002 +v -5.5859940e-002 1.1774313e-001 3.7253480e-002 +v 1.8045260e-002 8.3623160e-002 5.1285840e-002 +v -6.3757130e-002 1.5912175e-001 -5.0155730e-002 +v -1.8527620e-002 1.7653197e-001 -1.7043540e-002 +v 2.8734400e-002 1.0360053e-001 3.8035240e-002 +v 4.1414010e-002 1.0284216e-001 1.6578920e-002 +v 2.4411730e-002 9.8016880e-002 4.4687400e-002 +v 2.0925180e-002 8.6311430e-002 4.9433120e-002 +v 3.0445010e-002 8.4959560e-002 4.3011090e-002 +v 3.3030090e-002 8.3781640e-002 4.1636930e-002 +v 3.6975090e-002 7.9876480e-002 3.7198390e-002 +v -7.7721460e-002 1.1355888e-001 4.8155990e-002 +v 2.9250000e-002 1.0651935e-001 3.6590330e-002 +v -5.3078180e-002 1.3754688e-001 2.8266470e-002 +v -6.2990590e-002 1.1999459e-001 4.5235530e-002 +v -6.5398320e-002 1.1751956e-001 4.8735570e-002 +v 3.3373910e-002 1.1227890e-001 2.7788130e-002 +v 3.8413590e-002 8.7489930e-002 3.5185850e-002 +v -6.1945930e-002 1.6479234e-001 -5.6647670e-002 +v -2.2876480e-002 1.7392813e-001 -1.3431140e-002 +v 4.3766230e-002 8.8390020e-002 -3.5708800e-003 +v 3.9291530e-002 1.0125969e-001 2.7550520e-002 +v 1.0936230e-002 8.6027290e-002 5.4732670e-002 +v 2.4108720e-002 8.4492600e-002 4.8292310e-002 +v 3.6758390e-002 9.9195470e-002 3.2837670e-002 +v -5.1941640e-002 1.2565987e-001 3.4587860e-002 +v -3.1582110e-002 1.6641850e-001 -5.7320000e-003 +v 7.6405900e-003 8.6427230e-002 5.6117850e-002 +v 1.6771020e-002 8.8644690e-002 5.0522960e-002 +v 3.4404610e-002 8.6932850e-002 4.0574270e-002 +v 3.6143820e-002 8.4439200e-002 3.7936930e-002 +v 4.1258830e-002 1.0361081e-001 2.6760600e-003 +v 2.4766140e-002 1.1081111e-001 3.6728360e-002 +v -2.2601590e-002 1.6250449e-001 -6.0717000e-003 +v -1.2893670e-002 1.7879041e-001 -2.2624750e-002 +v -2.4939150e-002 1.7031135e-001 -1.1329700e-002 +v -4.8468630e-002 1.4559606e-001 8.3661500e-003 +v 1.2534490e-002 8.9593930e-002 5.3394630e-002 +v 2.5872860e-002 8.8482290e-002 4.6655260e-002 +v 3.2756470e-002 8.8969130e-002 4.2215450e-002 +v -2.3343620e-002 1.6103450e-001 -3.1862400e-003 +v -9.2594970e-002 1.1943826e-001 2.6802950e-002 +v -7.4314840e-002 1.3761738e-001 -6.6698800e-003 +v -9.2499230e-002 1.2131500e-001 2.9256200e-002 +v -7.7378260e-002 1.5764266e-001 -1.4133650e-002 +v -9.2907340e-002 1.2307021e-001 3.6523230e-002 +v 2.8423340e-002 8.8011080e-002 4.4234200e-002 +v 3.5251680e-002 9.0836820e-002 3.9183920e-002 +v 1.5760560e-002 9.3203560e-002 4.9939310e-002 +v 3.8785530e-002 9.4954300e-002 3.2520220e-002 +v -6.1511220e-002 1.2373565e-001 4.3062680e-002 +v -6.8145120e-002 1.2748676e-001 5.0148970e-002 +v -2.0616710e-002 1.8237588e-001 -1.4299100e-002 +v 1.5137190e-002 1.1571495e-001 3.7031980e-002 +v -5.0718270e-002 1.5276300e-001 1.1816680e-002 +v 3.0168690e-002 1.0048686e-001 3.9404710e-002 +v -8.7426500e-002 9.5469530e-002 4.0312400e-003 +v -6.0010390e-002 1.4284463e-001 3.5449690e-002 +v -5.8603310e-002 1.4637237e-001 3.3808800e-002 +v 3.2411650e-002 9.3736150e-002 4.0890240e-002 +v -7.5917780e-002 1.4997690e-001 -1.6842050e-002 +v 1.8596570e-002 3.5293940e-002 -8.6782200e-003 +v 1.7209800e-002 3.5259400e-002 -1.4685160e-002 +v 4.4326540e-002 9.0818120e-002 2.2097520e-002 +v 3.8335910e-002 3.8830830e-002 3.0938100e-003 +v 2.2192920e-002 3.6775320e-002 -2.0919300e-003 +v 1.9636020e-002 3.8234010e-002 -1.2507670e-002 +v 2.3682120e-002 3.9762540e-002 3.7148760e-002 +v 4.6693280e-002 4.2465320e-002 6.5649500e-003 +v 2.1621110e-002 3.7657240e-002 -4.7021600e-003 +v 1.6638610e-002 3.8196090e-002 -1.9884930e-002 +v -9.0253980e-002 1.1366307e-001 3.7720210e-002 +v -9.0593870e-002 1.1373094e-001 1.0276770e-002 +v -6.2541690e-002 1.7679461e-001 -5.7821820e-002 +v -1.1091940e-002 1.7992082e-001 -2.5996430e-002 +v -6.2263130e-002 1.5219935e-001 -2.2578880e-002 +v -4.2276760e-002 9.4982570e-002 -2.2562420e-002 +v 4.3293410e-002 4.1864140e-002 2.0634400e-003 +v 4.3779590e-002 4.4530720e-002 -1.2622500e-003 +v 2.1696990e-002 4.0427270e-002 -9.4629500e-003 +v -1.1183700e-002 1.6450000e-001 -1.6151690e-002 +v -6.2372570e-002 1.5313041e-001 -2.8997120e-002 +v -9.2489300e-003 1.7725850e-001 -2.8270200e-002 +v 4.1477400e-002 8.5509410e-002 -9.1575000e-003 +v -8.1268710e-002 1.0879438e-001 2.9440660e-002 +v 4.9575680e-002 4.3815900e-002 1.4582960e-002 +v 5.2987960e-002 4.7747690e-002 5.0420000e-003 +v 2.1977540e-002 4.2855330e-002 -1.4536230e-002 +v 1.8505700e-002 3.8294100e-002 -1.7136500e-002 +v -3.5100500e-002 1.5203437e-001 -1.3279000e-004 +v 4.8749130e-002 4.5265000e-002 2.3023500e-003 +v 3.1912900e-002 9.9870060e-002 -1.4620980e-002 +v -1.4222520e-002 1.6167426e-001 -1.3349060e-002 +v -4.8663640e-002 1.3638523e-001 6.8063900e-003 +v -9.5837200e-003 1.7426102e-001 -2.8390760e-002 +v 5.2801850e-002 4.6539940e-002 1.0427720e-002 +v 5.1433800e-002 4.8485200e-002 1.0401000e-003 +v 2.3911240e-002 9.8021670e-002 -2.0807290e-002 +v 2.4567060e-002 4.4130110e-002 -1.0820840e-002 +v 2.0356810e-002 4.3662400e-002 -2.0456280e-002 +v -2.1882420e-002 1.1087418e-001 -1.9695320e-002 +v -5.3831800e-002 1.4981693e-001 2.5066610e-002 +v 5.4114210e-002 4.7773090e-002 1.7484000e-002 +v 5.6730570e-002 5.0515740e-002 1.0627080e-002 +v 4.5941820e-002 4.8138820e-002 -3.8715700e-003 +v -8.3817760e-002 1.1109094e-001 2.8524490e-002 +v 2.9207770e-002 4.7450250e-002 -8.5081800e-003 +v 2.8454920e-002 4.8067390e-002 -1.2847240e-002 +v 2.6637260e-002 4.7607100e-002 -1.6427740e-002 +v 2.2040110e-002 4.4992500e-002 -1.7528500e-002 +v 1.9120080e-002 4.7167750e-002 -2.2114680e-002 +v -1.5782200e-002 1.0072957e-001 -2.3724130e-002 +v -6.2514170e-002 1.7213119e-001 -5.2788100e-002 +v -6.2345600e-002 1.4745498e-001 -7.6600200e-003 +v 4.5598180e-002 8.8151720e-002 1.3124070e-002 +v -4.9422610e-002 1.4283525e-001 8.9728300e-003 +v -8.2761860e-002 1.1162341e-001 4.4221460e-002 +v -5.2166220e-002 1.5013661e-001 1.7448750e-002 +v -6.3616740e-002 1.4801371e-001 -2.0170260e-002 +v -5.1492690e-002 1.3796388e-001 2.3662180e-002 +v -6.1517580e-002 1.7517449e-001 -6.0631700e-002 +v 5.6524870e-002 5.0125660e-002 1.5564490e-002 +v 5.5257900e-002 5.1416260e-002 3.2062600e-003 +v 5.0318130e-002 5.2786370e-002 -3.4166300e-003 +v -6.2681950e-002 1.6744086e-001 -4.5713890e-002 +v 5.6520150e-002 5.1179900e-002 1.9940560e-002 +v 5.6907980e-002 5.1578130e-002 7.2538300e-003 +v 5.2854160e-002 5.1898670e-002 -6.2070000e-004 +v -3.8921140e-002 3.3767390e-002 -2.9042560e-002 +v 2.9740700e-002 5.0324690e-002 -1.3990860e-002 +v -6.8796190e-002 3.5117720e-002 -5.2067400e-003 +v 5.8826020e-002 5.5503780e-002 1.8647920e-002 +v -2.6160570e-002 1.2309988e-001 -4.4735500e-003 +v -5.3341960e-002 1.4401200e-001 2.4261390e-002 +v 5.8177390e-002 5.2821320e-002 1.5182420e-002 +v 5.9798140e-002 5.6840180e-002 1.3342730e-002 +v 5.4549870e-002 5.6044630e-002 -6.6158000e-004 +v 2.6775460e-002 5.1423450e-002 -2.0234060e-002 +v -8.6960400e-003 1.7291588e-001 -2.6708770e-002 +v -7.7039560e-002 7.1967020e-002 2.6405070e-002 +v -6.3069890e-002 1.5897471e-001 -4.2951850e-002 +v 3.5706690e-002 5.6083040e-002 -8.9993300e-003 +v 3.2600380e-002 5.3707520e-002 -1.1006150e-002 +v 2.9739960e-002 5.2538430e-002 -1.6224950e-002 +v 5.9238530e-002 5.6362780e-002 9.4530800e-003 +v 5.7421750e-002 5.6012210e-002 4.0245600e-003 +v 2.9062990e-002 5.5210580e-002 -1.8042060e-002 +v -1.7224410e-002 9.5214090e-002 -3.2085300e-002 +v -8.5911380e-002 1.0968787e-001 7.6582400e-003 +v 6.0594930e-002 6.1677210e-002 1.5591560e-002 +v 5.9531640e-002 6.0504600e-002 5.8397000e-003 +v 5.7306470e-002 5.9944620e-002 1.8886400e-003 +v 3.8829380e-002 5.9839830e-002 -6.4252500e-003 +v 3.0662770e-002 5.7300390e-002 -1.6518370e-002 +v -2.7762070e-002 1.2068537e-001 -9.0152900e-003 +v -8.8194590e-002 1.0314633e-001 1.7509020e-002 +v 6.0778800e-002 6.1646560e-002 1.0463990e-002 +v 3.5915080e-002 5.9916380e-002 -1.1966510e-002 +v 2.4251860e-002 5.6457470e-002 -2.4254800e-002 +v -6.1954390e-002 1.6865320e-001 -5.2621160e-002 +v -9.0557930e-002 1.1275994e-001 1.6141030e-002 +v -8.8469220e-002 1.1124294e-001 1.2679160e-002 +v 5.9558010e-002 6.3099260e-002 5.9471000e-003 +v 3.0940440e-002 6.0518080e-002 -1.8132720e-002 +v -9.3575750e-002 1.2474629e-001 2.6213300e-002 +v -9.3189820e-002 1.2019919e-001 3.7913720e-002 +v -9.2296100e-003 1.7314463e-001 -2.4197660e-002 +v -8.1739460e-002 7.6861340e-002 2.3313610e-002 +v -3.6992750e-002 1.5063932e-001 -2.0372300e-003 +v 6.0093570e-002 6.5693450e-002 1.8533320e-002 +v 5.9837240e-002 6.6423180e-002 8.5139400e-003 +v 4.0706180e-002 6.4475310e-002 -5.5920300e-003 +v 3.4745940e-002 6.3261340e-002 -1.4646740e-002 +v -6.1879660e-002 1.6000450e-001 -2.5806250e-002 +v -7.6537810e-002 1.5344875e-001 -1.2898750e-002 +v 3.8111070e-002 6.4811810e-002 -1.1142000e-002 +v 3.1909340e-002 6.4657050e-002 -1.8473410e-002 +v -8.3159350e-002 1.4674277e-001 3.0757900e-003 +v -8.7055900e-002 1.0562761e-001 9.7651100e-003 +v -7.1448330e-002 1.8105301e-001 -5.5478550e-002 +v -8.5632110e-002 1.2461094e-001 -2.7335800e-003 +v 6.0728970e-002 6.5806600e-002 1.3974830e-002 +v 3.9909650e-002 6.8171740e-002 -9.5698200e-003 +v 3.4981790e-002 6.7740790e-002 -1.5683210e-002 +v -9.1822030e-002 1.2747346e-001 3.6458650e-002 +v -6.2425420e-002 1.6366637e-001 -4.9667290e-002 +v -7.1168950e-002 1.4740156e-001 -2.7590940e-002 +v -5.0364760e-002 1.3715763e-001 1.9526100e-003 +v -5.0492650e-002 1.4159899e-001 1.6291740e-002 +v 5.9886670e-002 6.8513050e-002 1.6171610e-002 +v -6.1406990e-002 1.7268822e-001 -5.8265750e-002 +v 2.4990740e-002 6.5897320e-002 -2.3568270e-002 +v -7.4852750e-002 1.4993112e-001 -2.7752940e-002 +v -6.2225690e-002 6.0265200e-002 2.0449290e-002 +v -6.2001940e-002 3.6435020e-002 4.3918940e-002 +v 5.8374570e-002 7.1186410e-002 1.3072740e-002 +v -3.6125040e-002 1.2286688e-001 -8.2927900e-003 +v 2.9216510e-002 6.7850250e-002 -2.0418570e-002 +v -4.1681700e-002 1.2575112e-001 -7.0193300e-003 +v -7.4226550e-002 1.6437012e-001 -3.8240340e-002 +v -9.7845700e-003 1.6928488e-001 -2.4756660e-002 +v -8.9577950e-002 1.2078310e-001 3.5229100e-003 +v -6.2311930e-002 1.6371109e-001 -4.0623990e-002 +v 4.3514770e-002 9.1519890e-002 -2.6468100e-003 +v -4.8434350e-002 1.3754973e-001 1.3244980e-002 +v -8.9313160e-002 1.3653006e-001 3.0458750e-002 +v -7.4230190e-002 1.5652681e-001 -2.5167090e-002 +v 3.7378600e-002 7.3093410e-002 -1.2635370e-002 +v 2.6321810e-002 7.0240650e-002 -2.3878680e-002 +v -4.8023620e-002 1.4426649e-001 4.2498600e-003 +v -9.2019580e-002 1.1611534e-001 3.5842730e-002 +v -7.1305510e-002 7.3899020e-002 3.5969780e-002 +v -6.2059290e-002 1.5697807e-001 -3.3784580e-002 +v -9.7015300e-003 1.6738863e-001 -1.9360250e-002 +v 4.3342140e-002 7.1676120e-002 -2.2304600e-003 +v 4.1772460e-002 6.9568020e-002 -6.1596000e-003 +v 3.3505410e-002 7.2809860e-002 -1.7034800e-002 +v 2.9665000e-002 7.1506830e-002 -2.1282340e-002 +v -2.9460160e-002 1.5550263e-001 -1.1914700e-003 +v -8.6396440e-002 1.0479356e-001 5.9820600e-003 +v -5.4910700e-002 1.4662313e-001 2.8438970e-002 +v 4.4203810e-002 8.5204260e-002 -2.1170500e-003 +v 4.3264350e-002 7.5810540e-002 -3.8843900e-003 +v 1.3096990e-002 9.1126480e-002 -2.9269770e-002 +v -6.7069210e-002 9.1144610e-002 -1.7425950e-002 +v -9.0821680e-002 1.2276896e-001 6.0998500e-003 +v 4.5620000e-002 7.4684430e-002 2.6073900e-003 +v -9.3039800e-002 1.2026416e-001 1.1216820e-002 +v 4.4635590e-002 9.2794290e-002 1.7832070e-002 +v -1.1243390e-002 1.6457514e-001 -1.8240780e-002 +v 4.5511190e-002 8.6953050e-002 3.8865500e-003 +v 4.6252720e-002 7.7373870e-002 6.9140800e-003 +v 4.0281640e-002 7.2637130e-002 -9.2881000e-003 +v 4.3218200e-002 9.9486740e-002 5.0153300e-003 +v -5.1108270e-002 1.4520219e-001 1.4279480e-002 +v 4.4692980e-002 9.2688550e-002 2.2466700e-003 +v 4.3422540e-002 9.1860370e-002 2.4538450e-002 +v 4.0751360e-002 1.0554729e-001 7.5074100e-003 +v -8.5613030e-002 9.6277110e-002 -6.6514000e-004 +v 4.0721470e-002 7.8475530e-002 -8.2130000e-003 +v 3.5538080e-002 7.6062960e-002 -1.4434750e-002 +v -9.2736510e-002 1.2073095e-001 3.2692730e-002 +v -6.2278520e-002 1.5166598e-001 -1.4672730e-002 +v 4.4960220e-002 8.0942630e-002 6.1119000e-004 +v 3.7814740e-002 7.9698150e-002 -1.3289630e-002 +v 3.3864490e-002 7.8656690e-002 -1.7632490e-002 +v -9.1044280e-002 1.4199862e-001 2.1729630e-002 +v -7.4004450e-002 1.7818523e-001 -5.3916320e-002 +v -6.1768650e-002 1.6067957e-001 -3.4046350e-002 +v -4.9747450e-002 1.4112519e-001 5.2937500e-003 +v 4.1065440e-002 9.0460700e-002 2.9888620e-002 +v -7.2916360e-002 6.5057400e-002 1.8794620e-002 +v -9.0949690e-002 1.3895375e-001 1.7371130e-002 +v 4.2879050e-002 1.0093777e-001 9.4753200e-003 +v -7.2455480e-002 1.7610676e-001 -5.3535420e-002 +v -7.5862940e-002 1.5071299e-001 -9.0209000e-003 +v -8.5269820e-002 1.0267793e-001 1.3935600e-003 +v -7.7025570e-002 1.1396763e-001 -4.6168100e-003 +v 4.6280880e-002 7.8702020e-002 1.4786330e-002 +v 4.2106910e-002 8.1533160e-002 -6.6690900e-003 +v 3.6523880e-002 8.1991750e-002 -1.6229590e-002 +v -3.7420220e-002 4.5428500e-002 -2.4226790e-002 +v -8.5148910e-002 1.3965520e-001 2.4808500e-003 +v -6.3313300e-002 1.6503258e-001 -3.2895120e-002 +v -6.1591410e-002 1.5681572e-001 -2.5945630e-002 +v 4.5918540e-002 8.7036220e-002 8.4236300e-003 +v 4.4631140e-002 8.4178380e-002 8.2665000e-004 +v -4.4842870e-002 1.4629393e-001 1.7114800e-003 +v -6.4124180e-002 1.7953625e-001 -5.8730420e-002 +v -6.7070300e-002 1.8072682e-001 -5.6618620e-002 +v -6.4793760e-002 1.7885275e-001 -5.5883250e-002 +v -6.4371030e-002 1.7296209e-001 -4.9225660e-002 +v -7.0381530e-002 1.8071180e-001 -5.3172590e-002 +v -7.5269270e-002 1.5232949e-001 3.4374060e-002 +v -1.6273090e-002 1.2844514e-001 1.6683610e-002 +v -6.2116150e-002 1.5600787e-001 1.8034420e-002 +v -5.6010790e-002 1.5381662e-001 2.5369280e-002 +v -3.7277920e-002 1.7289068e-001 -8.6627000e-004 +v -7.4158700e-002 1.7987275e-001 -5.0794750e-002 +v -7.9039960e-002 1.5537445e-001 1.5141810e-002 +v -7.2505530e-002 1.5459529e-001 2.9588830e-002 +v -6.7738180e-002 1.7728865e-001 -5.0375960e-002 +v -7.5346900e-003 1.0021302e-001 4.7488700e-002 +v -5.9575620e-002 1.5472401e-001 2.6373250e-002 +v -7.7382710e-002 1.5346600e-001 3.0894990e-002 +v -8.1496670e-002 1.5473104e-001 1.9697340e-002 +v -7.2223320e-002 1.5896734e-001 -5.4242300e-003 +v -1.3708500e-002 1.8491150e-001 -2.5549550e-002 +v -4.3465340e-002 1.2451145e-001 2.2518890e-002 +v -6.9103650e-002 1.5559479e-001 1.6370800e-003 +v -7.3748080e-002 1.5539253e-001 2.3491700e-003 +v -6.8192410e-002 1.7439828e-001 -4.5365870e-002 +v -6.0052850e-002 1.5280350e-001 3.2887630e-002 +v -2.3459490e-002 1.2615386e-001 1.6613770e-002 +v -7.2777220e-002 1.7854465e-001 -4.8208800e-002 +v -7.6595580e-002 1.7753227e-001 -4.7118080e-002 +v 1.3906410e-002 1.2790838e-001 2.5110240e-002 +v -8.6367510e-002 1.0906537e-001 1.1980640e-002 +v -3.1358850e-002 1.2140977e-001 2.5971090e-002 +v -4.9104590e-002 1.3666879e-001 1.9314030e-002 +v -4.2930640e-002 1.2928436e-001 9.2700700e-003 +v -6.5320350e-002 1.5390322e-001 9.1386000e-004 +v -3.7606490e-002 1.2422605e-001 2.4313530e-002 +v 9.5078400e-003 1.3041865e-001 2.0715020e-002 +v -1.7976800e-003 1.3117283e-001 1.6360660e-002 +v 3.6231700e-003 1.3076791e-001 2.1168600e-002 +v -9.2674700e-002 1.1701945e-001 1.1889520e-002 +v -6.5739720e-002 1.5565338e-001 2.6017600e-002 +v -8.6561940e-002 1.4249188e-001 8.4326800e-003 +v -7.0731530e-002 1.5569959e-001 6.9058200e-003 +v -8.0840700e-003 1.3030537e-001 1.6872280e-002 +v -4.4286250e-002 1.2606625e-001 2.0795220e-002 +v -7.0222260e-002 1.5143521e-001 3.6718910e-002 +v -1.5210690e-002 1.8463639e-001 -2.2057240e-002 +v -1.7270750e-002 1.8699602e-001 -1.9977570e-002 +v -8.3560950e-002 1.5255943e-001 7.6806700e-003 +v -8.8130280e-002 9.7540510e-002 5.6788000e-003 +v -8.8399240e-002 1.3899000e-001 1.0640660e-002 +v -6.7780550e-002 1.5614453e-001 1.4276320e-002 +v -6.5864600e-003 1.2641717e-001 3.0226390e-002 +v -8.8746180e-002 1.3625578e-001 7.1477800e-003 +v -7.7206730e-002 1.5639950e-001 -1.8972540e-002 +v -9.3176480e-002 1.1821016e-001 2.3362360e-002 +v -2.3506850e-002 1.2672006e-001 1.0996900e-002 +v -6.6546650e-002 1.7171115e-001 -4.2127770e-002 +v -6.9136000e-002 1.7247836e-001 -3.9013330e-002 +v 5.7180270e-002 7.1107690e-002 8.0307600e-003 +v -7.5390870e-002 1.7952824e-001 -5.2402050e-002 +v -3.1828840e-002 1.2639115e-001 1.0013410e-002 +v -8.9888800e-003 1.2952269e-001 2.2026810e-002 +v 3.4325880e-002 1.1193312e-001 -2.2406500e-003 +v -8.1414950e-002 9.7100250e-002 -6.8745800e-003 +v -2.3298830e-002 1.8324307e-001 -1.7923000e-002 +v -6.1641660e-002 1.5582039e-001 1.1099820e-002 +v -8.8826450e-002 9.0483320e-002 2.1204700e-002 +v 5.8373130e-002 6.8067590e-002 5.7247600e-003 +v -4.3045630e-002 1.2785122e-001 1.6842260e-002 +v 3.0835720e-002 1.1554234e-001 -3.1785500e-003 +v -8.8631270e-002 9.4881200e-002 7.9337600e-003 +v -9.1715140e-002 1.1709957e-001 3.0809400e-002 +v -7.2083780e-002 1.7499844e-001 -4.1930320e-002 +v -6.9540630e-002 1.5308527e-001 3.3865720e-002 +v 6.0078690e-002 6.8129260e-002 1.1454500e-002 +v -4.0081060e-002 1.2628381e-001 1.9607250e-002 +v 3.2819930e-002 1.1655625e-001 4.4458600e-003 +v -7.2823220e-002 1.4510601e-001 -1.5654680e-002 +v -8.5270210e-002 1.0551770e-001 2.3290940e-002 +v -7.6051320e-002 1.1103825e-001 -6.2722100e-003 +v -8.6537730e-002 1.5154801e-001 2.5875370e-002 +v 5.5888480e-002 7.2579250e-002 1.0669650e-002 +v -5.4642360e-002 1.5522963e-001 1.2612400e-002 +v 3.6729960e-002 1.1116756e-001 3.8670600e-003 +v 3.1501870e-002 1.1725172e-001 1.6855100e-003 +v -7.8751550e-002 9.5240290e-002 -1.0600670e-002 +v -8.9408160e-002 1.4352815e-001 3.0924750e-002 +v -2.0891130e-002 1.8595338e-001 -1.5037360e-002 +v -7.0863560e-002 1.6136525e-001 -9.7324600e-003 +v -7.0919760e-002 1.7136688e-001 -3.2763750e-002 +v -3.0771290e-002 1.2564075e-001 1.6594770e-002 +v -5.4454180e-002 1.5297699e-001 2.2505190e-002 +v -1.5539500e-003 1.2754717e-001 2.9232870e-002 +v 2.9130550e-002 1.2027445e-001 6.1117500e-003 +v 2.5725940e-002 1.2122705e-001 -3.6150000e-005 +v -8.9318970e-002 9.9546980e-002 1.3418110e-002 +v -7.5429500e-002 1.7095605e-001 -3.2879890e-002 +v -2.8596020e-002 1.1901156e-001 2.9888170e-002 +v 2.1069780e-002 1.2497756e-001 1.0998100e-003 +v -9.2240760e-002 1.1816838e-001 4.1201730e-002 +v 2.4094600e-003 1.0016785e-001 4.6938070e-002 +v -5.6627620e-002 1.5270606e-001 2.9629030e-002 +v -5.7264800e-002 1.5506250e-001 1.9322430e-002 +v -3.6452070e-002 1.2199869e-001 2.7670650e-002 +v -7.4108160e-002 1.7355729e-001 -3.7986840e-002 +v 5.1537130e-002 7.3496690e-002 1.2698700e-002 +v -6.6096040e-002 1.5532529e-001 7.1561800e-003 +v 3.6102000e-002 1.1266103e-001 1.0491780e-002 +v 1.6715210e-002 1.2689851e-001 2.2331000e-004 +v -8.0767920e-002 1.4301400e-001 -1.5312800e-003 +v -9.1757600e-002 1.4334588e-001 1.7790710e-002 +v -8.6824940e-002 1.5280775e-001 1.5521450e-002 +v -6.5808100e-002 1.6764344e-001 -3.0558670e-002 +v -7.8217340e-002 1.6873975e-001 -3.3564250e-002 +v -7.2567060e-002 1.4753230e-001 4.1714090e-002 +v 5.8439960e-002 7.0200810e-002 1.7779620e-002 +v 5.6847560e-002 7.2017160e-002 1.7139380e-002 +v 5.4919390e-002 7.3161610e-002 1.5223590e-002 +v 4.7446900e-002 7.3691410e-002 1.2430020e-002 +v 1.2319360e-002 1.2903768e-001 1.3336200e-003 +v -7.9790640e-002 1.0351662e-001 -6.6275400e-003 +v -7.6655210e-002 1.5509766e-001 7.9686300e-003 +v 2.1747320e-002 1.2118456e-001 3.0878810e-002 +v -7.5260490e-002 1.4938613e-001 3.9175980e-002 +v -2.5919610e-002 1.8272826e-001 -1.3541090e-002 +v -6.7983790e-002 1.6974781e-001 -3.1627490e-002 +v 1.6831110e-002 1.2487146e-001 2.8425580e-002 +v 5.4016490e-002 7.2883850e-002 1.8678010e-002 +v 5.0522750e-002 7.3397910e-002 1.6166890e-002 +v -5.9582440e-002 1.5623338e-001 7.9209900e-003 +v 2.5343500e-002 1.2374750e-001 9.9818800e-003 +v 1.9262750e-002 1.2689390e-001 5.5552100e-003 +v -9.0758520e-002 1.4223375e-001 2.6008130e-002 +v -4.6548490e-002 1.3320769e-001 1.6889630e-002 +v -2.4106950e-002 1.8380887e-001 -1.1544760e-002 +v 8.6784400e-003 1.2894574e-001 2.6156880e-002 +v 2.4919200e-003 1.2983563e-001 2.4847110e-002 +v 5.7345150e-002 6.9482720e-002 2.1153510e-002 +v -8.5329840e-002 1.5339912e-001 2.0378290e-002 +v 3.2877320e-002 1.1691463e-001 9.2957500e-003 +v 2.4246630e-002 1.2377758e-001 4.8764500e-003 +v -4.7765650e-002 1.3301969e-001 2.2874020e-002 +v -6.3541830e-002 1.6332115e-001 -2.5912990e-002 +v -6.6605200e-002 1.6477375e-001 -2.0670760e-002 +v -6.8504220e-002 1.6732018e-001 -2.3959570e-002 +v -7.2759160e-002 1.6965906e-001 -2.7013420e-002 +v 4.8206850e-002 7.2698580e-002 1.6994630e-002 +v -2.7383180e-002 1.2324257e-001 2.1658860e-002 +v -4.5077500e-002 1.3124443e-001 1.1145770e-002 +v 2.9253150e-002 1.2057701e-001 1.2299330e-002 +v 1.3677610e-002 1.2967262e-001 6.9327400e-003 +v 8.4210900e-003 1.3090986e-001 6.2754400e-003 +v 9.6836000e-004 1.3064303e-001 2.5865900e-003 +v 3.0802000e-003 9.8307360e-002 5.0535640e-002 +v -5.2420170e-002 1.5310101e-001 1.2927370e-002 +v -7.0359720e-002 1.6906988e-001 -2.6144260e-002 +v 5.4359390e-002 7.1467260e-002 2.1381250e-002 +v 4.5161440e-002 7.1030380e-002 2.2530690e-002 +v 1.9320440e-002 1.2738348e-001 1.1296310e-002 +v -9.3281210e-002 1.2691094e-001 1.3505010e-002 +v -8.7405060e-002 1.0593990e-001 1.3645920e-002 +v -2.2851640e-002 9.0635040e-002 5.2280460e-002 +v -6.2099370e-002 1.5406697e-001 3.0837360e-002 +v -4.5851560e-002 1.2072981e-001 2.7665040e-002 +v 5.0781670e-002 7.2155170e-002 2.0680180e-002 +v -8.9607270e-002 1.3971105e-001 2.9308560e-002 +v -5.3323050e-002 1.5273520e-001 1.6213860e-002 +v -1.5227080e-002 1.2784878e-001 2.1545200e-002 +v 3.3663540e-002 1.1574212e-001 1.7181290e-002 +v 2.4000260e-002 1.2468761e-001 1.5517930e-002 +v -8.4166840e-002 9.7756820e-002 -3.2761900e-003 +v -3.6223590e-002 1.2777519e-001 9.8501500e-003 +v -3.9189580e-002 1.2828193e-001 5.0346300e-003 +v -3.3674050e-002 1.7774449e-001 -8.1799500e-003 +v -7.4488620e-002 1.5649443e-001 -2.5954600e-003 +v -4.6755620e-002 1.3284294e-001 8.1212800e-003 +v -8.4970410e-002 1.5322309e-001 1.2654460e-002 +v -1.0866210e-002 1.2691699e-001 2.7575440e-002 +v -3.1074000e-003 1.3072898e-001 5.6428500e-003 +v -8.8760540e-002 9.7037440e-002 2.1079040e-002 +v -6.4811320e-002 3.4530640e-002 1.5508440e-002 +v -6.4300260e-002 3.5086450e-002 2.4272050e-002 +v -6.6727020e-002 3.5895770e-002 3.3849430e-002 +v 1.9838510e-002 9.6518890e-002 -2.2785880e-002 +v -3.8670510e-002 1.6070199e-001 -1.2357760e-002 +v -7.6890090e-002 1.3041906e-001 -6.9570100e-003 +v -7.2539730e-002 3.5399270e-002 7.0298800e-003 +v -6.9209050e-002 3.5454810e-002 1.2042140e-002 +v -6.4160810e-002 3.5900770e-002 1.7687570e-002 +v -6.6804150e-002 3.7377740e-002 3.3296290e-002 +v -6.2928350e-002 3.9061660e-002 4.2707680e-002 +v -7.1752230e-002 3.6789350e-002 8.6966700e-003 +v -6.5171380e-002 3.7289500e-002 2.5953770e-002 +v -6.6392030e-002 3.7712350e-002 2.9621950e-002 +v -6.4558720e-002 3.9639900e-002 3.9411530e-002 +v -6.0145790e-002 4.1202050e-002 4.4293830e-002 +v -6.0318430e-002 3.8442990e-002 4.5245950e-002 +v -3.6756310e-002 8.8663360e-002 -2.3868800e-002 +v -3.9494750e-002 3.7551570e-002 4.2870900e-002 +v -7.2016030e-002 3.7572700e-002 3.9789400e-003 +v -7.1693630e-002 3.9461000e-002 6.0145000e-003 +v -7.1165950e-002 3.9366310e-002 8.1142100e-003 +v -6.9000300e-002 3.8467710e-002 1.0768900e-002 +v -6.7253420e-002 3.8142160e-002 1.3533960e-002 +v -6.1125670e-002 3.7790050e-002 1.9710900e-002 +v -3.9179680e-002 4.2406740e-002 4.1476070e-002 +v -3.5145960e-002 3.8585920e-002 4.7732690e-002 +v -2.8950940e-002 3.9285940e-002 5.3309090e-002 +v -1.8223900e-002 9.7494570e-002 4.6847940e-002 +v -6.6916260e-002 1.2278907e-001 -8.9077400e-003 +v -6.3754640e-002 3.8250120e-002 1.6593500e-002 +v -6.4415760e-002 4.1283840e-002 2.8243480e-002 +v -8.5856340e-002 9.7025390e-002 2.7414960e-002 +v -3.7501130e-002 4.0221900e-002 4.4296550e-002 +v -3.4333970e-002 4.0923630e-002 4.8425810e-002 +v -3.1172890e-002 4.0294330e-002 5.1312460e-002 +v -6.9997320e-002 4.2073080e-002 6.6897800e-003 +v -8.0379330e-002 9.7800660e-002 3.3645750e-002 +v -2.6273160e-002 7.7631160e-002 4.8356180e-002 +v -3.7501450e-002 4.2736690e-002 4.2988400e-002 +v -2.6177500e-002 4.2498930e-002 5.3315220e-002 +v -6.9637250e-002 4.1881270e-002 3.1825800e-003 +v -6.7156510e-002 4.1972860e-002 1.0240940e-002 +v -8.7405510e-002 1.0205209e-001 2.2020360e-002 +v -2.3944380e-002 7.8800140e-002 5.3534730e-002 +v -6.0902360e-002 4.3429500e-002 4.2678530e-002 +v -3.1217880e-002 4.3847510e-002 4.9780920e-002 +v -7.5729440e-002 1.0354026e-001 3.6070970e-002 +v -6.2425320e-002 4.1885720e-002 1.4646770e-002 +v -6.1051660e-002 4.4392230e-002 1.2421940e-002 +v 2.5855060e-002 8.9610660e-002 -2.2701840e-002 +v -7.7644960e-002 8.2214940e-002 3.5797660e-002 +v -6.0381270e-002 4.5921420e-002 4.0088740e-002 +v -2.4982010e-002 8.1777650e-002 5.3421060e-002 +v -3.4453850e-002 4.4563960e-002 4.5422990e-002 +v -2.9842910e-002 4.6782280e-002 4.7746920e-002 +v -1.5119580e-002 9.9930020e-002 4.4500270e-002 +v -6.7306470e-002 4.4176830e-002 7.5958300e-003 +v -5.7852990e-002 4.6444500e-002 1.1062610e-002 +v -5.1815260e-002 1.6392582e-001 1.7488800e-003 +v -5.5174130e-002 4.8383880e-002 3.8517780e-002 +v -7.8849150e-002 1.1867375e-001 5.0622870e-002 +v -2.7229070e-002 8.7991480e-002 4.7909730e-002 +v -7.5536880e-002 1.5977062e-001 -1.0438650e-002 +v -3.6151280e-002 4.6505140e-002 4.0740900e-002 +v -2.5439220e-002 9.0677870e-002 4.8852330e-002 +v -8.0050370e-002 1.1670406e-001 4.8762460e-002 +v -5.2513640e-002 4.7577880e-002 1.4858440e-002 +v -3.2043560e-002 5.0461830e-002 3.9341520e-002 +v -3.1487770e-002 4.6930210e-002 4.5253210e-002 +v -2.0321500e-002 9.3999570e-002 5.1588540e-002 +v -7.2145040e-002 9.1556450e-002 4.1494780e-002 +v -5.3644200e-002 4.9358170e-002 1.2201850e-002 +v -8.2403890e-002 1.2186563e-001 4.9365030e-002 +v -4.9754420e-002 4.9738300e-002 3.7037110e-002 +v -3.2332060e-002 4.8672840e-002 4.2523960e-002 +v -2.3122950e-002 9.4515900e-002 4.7358870e-002 +v -8.6347140e-002 9.1722090e-002 2.6811080e-002 +v -5.7713110e-002 4.8717820e-002 7.2765100e-003 +v -8.6970360e-002 8.8912090e-002 2.4879860e-002 +v -9.2237750e-002 1.2488519e-001 4.0786530e-002 +v -1.5862800e-002 9.7021620e-002 5.0139360e-002 +v -2.7720040e-002 5.0502090e-002 4.3340720e-002 +v -8.5918770e-002 1.4263412e-001 3.9849810e-002 +v -7.5097360e-002 9.0073560e-002 3.9581000e-002 +v -8.9430840e-002 1.4730552e-001 2.7694960e-002 +v -5.3288350e-002 5.1925760e-002 1.1730350e-002 +v -5.0168720e-002 5.3462260e-002 1.6255440e-002 +v -8.5986050e-002 1.4670902e-001 3.4827030e-002 +v -6.9937250e-002 8.6076860e-002 4.2175690e-002 +v -5.0399320e-002 5.1831330e-002 3.4037400e-002 +v -8.3298980e-002 1.4960772e-001 3.3740890e-002 +v -2.9174820e-002 5.2264530e-002 3.7637320e-002 +v -8.8763730e-002 1.1944938e-001 4.6560090e-002 +v -7.7693460e-002 1.7367969e-001 -4.1478670e-002 +v -8.3418140e-002 9.4127440e-002 3.0898450e-002 +v -5.6067510e-002 5.3470630e-002 7.3718200e-003 +v -7.8935630e-002 1.4817228e-001 3.9463070e-002 +v -6.7902770e-002 8.7817230e-002 4.3526990e-002 +v -4.4111240e-002 9.2883990e-002 -2.2373210e-002 +v -8.6605100e-002 1.3226807e-001 4.6783020e-002 +v -9.2654280e-002 1.2084025e-001 4.1629650e-002 +v -5.0887310e-002 5.2727900e-002 1.4455790e-002 +v -4.9763410e-002 5.6241200e-002 3.3624250e-002 +v -8.9771330e-002 1.2904861e-001 4.3022990e-002 +v -2.8054240e-002 5.4551030e-002 3.6786850e-002 +v -2.5867080e-002 5.6689210e-002 3.9182240e-002 +v -8.3702200e-002 1.2226381e-001 -3.7301400e-003 +v -8.1455470e-002 1.3012213e-001 5.2117660e-002 +v -5.1458550e-002 5.5878150e-002 1.5900350e-002 +v -7.8597700e-002 1.7441574e-001 -4.6607580e-002 +v -5.2909820e-002 5.7043070e-002 2.0988410e-002 +v -5.2978500e-002 5.9553770e-002 2.6211920e-002 +v -5.2130640e-002 5.6302970e-002 2.6672460e-002 +v -4.7714500e-002 6.1944520e-002 3.6705820e-002 +v -8.3539790e-002 8.1169560e-002 2.7014070e-002 +v -1.8340000e-002 5.7489970e-002 4.9763020e-002 +v -8.0069810e-002 9.0586130e-002 3.4593070e-002 +v -8.3812250e-002 8.6337700e-002 2.9223270e-002 +v -5.5436650e-002 5.9420250e-002 2.3018970e-002 +v -8.2227680e-002 1.4513771e-001 4.0600080e-002 +v -2.4187580e-002 7.2269150e-002 4.7681090e-002 +v -2.5353150e-002 6.2567200e-002 4.0642170e-002 +v -9.1132110e-002 1.2282100e-001 4.4115160e-002 +v -4.6076290e-002 1.6819719e-001 7.3744000e-004 +v -8.7829280e-002 1.4351461e-001 3.5707670e-002 +v -8.6990640e-002 1.3812326e-001 4.2316550e-002 +v -1.5715900e-002 6.0822970e-002 5.2365440e-002 +v -8.3803580e-002 1.2561100e-001 5.0440490e-002 +v -6.2786680e-002 1.1274190e-001 -1.3605440e-002 +v -8.1033840e-002 8.4698180e-002 3.3106400e-002 +v -8.8563540e-002 1.1624535e-001 4.5392840e-002 +v -2.0268380e-002 6.2266810e-002 4.8212120e-002 +v -1.2619630e-002 6.1635030e-002 5.4424080e-002 +v -7.0491190e-002 8.1818160e-002 4.0609890e-002 +v -8.3882520e-002 1.3331465e-001 4.9113540e-002 +v -5.6560350e-002 4.8355540e-002 3.6607050e-002 +v 9.9444900e-003 1.0919723e-001 -1.9472810e-002 +v -5.5928250e-002 3.5917310e-002 4.6376100e-002 +v -7.6003260e-002 1.6361344e-001 -1.8021110e-002 +v -8.3798850e-002 1.0290691e-001 2.8038330e-002 +v -8.8252110e-002 1.2692730e-001 4.6141300e-002 +v -7.9126720e-002 1.0619883e-001 3.2050700e-002 +v -8.8206230e-002 9.4485700e-002 2.3744010e-002 +v -8.9110330e-002 1.3851394e-001 3.7658780e-002 +v -1.9321360e-002 9.2123890e-002 5.3820650e-002 +v -5.8265630e-002 9.0926390e-002 -2.0948690e-002 +v -2.7046310e-002 6.7014450e-002 3.9672140e-002 +v -2.1416300e-002 1.7977662e-001 -2.1732520e-002 +v -7.8240000e-003 1.0924112e-001 -2.2185670e-002 +v -2.3988340e-002 8.5995590e-002 5.3716430e-002 +v -6.0483580e-002 1.5567975e-001 4.3343800e-003 +v -8.6389150e-002 1.2168475e-001 4.8412440e-002 +v -7.4084360e-002 1.4987744e-001 -3.2610050e-002 +v -2.0580600e-002 7.9572500e-002 5.6013880e-002 +v -8.3837500e-002 1.3927865e-001 4.4893850e-002 +v -2.2933960e-002 3.5632910e-002 5.2865490e-002 +v -8.6153620e-002 1.2735612e-001 4.8563960e-002 +v -6.5728590e-002 1.0709818e-001 -1.4317670e-002 +v -2.1481090e-002 7.4194460e-002 5.2857680e-002 +v -7.6423900e-002 1.5736285e-001 -9.0354600e-003 +v -7.7216010e-002 8.5594880e-002 3.7420770e-002 +v -8.4150830e-002 1.2955013e-001 5.0483700e-002 +v -8.1221440e-002 8.1003250e-002 3.1255840e-002 +v -8.1704000e-002 1.0167226e-001 3.0939660e-002 +v -8.6252730e-002 1.0106846e-001 2.5413770e-002 +v -8.0944970e-002 1.3903572e-001 4.7359080e-002 +v -7.8908350e-002 9.4830900e-002 3.5435500e-002 +v -7.3440160e-002 9.5412600e-002 4.0210650e-002 +v -5.2675780e-002 8.8220740e-002 -2.1886300e-002 +v -7.6440670e-002 7.7511060e-002 3.3748300e-002 +v -2.1791140e-002 1.0658035e-001 -2.2327000e-002 +v -8.8360940e-002 1.4996706e-001 2.6044170e-002 +v -2.4078870e-002 6.7906700e-002 4.5178370e-002 +v -2.0018090e-002 6.7569300e-002 5.1565340e-002 +v -8.3577750e-002 1.2052625e-001 4.9177500e-002 +v -1.4655950e-002 1.7456543e-001 -2.5972690e-002 +v -2.7395940e-002 8.4108300e-002 4.8745680e-002 +v -4.1933580e-002 8.8463400e-002 -2.2126350e-002 +v -3.1693900e-002 1.0261265e-001 -2.2352310e-002 +v -2.7890200e-002 1.0440703e-001 -2.2830920e-002 +v -7.3790400e-002 1.2016662e-001 -7.8851200e-003 +v -4.6124160e-002 1.0506369e-001 -2.0457580e-002 +v -2.7412650e-002 7.3269450e-002 4.2641380e-002 +v -4.5532880e-002 3.4736480e-002 -2.1363200e-002 +v -4.4993030e-002 3.9017010e-002 -2.1097830e-002 +v -4.6462610e-002 3.6800270e-002 -1.7778710e-002 +v -8.8366460e-002 1.1361863e-001 5.8227800e-003 +v 5.1746240e-002 7.2897250e-002 9.0647400e-003 +v -7.0385250e-002 3.7450300e-002 -9.3190000e-004 +v -6.0923170e-002 3.8621820e-002 2.2468850e-002 +v -7.7696720e-002 1.7027889e-001 -4.3117910e-002 +v -4.3793210e-002 1.6955506e-001 -7.3026400e-003 +v -7.7587180e-002 1.7717875e-001 -5.0221090e-002 +v -4.0541880e-002 3.8886010e-002 -2.7364950e-002 +v -4.4215850e-002 3.6131460e-002 -2.4252210e-002 +v -6.6634880e-002 4.0430310e-002 -5.0180700e-003 +v -6.9242120e-002 4.1474050e-002 1.9289000e-004 +v -7.5640690e-002 1.5930400e-001 -2.6908460e-002 +v -6.3087030e-002 3.9614170e-002 2.5181560e-002 +v -7.2303020e-002 1.5186699e-001 -4.1544310e-002 +v -4.1051490e-002 4.1528620e-002 -2.4061000e-002 +v -4.6990580e-002 3.8892380e-002 -1.4016920e-002 +v -8.9559690e-002 1.2851666e-001 4.5457500e-003 +v -7.6987340e-002 1.5369375e-001 -2.2970800e-003 +v -7.0121670e-002 1.6882633e-001 -5.1173650e-002 +v -6.4792610e-002 4.1724530e-002 3.1616900e-002 +v -4.2148060e-002 1.2409627e-001 -9.5602500e-003 +v -4.8069700e-002 1.2493027e-001 -8.4076400e-003 +v -4.2150480e-002 4.3343970e-002 -2.1508710e-002 +v -6.7315160e-002 4.4034000e-002 1.5741800e-003 +v -7.3386640e-002 1.5463418e-001 -2.9943830e-002 +v -5.5352770e-002 4.2936210e-002 1.9135490e-002 +v -6.0067770e-002 4.1419500e-002 2.2953280e-002 +v -6.5488460e-002 4.0937780e-002 3.5315470e-002 +v -8.0066400e-002 1.5039650e-001 6.0518000e-004 +v -4.4031300e-002 4.1949070e-002 -1.7993960e-002 +v -4.5186510e-002 4.2453420e-002 -1.4193620e-002 +v -8.3109430e-002 1.0265445e-001 -3.2933400e-003 +v -6.5472800e-002 4.5627570e-002 4.5575400e-003 +v -7.5427730e-002 1.5201213e-001 -1.4393690e-002 +v -5.4473420e-002 4.5937510e-002 2.3612600e-002 +v -6.2464100e-002 4.3722000e-002 2.8493310e-002 +v -6.2832600e-002 4.5182750e-002 3.4622890e-002 +v -6.3538130e-002 4.3524020e-002 3.7974010e-002 +v -6.0255260e-002 4.4749620e-002 -4.1316200e-003 +v -6.3242050e-002 4.5549700e-002 4.8428000e-004 +v -6.2249430e-002 4.6540050e-002 7.1903500e-003 +v -9.1003650e-002 1.4885725e-001 2.1507030e-002 +v -5.7094130e-002 4.5996540e-002 2.6865280e-002 +v -5.7276490e-002 4.7299580e-002 2.9889950e-002 +v -3.9519900e-002 1.7385855e-001 -7.5752600e-003 +v -8.9641110e-002 1.3841920e-001 3.4141800e-002 +v -9.2601430e-002 1.3018652e-001 2.5183580e-002 +v -9.2280860e-002 1.2762053e-001 2.9751670e-002 +v -3.3957310e-002 4.1025060e-002 -2.9660250e-002 +v -9.0199540e-002 1.1657506e-001 5.6754900e-003 +v -5.8515890e-002 4.7731310e-002 2.1246000e-004 +v -7.1723560e-002 1.4617438e-001 -2.1567820e-002 +v -5.2389820e-002 4.5449130e-002 1.7686300e-002 +v -5.9414350e-002 4.7277990e-002 3.4172420e-002 +v -5.7520620e-002 1.5877600e-001 4.1621200e-003 +v -8.0959140e-002 1.0926674e-001 -2.0189900e-003 +v -5.1904000e-002 4.6100060e-002 1.9421290e-002 +v -5.1830050e-002 4.8568730e-002 2.1647030e-002 +v -7.7650400e-002 1.5658012e-001 -1.6599150e-002 +v -3.7416450e-002 4.7682130e-002 -1.7147280e-002 +v -7.8876110e-002 1.5347012e-001 3.9875800e-003 +v -5.7635420e-002 5.0425540e-002 4.6108400e-003 +v -5.2625440e-002 5.0434620e-002 2.9046740e-002 +v -5.2998720e-002 4.9169020e-002 3.3967600e-002 +v -7.3502600e-002 1.6871934e-001 -4.4791800e-002 +v -5.4420720e-002 4.7836520e-002 -5.9186900e-003 +v -5.2312740e-002 5.1085350e-002 2.4485690e-002 +v -7.9129930e-002 1.6736568e-001 -3.5506230e-002 +v 9.4115700e-003 1.2350285e-001 -9.8291000e-003 +v -3.2715700e-002 1.0896631e-001 -1.8941410e-002 +v -3.1133380e-002 4.9607260e-002 -1.9406940e-002 +v 4.5997330e-002 6.9814450e-002 3.0143300e-003 +v 3.3525460e-002 1.0966209e-001 -6.9894800e-003 +v -5.5047160e-002 5.2767560e-002 -3.9461300e-003 +v -5.6897890e-002 4.9655570e-002 -1.5319000e-003 +v -5.0290500e-002 4.9098930e-002 1.7164780e-002 +v -5.0595170e-002 4.9923270e-002 1.9174130e-002 +v -5.1887420e-002 5.3324670e-002 2.8705560e-002 +v -6.7684480e-002 1.6533627e-001 -5.5466400e-002 +v -3.0271440e-002 5.2106080e-002 -1.7676140e-002 +v -9.1087300e-003 1.1141669e-001 -2.0543230e-002 +v -5.7069360e-002 5.4424380e-002 2.3395500e-003 +v -3.2748380e-002 1.7759875e-001 -1.1627470e-002 +v -2.9009580e-002 5.1265290e-002 -2.2175780e-002 +v -3.1383130e-002 5.1791310e-002 -1.3886800e-002 +v -5.5673960e-002 5.6983850e-002 -3.3510400e-003 +v -5.0916050e-002 5.3813610e-002 1.9753140e-002 +v -8.8875380e-002 1.5169443e-001 2.0086580e-002 +v -7.7153050e-002 1.7378676e-001 -4.7867620e-002 +v -7.8577770e-002 1.6420639e-001 -3.1825860e-002 +v -2.7545910e-002 5.4021570e-002 -2.5147390e-002 +v -5.4463660e-002 5.5357450e-002 1.0326840e-002 +v -8.7041410e-002 1.3058932e-001 9.1161000e-004 +v -9.0009340e-002 1.3278082e-001 5.9220600e-003 +v -9.2232620e-002 1.3195400e-001 1.5430650e-002 +v -4.8639980e-002 1.6472475e-001 -5.0591500e-003 +v -5.4066480e-002 5.9959350e-002 -7.5992200e-003 +v -5.7434090e-002 5.7683500e-002 8.7259700e-003 +v -8.6794730e-002 1.3850688e-001 4.5575900e-003 +v -9.2989530e-002 1.3092307e-001 1.9919290e-002 +v -9.1282030e-002 1.3311897e-001 2.4688630e-002 +v 2.1815020e-002 1.1770533e-001 -1.0015300e-002 +v -2.9647120e-002 5.8104260e-002 -2.1311320e-002 +v -3.1289530e-002 5.5208570e-002 -1.4387840e-002 +v -5.9002160e-002 5.9234620e-002 2.6140800e-003 +v -9.0241700e-002 1.3575994e-001 1.4149160e-002 +v -6.1569420e-002 1.7084875e-001 -6.1679170e-002 +v -6.6070180e-002 1.6557822e-001 -5.8644080e-002 +v -2.4539930e-002 1.8005865e-001 -1.8726950e-002 +v -1.6131750e-002 1.8298848e-001 -2.6037190e-002 +v -3.0809390e-002 5.6998040e-002 -1.7835020e-002 +v 1.0464280e-002 9.6180450e-002 -2.5898970e-002 +v -5.7491630e-002 5.9530160e-002 -1.0786100e-003 +v -8.9146460e-002 1.3650500e-001 2.5952780e-002 +v 4.3714500e-003 1.0391901e-001 -2.1515100e-002 +v -9.0377040e-002 1.3252490e-001 3.1082650e-002 +v -9.0795450e-002 1.3855232e-001 2.0562560e-002 +v -9.4237710e-002 1.2615419e-001 2.2201450e-002 +v -9.0336910e-002 1.3119830e-001 3.8138790e-002 +v -4.5082610e-002 1.2218447e-001 -1.1569430e-002 +v 1.1348010e-002 9.8243750e-002 -2.3024250e-002 +v -3.9227920e-002 9.9184630e-002 -2.1912720e-002 +v -6.5509530e-002 1.5857325e-001 -5.5600270e-002 +v -7.7409510e-002 1.6260515e-001 -2.0754580e-002 +v -4.8580010e-002 1.6689211e-001 -2.5256100e-003 +v -7.6922910e-002 1.5351394e-001 -9.0785600e-003 +v -6.7750580e-002 1.5734825e-001 -5.3982110e-002 +v 5.2906410e-002 6.5230450e-002 -5.1112000e-004 +v -2.9054820e-002 6.1084120e-002 -2.4918230e-002 +v -3.1066920e-002 6.5058860e-002 -2.2751080e-002 +v 2.4249720e-002 1.0266151e-001 -1.8313830e-002 +v -5.5473660e-002 1.6050213e-001 1.3763500e-003 +v -6.6642850e-002 1.6040875e-001 -5.6842680e-002 +v -7.8200320e-002 1.6073213e-001 -2.3999690e-002 +v -1.8320680e-002 1.1968625e-001 -1.1110660e-002 +v 2.1712970e-002 1.0956342e-001 -1.5081090e-002 +v -6.8382640e-002 1.5980248e-001 -5.4208800e-002 +v -2.5445620e-002 6.0208550e-002 -3.0864700e-002 +v -2.6540330e-002 6.5084000e-002 -3.1664870e-002 +v -2.8425710e-002 6.2199610e-002 -2.7938500e-002 +v -3.2605750e-002 6.1264600e-002 -1.5453010e-002 +v -7.0872290e-002 1.1611638e-001 -7.9563700e-003 +v -6.9780530e-002 1.5938570e-001 -4.9418240e-002 +v -3.0324870e-002 6.7694720e-002 -2.7654950e-002 +v -3.2977370e-002 6.6365180e-002 -1.8385530e-002 +v 1.3533490e-002 1.0255388e-001 -2.1579310e-002 +v 4.4408530e-002 6.9758860e-002 9.4765000e-004 +v -2.1999000e-003 1.1215881e-001 -1.9658660e-002 +v -7.2028500e-002 6.7046610e-002 -7.2256000e-004 +v -7.8699630e-002 1.7313910e-001 -4.2720470e-002 +v -8.3211970e-002 1.5072131e-001 4.2128500e-003 +v -8.7439060e-002 1.3374875e-001 2.3974700e-003 +v 2.6348020e-002 8.4562230e-002 -2.3151710e-002 +v -7.4901490e-002 7.0419350e-002 -2.2854300e-003 +v -5.4576350e-002 9.1562950e-002 -2.2098700e-002 +v -7.3242520e-002 1.5231332e-001 -3.5703520e-002 +v -7.4550960e-002 1.7218738e-001 -4.7551010e-002 +v -2.8680680e-002 6.8283500e-002 -3.0610160e-002 +v 1.7372900e-002 1.0246037e-001 -2.1487700e-002 +v -8.1257430e-002 7.3025200e-002 7.1020400e-003 +v -7.4982300e-002 1.5407794e-001 -1.8974470e-002 +v -9.1556500e-002 1.3196262e-001 1.0638150e-002 +v -8.2448000e-004 9.5165120e-002 -3.2056320e-002 +v -7.7618830e-002 7.3999130e-002 -5.3263500e-003 +v -7.9858790e-002 7.2755040e-002 3.0420200e-003 +v -8.1627470e-002 7.3470610e-002 1.1161690e-002 +v -7.3679290e-002 1.4785987e-001 -2.0236290e-002 +v -9.1309820e-002 1.4848588e-001 1.6270070e-002 +v -9.0850140e-002 1.4625613e-001 1.4809050e-002 +v -6.8543890e-002 1.7513008e-001 -5.7187900e-002 +v -2.7253960e-002 1.0747453e-001 -2.1279680e-002 +v 2.1443580e-002 1.2273826e-001 -2.9316700e-003 +v -7.9061200e-002 7.3724300e-002 -8.4521000e-004 +v -8.2063500e-002 7.5993670e-002 1.7615500e-003 +v -8.3736580e-002 7.6771840e-002 8.9586000e-003 +v -9.0205720e-002 1.4947775e-001 1.3035090e-002 +v 8.4818000e-004 1.1670025e-001 -1.7337090e-002 +v -7.4577550e-002 1.5164041e-001 -2.8647990e-002 +v -2.9087460e-002 7.2924630e-002 -3.3354470e-002 +v -3.1184020e-002 7.3989530e-002 -3.0339870e-002 +v -3.2606620e-002 7.1955620e-002 -2.4866580e-002 +v -8.0575990e-002 7.6607800e-002 -2.9879400e-003 +v -8.9491020e-002 1.4392581e-001 1.2488490e-002 +v -7.7388410e-002 1.4656426e-001 -4.3543000e-003 +v -7.2896160e-002 1.5834962e-001 -3.4109420e-002 +v 7.1346500e-003 1.1468229e-001 -1.8345640e-002 +v -3.4502610e-002 7.6130020e-002 -2.2373150e-002 +v -8.3890740e-002 8.0789530e-002 2.2951400e-003 +v -8.3740480e-002 7.7240270e-002 4.6673300e-003 +v -8.6204620e-002 8.0930750e-002 1.0535420e-002 +v -8.6061500e-002 7.9931100e-002 1.4440780e-002 +v -8.1542760e-002 7.7950660e-002 2.6727280e-002 +v 2.6666170e-002 1.1268609e-001 -1.0509540e-002 +v -7.6041430e-002 1.5663068e-001 -2.1420480e-002 +v -9.0012110e-002 1.5083344e-001 1.5752740e-002 +v -7.1156510e-002 1.6335125e-001 -4.5360530e-002 +v -3.3210960e-002 7.6873190e-002 -2.7708380e-002 +v -7.3263090e-002 7.9983830e-002 -1.3749940e-002 +v -7.9285950e-002 8.0048830e-002 -7.0125500e-003 +v -8.6034510e-002 8.2645720e-002 1.9542680e-002 +v -8.4335410e-002 8.0729950e-002 2.2180460e-002 +v -7.1351460e-002 1.5727092e-001 -4.2183090e-002 +v -7.3548450e-002 1.6120822e-001 -3.5288420e-002 +v 1.6732620e-002 1.0991230e-001 -1.7020040e-002 +v -3.0978770e-002 7.7020860e-002 -3.2816490e-002 +v -6.2359240e-002 1.7544824e-001 -6.1485990e-002 +v -1.7587870e-002 1.1491318e-001 -1.7205040e-002 +v -8.2354050e-002 8.0876320e-002 -2.4038900e-003 +v -7.8578910e-002 1.4050129e-001 -4.6031000e-003 +v -2.8931080e-002 7.9247620e-002 -3.5049800e-002 +v -3.1225710e-002 8.0413100e-002 -3.2182320e-002 +v -3.3258680e-002 7.9621670e-002 -2.7146060e-002 +v -4.4697400e-002 1.1791537e-001 -1.4725860e-002 +v -7.9723740e-002 8.4226660e-002 -8.7608600e-003 +v -8.5042160e-002 8.3817830e-002 -7.7640000e-005 +v -8.6776400e-002 8.4344860e-002 1.2419030e-002 +v -8.6674670e-002 8.2665010e-002 1.5174340e-002 +v -8.5106250e-002 8.5176580e-002 2.5679440e-002 +v -7.6975760e-002 8.2935940e-002 -1.1450630e-002 +v -8.2776390e-002 8.3430890e-002 -4.3687000e-003 +v -8.6180440e-002 8.2572150e-002 6.3639000e-003 +v -9.1160820e-002 1.4144362e-001 1.5673910e-002 +v -7.4638800e-002 1.4398484e-001 -7.1504600e-003 +v -8.3448500e-002 1.3393299e-001 -1.6873200e-003 +v -7.5804700e-002 1.5134475e-001 -1.9881200e-002 +v -7.4924140e-002 1.5273013e-001 -1.9397440e-002 +v -5.2314440e-002 1.2159646e-001 -1.0798060e-002 +v -3.0734050e-002 8.5427560e-002 -3.0506670e-002 +v -3.2590560e-002 8.1942660e-002 -2.9100210e-002 +v -8.6454830e-002 8.6940490e-002 9.1667000e-004 +v -1.2501820e-002 1.0634409e-001 -2.2360190e-002 +v -8.8585880e-002 1.4605869e-001 9.8780000e-003 +v -8.5609750e-002 1.4712513e-001 6.5981100e-003 +v -8.7511210e-002 1.5061504e-001 1.0152460e-002 +v -6.0113540e-002 3.5550440e-002 4.4907580e-002 +v -8.8284200e-002 8.6869110e-002 8.1029200e-003 +v -8.8812560e-002 8.7765490e-002 1.4226540e-002 +v -8.8001070e-002 8.6626430e-002 1.5466680e-002 +v -8.6991110e-002 8.6444700e-002 2.2420950e-002 +v -7.4609990e-002 1.4727815e-001 -1.4172380e-002 +v -3.4707910e-002 8.4035880e-002 -2.4302260e-002 +v -8.4964900e-002 8.9962540e-002 -3.0068000e-003 +v -8.8091450e-002 8.7741580e-002 4.8489900e-003 +v -9.1490470e-002 1.4543178e-001 2.2277220e-002 +v -9.4380420e-002 1.2183919e-001 1.7904340e-002 +v -2.9164530e-002 8.5393440e-002 -3.3666780e-002 +v -3.0557790e-002 8.8625920e-002 -2.7550670e-002 +v -7.7770550e-002 8.7844840e-002 -1.1694810e-002 +v -8.0728260e-002 8.8204150e-002 -7.8003100e-003 +v -8.3272540e-002 8.9476690e-002 -5.6502900e-003 +v -8.9398710e-002 8.9539000e-002 1.1645550e-002 +v -8.9698390e-002 1.3971257e-001 1.3774760e-002 +v -7.7134890e-002 1.5151225e-001 -5.5823000e-003 +v -5.1121410e-002 1.6374125e-001 -2.6640500e-003 +v -8.6442960e-002 1.2767438e-001 -1.4864100e-003 +v -6.9605590e-002 1.5490763e-001 -5.0188670e-002 +v -8.7265180e-002 9.2110030e-002 4.2059000e-003 +v -8.9086250e-002 9.2377120e-002 1.0569860e-002 +v -8.9612340e-002 9.1599880e-002 1.7812280e-002 +v -8.2732460e-002 1.4196856e-001 1.2529100e-003 +v -7.2618370e-002 1.4368135e-001 -1.0987100e-002 +v -7.7677230e-002 1.6610992e-001 -3.6777320e-002 +v -1.5078060e-002 9.3863440e-002 -3.4317310e-002 +v -7.1057280e-002 1.5476885e-001 -4.5778530e-002 +v -9.2331920e-002 1.2523886e-001 9.1589500e-003 +v -7.6046700e-002 9.1037250e-002 -1.3643150e-002 +v -8.2942810e-002 9.3291700e-002 -6.1856300e-003 +v -1.0411170e-002 9.4592340e-002 -3.3784850e-002 +v -2.9331140e-002 1.1476230e-001 -1.5844640e-002 +v -3.7218250e-002 1.1594244e-001 -1.5173050e-002 +v -1.2429920e-002 1.0286006e-001 -2.3822480e-002 +v 6.6509600e-003 8.8144500e-002 -3.2945810e-002 +v -6.4119900e-003 9.2876210e-002 -3.4817640e-002 +v 1.5800150e-002 1.1996558e-001 -1.1415630e-002 +v 2.9102740e-002 1.0247506e-001 -1.5768380e-002 +v 4.2080690e-002 6.3480630e-002 -2.5405300e-003 +v 2.8723120e-002 9.7943220e-002 -1.7497350e-002 +v -1.9987640e-002 1.0278313e-001 -2.3392920e-002 +v 3.3748350e-002 8.3644140e-002 -1.8630450e-002 +v -1.8685680e-002 1.8689625e-001 -2.0248700e-002 +v 6.4154900e-003 1.1790181e-001 -1.6282740e-002 +v 5.6305210e-002 6.7769910e-002 2.6525000e-003 +v -5.3608300e-003 1.1289400e-001 -1.9613290e-002 +v 4.5769430e-002 6.4628800e-002 -1.2166100e-003 +v -1.0090870e-002 9.8229650e-002 -2.7731360e-002 +v -6.0458520e-002 1.1755645e-001 -1.1354580e-002 +v 1.2933940e-002 1.1887250e-001 -1.3979370e-002 +v 1.5235680e-002 9.4977900e-002 -2.4437140e-002 +v -3.0892950e-002 4.7409030e-002 -2.4954000e-002 +v -1.7766190e-002 1.8572344e-001 -2.3049280e-002 +v -1.3034890e-002 1.1002855e-001 -2.0161170e-002 +v -7.1206550e-002 3.8608570e-002 7.7218000e-004 +v 1.7904800e-002 1.0627709e-001 -1.7729250e-002 +v -3.3623490e-002 1.1840428e-001 -1.1927480e-002 +v -4.9906840e-002 1.1788332e-001 -1.4402480e-002 +v -6.6878100e-003 1.1747209e-001 -1.5359280e-002 +v -1.5451470e-002 1.8597600e-001 -2.4795870e-002 +v -3.0603900e-002 3.8038460e-002 -3.0123840e-002 +v -1.3220270e-002 1.8397188e-001 -2.7519460e-002 +v -4.7859450e-002 1.1162729e-001 -1.7482120e-002 +v -1.3098990e-002 9.0776040e-002 -3.6659270e-002 +v -6.3117340e-002 1.5425437e-001 2.9730400e-003 +v -5.5139750e-002 1.1051601e-001 -1.7672740e-002 +v -1.1096770e-002 1.8202324e-001 -2.8042450e-002 +v -2.6568900e-002 3.4695830e-002 -2.9113750e-002 +v -6.6396600e-003 1.0222209e-001 -2.3519320e-002 +v -5.6996400e-002 1.5741713e-001 6.0244000e-004 +v 1.9076550e-002 9.1870620e-002 -2.4890230e-002 +v 1.3473090e-002 1.2429893e-001 -6.8361400e-003 +v -2.1730490e-002 9.8410960e-002 -2.4306850e-002 +v -1.7142170e-002 9.8057460e-002 -2.4924330e-002 +v -5.8698110e-002 1.5137318e-001 -6.5801000e-004 +v 3.5641100e-003 1.2764883e-001 -4.4672400e-003 +v -8.5369800e-003 9.9921220e-002 -2.4351070e-002 +v -1.2171980e-002 1.8125102e-001 -2.9061170e-002 +v -6.1113980e-002 1.5305212e-001 9.9983000e-004 +v -2.9570620e-002 1.1713871e-001 -1.3675530e-002 +v 3.0530110e-002 1.1221207e-001 -8.1860600e-003 +v -3.1714100e-002 3.5111530e-002 -3.0658990e-002 +v -1.3691130e-002 1.7914707e-001 -2.8126410e-002 +v 1.1620840e-002 1.1548972e-001 -1.6385680e-002 +v -6.1993570e-002 1.5028063e-001 -1.6297100e-003 +v 3.6684020e-002 1.0099570e-001 -9.8485900e-003 +v 4.8512670e-002 7.1798180e-002 6.0005000e-003 +v -4.6583000e-004 1.1983662e-001 -1.3610580e-002 +v 1.6747170e-002 9.0113950e-002 -2.7127190e-002 +v 6.9832400e-003 9.7730080e-002 -2.4800310e-002 +v -4.3226830e-002 4.6263570e-002 -1.1771730e-002 +v -8.3562500e-003 1.1373600e-001 -1.8239810e-002 +v -1.2354410e-002 1.1556773e-001 -1.6486930e-002 +v 4.6834470e-002 7.4354100e-002 1.0139500e-002 +v 2.5319170e-002 1.0931725e-001 -1.3579660e-002 +v -4.2459500e-002 1.1392482e-001 -1.6188050e-002 +v 5.7744640e-002 6.4158440e-002 2.6277600e-003 +v -5.9710530e-002 3.6535780e-002 -9.4949000e-003 +v -3.2078400e-003 1.0962100e-001 -2.1523850e-002 +v 2.7020740e-002 6.1345700e-002 -2.2292060e-002 +v 7.1030200e-003 1.0191162e-001 -2.1230990e-002 +v -3.8225680e-002 1.2465525e-001 -7.3257400e-003 +v 2.5941540e-002 1.1576352e-001 -8.2193900e-003 +v -6.1297960e-002 3.3900220e-002 -9.3216600e-003 +v -5.9466670e-002 1.4743956e-001 -1.8885400e-003 +v 1.0506610e-002 1.0087700e-001 -2.2109510e-002 +v 3.3081340e-002 1.0273382e-001 -1.2787210e-002 +v 1.2517840e-002 1.0475378e-001 -1.9915960e-002 +v 2.3087990e-002 9.3998720e-002 -2.2210680e-002 +v 3.1555430e-002 9.2484730e-002 -1.8204280e-002 +v 6.2723100e-003 9.9910370e-002 -2.2296890e-002 +v -4.0917240e-002 4.6121780e-002 -1.7942580e-002 +v 3.5407360e-002 9.8188850e-002 -1.2008970e-002 +v 9.4135900e-003 1.2121902e-001 -1.2937780e-002 +v 5.3735190e-002 7.2027350e-002 6.8010000e-003 +v 2.5620340e-002 1.1880719e-001 -5.0330800e-003 +v -3.8150260e-002 4.2466610e-002 -2.6893990e-002 +v -2.8212410e-002 1.1116862e-001 -1.8001930e-002 +v -6.0253590e-002 1.4339100e-001 -3.7906300e-003 +v 1.9016880e-002 1.0401450e-001 -1.9333120e-002 +v 7.5446700e-003 9.1682150e-002 -3.1643140e-002 +v -7.0760800e-003 1.2240119e-001 -1.1364410e-002 +v -1.9047500e-002 9.6562130e-002 -2.7579900e-002 +v -1.6953390e-002 1.0669256e-001 -2.2002990e-002 +v -6.7307000e-004 1.0119875e-001 -2.2857770e-002 +v -9.0179300e-003 1.2528031e-001 -7.7912000e-003 +v -6.8136180e-002 1.8006113e-001 -5.8816050e-002 +v -2.3600190e-002 1.1513818e-001 -1.5577390e-002 +v -5.9831220e-002 4.2842260e-002 -6.6469100e-003 +v 5.3124070e-002 5.9012380e-002 -2.8853800e-003 +v -3.6931840e-002 3.7107370e-002 -2.9714170e-002 +v -5.6215140e-002 1.4139213e-001 -2.8027300e-003 +v 3.6695880e-002 1.0372844e-001 -7.9621500e-003 +v -3.5885070e-002 1.2040038e-001 -1.0640470e-002 +v -9.3569500e-003 8.5423730e-002 -3.8112540e-002 +v -6.0127340e-002 1.2041391e-001 -9.3791100e-003 +v -3.9842790e-002 1.2156113e-001 -1.1570310e-002 +v 2.8322200e-002 1.0847957e-001 -1.2623390e-002 +v -1.8733500e-003 1.1593910e-001 -1.7169430e-002 +v 3.8648150e-002 9.0153340e-002 -1.2549680e-002 +v -1.7359200e-003 9.2244170e-002 -3.4310460e-002 +v 5.0000820e-002 6.1612070e-002 -3.4649900e-003 +v 5.5858960e-002 6.2910170e-002 6.9037000e-004 +v 2.0461520e-002 1.1515372e-001 -1.3103780e-002 +v -1.5165840e-002 1.1798075e-001 -1.4465520e-002 +v -7.0859540e-002 7.1510150e-002 3.3895100e-002 +v 2.2674030e-002 8.6606050e-002 -2.4925490e-002 +v 3.5358840e-002 8.7438890e-002 -1.7109050e-002 +v 1.8400920e-002 1.2145507e-001 -7.6804200e-003 +v -2.5425900e-002 4.1421010e-002 -2.9204830e-002 +v -8.2085100e-003 9.6777440e-002 -3.0809780e-002 +v -5.6810660e-002 3.3873940e-002 -1.1166310e-002 +v -3.4588640e-002 4.4744960e-002 -2.7122900e-002 +v -4.0251680e-002 1.1827531e-001 -1.3674080e-002 +v 1.6387020e-002 1.1402346e-001 -1.5496900e-002 +v 4.2635280e-002 6.0797460e-002 -3.4583700e-003 +v -5.0687200e-002 3.5935870e-002 -1.2380790e-002 +v 7.3446800e-003 9.4509570e-002 -2.9683220e-002 +v -1.9706700e-002 9.2917340e-002 -3.4636880e-002 +v -1.2083040e-002 1.2219229e-001 -9.7120900e-003 +v 4.8805930e-002 6.8457810e-002 1.6952900e-003 +v -3.0869700e-003 9.8402500e-002 -2.7403170e-002 +v -5.3198790e-002 1.3672896e-001 -1.6580500e-003 +v -4.7290060e-002 1.3055355e-001 1.6909100e-003 +v 4.4651700e-003 1.2044039e-001 -1.3931400e-002 +v -2.3850100e-003 1.2290534e-001 -1.0382460e-002 +v -2.4833330e-002 9.5858030e-002 -2.5162110e-002 +v -4.2296900e-002 3.6291920e-002 -2.7253600e-002 +v -5.4388260e-002 1.3404922e-001 -3.9920400e-003 +v -5.0539380e-002 1.3336659e-001 -1.0872200e-003 +v 2.6040300e-003 9.6942660e-002 -2.8407060e-002 +v -7.8163100e-003 1.2821209e-001 -1.9430400e-003 +v 6.5111700e-003 1.3002517e-001 9.2881000e-004 +v 3.4742860e-002 9.2274140e-002 -1.5654590e-002 +v -6.7787700e-002 1.8088887e-001 -5.8191050e-002 +v -3.3715410e-002 1.1151566e-001 -1.8078440e-002 +v 4.4630400e-003 1.2427294e-001 -9.4291400e-003 +v -2.3370170e-002 9.3392760e-002 -3.2031820e-002 +v -4.8982070e-002 1.2980647e-001 -1.3229400e-003 +v -7.8164000e-004 1.2822918e-001 -3.2490000e-003 +v 2.4960400e-003 8.9857600e-002 -3.3628450e-002 +v 7.4553300e-003 1.1196790e-001 -1.9554260e-002 +v 2.8791140e-002 9.1157340e-002 -2.0370210e-002 +v -5.3590150e-002 1.2437450e-001 -7.3470400e-003 +v -4.7743630e-002 1.2064432e-001 -1.2812990e-002 +v -1.9616230e-002 1.2109197e-001 -9.5487700e-003 +v -6.5047370e-002 1.7999148e-001 -5.9758600e-002 +v -5.1704160e-002 3.7620360e-002 -1.1763450e-002 +v -5.2124270e-002 1.2929832e-001 -4.1187000e-003 +v -4.5334450e-002 1.2891494e-001 1.5819100e-003 +v -3.0471200e-003 1.2919453e-001 -1.0688000e-003 +v 7.2129600e-003 1.2721957e-001 -5.2073700e-003 +v 1.1669320e-002 1.2720154e-001 -3.1850900e-003 +v 5.3056400e-002 6.9708830e-002 3.1291400e-003 +v -6.3021150e-002 1.7810951e-001 -6.0393570e-002 +v 2.8204800e-002 6.4391270e-002 -2.0698040e-002 +v 3.4400180e-002 1.0503000e-001 -1.0224920e-002 +v 3.0975190e-002 1.0790250e-001 -1.1058430e-002 +v -4.8984390e-002 1.1480518e-001 -1.5966690e-002 +v -3.2821710e-002 1.2300500e-001 -5.9088300e-003 +v -5.0792860e-002 1.2716487e-001 -4.8183200e-003 +v -3.5301670e-002 1.2547815e-001 -3.1542800e-003 +v 5.6455250e-002 6.9951490e-002 4.9191700e-003 +v -1.6240450e-002 1.2512177e-001 -3.6499700e-003 +v -1.6970400e-002 1.1119793e-001 -1.9586410e-002 +v -5.4088120e-002 3.9781210e-002 -1.0544680e-002 +v -3.4190490e-002 4.7514010e-002 -2.2301500e-002 +v 1.3699090e-002 9.3914220e-002 -2.6427690e-002 +v 8.8000000e-004 9.9234930e-002 -2.4355670e-002 +v -4.6459460e-002 1.2723953e-001 -4.8843300e-003 +v -4.1735500e-002 1.2687599e-001 -4.1742000e-003 +v -2.1000480e-002 1.2313643e-001 -6.1190100e-003 +v -1.2130450e-002 1.2572568e-001 -5.2007900e-003 +v -4.3822400e-003 1.2640753e-001 -6.9495200e-003 +v 1.4085700e-003 3.4781990e-002 -2.3265200e-002 +v -1.4846200e-002 3.5070930e-002 -2.6071900e-002 +v -2.1399500e-002 3.4795120e-002 -2.7958820e-002 +v 1.2009220e-002 3.5961900e-002 -2.1735750e-002 +v 3.8249200e-003 3.6129220e-002 -2.3878090e-002 +v -5.1139560e-002 9.6617580e-002 -2.2095120e-002 +v -5.4813320e-002 9.8102480e-002 -2.1425370e-002 +v -2.7597040e-002 1.6979824e-001 -1.8170420e-002 +v 1.3359870e-002 3.9377410e-002 -2.2496330e-002 +v 4.3919300e-003 3.8674430e-002 -2.4170290e-002 +v -6.8478200e-003 3.6444540e-002 -2.5177120e-002 +v -1.3280260e-002 3.7699590e-002 -2.6391810e-002 +v -4.7672760e-002 3.6116650e-002 -1.3301210e-002 +v -4.5590120e-002 1.0853826e-001 -1.8796680e-002 +v -5.0095670e-002 1.0990925e-001 -1.8504510e-002 +v -6.5766640e-002 3.6469550e-002 -7.2073000e-003 +v -2.3455840e-002 1.6824727e-001 -1.8822880e-002 +v -4.5918000e-003 3.8404570e-002 -2.5412870e-002 +v -2.4954130e-002 3.7441060e-002 -2.9152720e-002 +v 2.9007770e-002 3.7358220e-002 -2.7474000e-004 +v -7.9468800e-003 4.1489920e-002 -2.5911270e-002 +v -1.6803800e-002 3.9753810e-002 -2.7565350e-002 +v -6.5156150e-002 1.4034537e-001 -7.6848600e-003 +v -4.7080100e-002 4.0700690e-002 -1.1869830e-002 +v -6.8470630e-002 3.7477700e-002 -4.9557400e-003 +v 3.7326850e-002 4.0209510e-002 -8.5850000e-004 +v 3.5349870e-002 4.1257050e-002 -2.8075100e-003 +v 5.1820700e-003 4.1536320e-002 -2.4065670e-002 +v 1.8660660e-002 1.0030784e-001 -2.2127290e-002 +v -6.0510780e-002 1.0748450e-001 -1.7042300e-002 +v -6.2374340e-002 4.0146090e-002 -7.4040200e-003 +v 2.5456950e-002 3.9483890e-002 -4.0251400e-003 +v -2.2828000e-004 4.3394940e-002 -2.5124420e-002 +v -8.1088400e-003 4.3439060e-002 -2.6140070e-002 +v -1.7362450e-002 4.3237420e-002 -2.7665190e-002 +v -2.6416670e-002 4.4674020e-002 -2.8209740e-002 +v 3.8064500e-003 1.0944331e-001 -2.0203790e-002 +v -5.8232370e-002 9.5690400e-002 -2.0616030e-002 +v -6.6122370e-002 4.2341260e-002 -2.7538800e-003 +v -6.0959920e-002 9.4173040e-002 -1.9015670e-002 +v 3.1352250e-002 4.2649280e-002 -4.6745000e-003 +v -3.3540900e-002 3.6342620e-002 4.9089960e-002 +v 1.7252780e-002 4.4335610e-002 -2.3067190e-002 +v 1.0637660e-002 4.4161560e-002 -2.4926170e-002 +v 4.3843100e-003 4.5806710e-002 -2.6788990e-002 +v -8.2506400e-003 4.5148720e-002 -2.8441070e-002 +v -1.5748410e-002 4.5043860e-002 -2.7877790e-002 +v 2.8990330e-002 4.4697850e-002 -6.1863000e-003 +v 8.1686400e-003 4.5053030e-002 -2.5178740e-002 +v -9.6291000e-004 4.5378230e-002 -2.7308280e-002 +v -1.7033400e-003 4.7819200e-002 -2.9928930e-002 +v -3.1535830e-002 4.4740410e-002 -2.8079410e-002 +v -3.3619650e-002 1.5691468e-001 -1.1024870e-002 +v -5.0751180e-002 4.3109620e-002 -1.0018680e-002 +v 3.6890890e-002 4.7353200e-002 -6.1057100e-003 +v 2.4975630e-002 4.2644580e-002 -7.0169900e-003 +v 2.4562420e-002 4.8369560e-002 -1.9672760e-002 +v 1.3964040e-002 4.5579170e-002 -2.4706510e-002 +v 1.3376130e-002 4.8630300e-002 -2.6551500e-002 +v 3.7308900e-003 4.8127990e-002 -2.9025970e-002 +v -8.7947000e-003 4.7056850e-002 -2.9881630e-002 +v -1.3753770e-002 5.1865060e-002 -3.2243480e-002 +v -2.1200840e-002 4.6657090e-002 -2.7951320e-002 +v 3.9693540e-002 4.5658580e-002 -4.5274100e-003 +v 3.3627400e-002 4.8717730e-002 -6.3904600e-003 +v -6.5352120e-002 9.9294570e-002 -1.6820150e-002 +v 1.2868100e-003 5.0383670e-002 -3.0357440e-002 +v -8.1797500e-003 4.9845800e-002 -3.1071390e-002 +v -1.7184350e-002 4.8210500e-002 -2.9741930e-002 +v -2.6049450e-002 4.7692500e-002 -2.6149500e-002 +v -8.4747010e-002 1.1078350e-001 3.9488380e-002 +v -5.1316870e-002 4.8270690e-002 -7.9310500e-003 +v -8.2506510e-002 1.2765487e-001 -4.6796400e-003 +v 3.8663690e-002 5.1696670e-002 -6.6910200e-003 +v -7.5643160e-002 9.9440450e-002 -1.1927610e-002 +v 2.0284470e-002 5.1349190e-002 -2.4895380e-002 +v 5.9436000e-003 5.0976660e-002 -2.9119360e-002 +v -2.5528290e-002 5.1472710e-002 -2.6884680e-002 +v -3.5562670e-002 4.9399890e-002 -1.2865040e-002 +v -4.2818980e-002 1.6220182e-001 -1.0337510e-002 +v -6.5593600e-002 1.7665711e-001 -6.0504730e-002 +v -3.4151080e-002 1.7442797e-001 -1.3312550e-002 +v 4.3673180e-002 5.0162230e-002 -5.9843500e-003 +v -5.0342410e-002 1.5546197e-001 -5.1927700e-003 +v 2.5464180e-002 5.4029700e-002 -2.1691010e-002 +v 1.0149790e-002 4.9258540e-002 -2.7750590e-002 +v -2.2043190e-002 5.3612020e-002 -3.0135610e-002 +v -3.2875520e-002 5.1677630e-002 -1.0888650e-002 +v -3.7613820e-002 4.9534770e-002 -1.1626140e-002 +v -4.0750630e-002 4.9285110e-002 -1.1286200e-002 +v -4.6385170e-002 4.7490850e-002 -1.0085980e-002 +v 4.4473170e-002 5.3293010e-002 -6.3327900e-003 +v 3.3205620e-002 5.1020650e-002 -7.2382500e-003 +v 1.5678350e-002 5.1169270e-002 -2.6397810e-002 +v 6.8341700e-003 5.5010170e-002 -3.0561130e-002 +v 2.1424700e-003 5.5502800e-002 -3.1334400e-002 +v 5.9285000e-004 5.2867950e-002 -3.0513830e-002 +v -3.6481400e-003 5.1869000e-002 -3.1457940e-002 +v -9.4245600e-003 5.5399220e-002 -3.3653980e-002 +v -1.9302150e-002 5.8224770e-002 -3.3919700e-002 +v -6.1084270e-002 1.3386190e-001 -7.2248900e-003 +v -4.3309760e-002 5.5656840e-002 -1.1402110e-002 +v -6.1080540e-002 1.6833773e-001 -5.9192060e-002 +v 4.7574690e-002 5.2943630e-002 -5.1300300e-003 +v -3.7403030e-002 1.1150775e-001 -1.8243310e-002 +v 1.9972490e-002 5.4409710e-002 -2.7108230e-002 +v 5.3974800e-003 5.8382570e-002 -3.0903760e-002 +v -1.0603590e-002 5.3602910e-002 -3.3403350e-002 +v -3.4998290e-002 5.2331560e-002 -1.0347380e-002 +v -4.6471230e-002 5.1304340e-002 -9.8299800e-003 +v -6.7945360e-002 1.1493603e-001 -9.5107300e-003 +v -7.1048210e-002 1.5161088e-001 -4.4679270e-002 +v -5.8903800e-003 3.4790620e-002 -2.4224470e-002 +v 1.6842140e-002 5.5555670e-002 -2.8284560e-002 +v 1.0711040e-002 5.4687610e-002 -2.9767520e-002 +v -1.1826800e-003 5.9492420e-002 -3.3360920e-002 +v -5.2325900e-003 5.5688960e-002 -3.2840220e-002 +v -5.1705830e-002 5.2470760e-002 -7.4047200e-003 +v -5.2626360e-002 6.0043760e-002 -8.9566900e-003 +v -7.2598590e-002 9.7762720e-002 -1.4434510e-002 +v 4.4331260e-002 5.5818010e-002 -6.0362700e-003 +v 3.8463400e-002 5.4934820e-002 -6.1822500e-003 +v 3.8838620e-002 5.7808260e-002 -5.2584800e-003 +v -9.2015400e-003 5.9510130e-002 -3.4437110e-002 +v -3.5262560e-002 5.5284900e-002 -1.0545060e-002 +v -3.8336450e-002 5.4503540e-002 -1.0905320e-002 +v -1.7727540e-002 3.6289540e-002 5.2222250e-002 +v 5.0006490e-002 5.8095800e-002 -4.6211800e-003 +v 4.6133970e-002 5.9278810e-002 -4.7769600e-003 +v 1.5110300e-002 5.9819840e-002 -2.8645750e-002 +v 1.0312380e-002 5.7586530e-002 -2.9995250e-002 +v -6.1353400e-003 6.0256790e-002 -3.4695830e-002 +v -1.2318220e-002 5.9396390e-002 -3.5268510e-002 +v -1.4466910e-002 6.3136020e-002 -3.6865870e-002 +v -4.6650260e-002 5.9840950e-002 -1.2135840e-002 +v -5.6572080e-002 1.2480275e-001 -7.1885700e-003 +v -7.9237500e-002 1.2055419e-001 -5.6744800e-003 +v -7.9334790e-002 1.2560650e-001 -6.1175900e-003 +v 2.2340000e-002 5.8492230e-002 -2.6014120e-002 +v 7.6270400e-003 6.2098330e-002 -3.1135840e-002 +v 3.3101700e-003 6.0456840e-002 -3.2481070e-002 +v -1.6811880e-002 6.1275230e-002 -3.5929330e-002 +v -3.2491910e-002 5.7196350e-002 -1.2104730e-002 +v -3.4108240e-002 6.1466560e-002 -1.3053130e-002 +v -3.3896980e-002 5.7025330e-002 -1.1047570e-002 +v -3.8623580e-002 5.8303290e-002 -1.1505750e-002 +v -4.5008400e-002 6.2723940e-002 -1.3390450e-002 +v -5.6896010e-002 1.3398739e-001 -5.6270700e-003 +v -4.4853890e-002 1.5746031e-001 -8.6731600e-003 +v -7.8609550e-002 6.9656870e-002 1.1810740e-002 +v -2.3730020e-002 1.0186156e-001 -2.3836400e-002 +v -2.8122930e-002 9.9322390e-002 -2.3580130e-002 +v -5.0076720e-002 1.4997652e-001 -3.6419700e-003 +v -3.3048420e-002 9.5958590e-002 -2.3426460e-002 +v 1.9520390e-002 6.2064770e-002 -2.7292470e-002 +v -3.8864710e-002 1.0333987e-001 -2.0641400e-002 +v -4.8952940e-002 5.6281090e-002 -1.0220880e-002 +v -5.3993040e-002 1.4498656e-001 -1.1093400e-003 +v -4.5530560e-002 9.8510850e-002 -2.1729510e-002 +v -5.0910960e-002 1.0074570e-001 -2.1619430e-002 +v 2.3245830e-002 6.2792530e-002 -2.5047990e-002 +v 9.7412800e-003 6.3181400e-002 -3.1141370e-002 +v -8.6614000e-004 6.4559630e-002 -3.4490930e-002 +v -8.5264000e-003 6.4001730e-002 -3.5850480e-002 +v -4.8451500e-002 6.4794120e-002 -1.3029910e-002 +v -5.2325160e-002 1.0614813e-001 -1.9271240e-002 +v -5.5265350e-002 1.0216682e-001 -1.9897100e-002 +v -5.9042010e-002 9.9032210e-002 -1.9222950e-002 +v -5.7846760e-002 1.0433496e-001 -1.8525740e-002 +v -2.7113460e-002 1.7332156e-001 -1.8538890e-002 +v 2.2832000e-002 6.7082570e-002 -2.6297510e-002 +v 1.4519060e-002 6.4595540e-002 -2.9855690e-002 +v 1.1471330e-002 6.7581440e-002 -3.0901170e-002 +v -1.7739360e-002 6.6260830e-002 -3.7657310e-002 +v -6.5059750e-002 1.3452104e-001 -8.0899900e-003 +v -7.5829320e-002 1.4244605e-001 -5.8090000e-003 +v -4.1362350e-002 6.1637330e-002 -1.2813770e-002 +v -5.6147890e-002 6.1921550e-002 -5.7541100e-003 +v -6.2126110e-002 6.2845360e-002 -4.5202600e-003 +v -3.7292480e-002 1.6449057e-001 -1.3627050e-002 +v -1.9818920e-002 1.6509494e-001 -1.7608980e-002 +v 6.2881100e-003 6.5416350e-002 -3.2563040e-002 +v -5.9250500e-003 6.9515630e-002 -3.5933480e-002 +v -1.0538630e-002 6.7999180e-002 -3.6517060e-002 +v -3.5385700e-002 6.6817430e-002 -1.5434860e-002 +v -5.3994500e-002 6.4638700e-002 -9.3254900e-003 +v -6.3852310e-002 6.5572310e-002 -6.9393300e-003 +v -6.3920880e-002 1.2774242e-001 -8.5494600e-003 +v -2.6940700e-002 3.6184050e-002 5.3351850e-002 +v 1.9618650e-002 6.7007390e-002 -2.8356120e-002 +v 1.2275180e-002 6.9933940e-002 -3.1553160e-002 +v 5.4265100e-003 6.8247960e-002 -3.2730520e-002 +v -4.4084200e-003 6.6619200e-002 -3.4870250e-002 +v -2.1911350e-002 6.7144790e-002 -3.6535750e-002 +v -4.5643150e-002 1.5466949e-001 -7.2969400e-003 +v -5.1673460e-002 6.6850660e-002 -1.2120350e-002 +v -5.8105180e-002 6.6465950e-002 -1.0044340e-002 +v -5.6992260e-002 1.4311862e-001 -2.2403000e-003 +v -8.0651110e-002 1.3119854e-001 -4.4397800e-003 +v -5.6544310e-002 1.2850938e-001 -6.2014700e-003 +v 1.7758080e-002 7.0138540e-002 -2.9404680e-002 +v 6.4980500e-003 7.0791870e-002 -3.3525310e-002 +v 7.5831000e-004 7.0434460e-002 -3.4462560e-002 +v -1.3235950e-002 6.9292820e-002 -3.7917490e-002 +v -6.7390780e-002 1.1889688e-001 -8.7301400e-003 +v -3.8119520e-002 6.4162310e-002 -1.3829140e-002 +v 1.8527400e-003 1.1303356e-001 -1.9794270e-002 +v -7.5950810e-002 6.8170610e-002 1.8117970e-002 +v -1.0001990e-002 7.2671480e-002 -3.7661370e-002 +v -1.7976070e-002 7.0613770e-002 -3.8443880e-002 +v -2.3035990e-002 7.2778460e-002 -3.8072640e-002 +v -2.6120100e-002 7.1177480e-002 -3.5451530e-002 +v -6.8535420e-002 1.3929375e-001 -7.8046600e-003 +v -3.5263040e-002 7.1067650e-002 -1.8011860e-002 +v -4.1558180e-002 6.9774010e-002 -1.6774100e-002 +v -5.2831730e-002 7.0298920e-002 -1.4864960e-002 +v -6.6978850e-002 6.7638980e-002 -6.8094400e-003 +v -1.0244470e-002 1.7895826e-001 -2.9538870e-002 +v -7.5272650e-002 1.2680098e-001 -8.0241700e-003 +v -8.7359900e-002 1.1248315e-001 4.2049490e-002 +v 8.7503000e-003 7.4301560e-002 -3.3398210e-002 +v -6.4249520e-002 1.6045024e-001 -5.7041470e-002 +v -4.4354010e-002 7.3372220e-002 -1.7874430e-002 +v -4.5762580e-002 6.9445320e-002 -1.5928780e-002 +v -4.7957440e-002 7.2542990e-002 -1.6106990e-002 +v -5.7822630e-002 6.9538010e-002 -1.4416470e-002 +v -7.2071600e-002 7.1538150e-002 -7.4714400e-003 +v 2.5472930e-002 7.4094500e-002 -2.4938540e-002 +v 1.5719730e-002 7.3756350e-002 -2.9747770e-002 +v 4.8214000e-003 7.3763980e-002 -3.4552450e-002 +v -2.2528600e-003 7.3921320e-002 -3.5887190e-002 +v -7.3834900e-003 7.4799620e-002 -3.7223830e-002 +v -2.0225340e-002 7.7095190e-002 -3.9044290e-002 +v -3.4016180e-002 7.2101270e-002 -2.0823150e-002 +v -3.8493370e-002 7.2839870e-002 -1.7502230e-002 +v -6.4392550e-002 7.3116330e-002 -1.5335340e-002 +v -6.4480660e-002 7.0187350e-002 -1.2261750e-002 +v -2.3854330e-002 1.6164528e-001 -1.4504190e-002 +v 2.2104450e-002 7.2692600e-002 -2.6900140e-002 +v 1.5532370e-002 7.6586960e-002 -2.9606940e-002 +v 1.1574050e-002 7.4860570e-002 -3.1383860e-002 +v -1.4731560e-002 7.7640750e-002 -3.8490670e-002 +v -1.6018820e-002 7.4288800e-002 -3.8864420e-002 +v -5.1103620e-002 7.3071950e-002 -1.6243060e-002 +v -5.7989540e-002 7.4017880e-002 -1.7522320e-002 +v -6.9608380e-002 7.2322890e-002 -1.0934430e-002 +v -7.5996110e-002 1.1714132e-001 -6.5577200e-003 +v -3.7987660e-002 1.0751453e-001 -1.9975760e-002 +v 1.0696210e-002 7.9889200e-002 -3.2009580e-002 +v -5.3433400e-003 7.8264580e-002 -3.7476940e-002 +v -2.6081990e-002 7.6191290e-002 -3.6780200e-002 +v -3.9161040e-002 1.5718885e-001 -1.0580510e-002 +v -6.5609880e-002 7.5860010e-002 -1.6750060e-002 +v -7.0177600e-002 7.5663330e-002 -1.3839210e-002 +v -7.4291360e-002 7.4808360e-002 -9.3537900e-003 +v -6.3428890e-002 1.7185387e-001 -6.1412170e-002 +v 3.0684890e-002 7.5726870e-002 -2.0778090e-002 +v 1.9305010e-002 7.9017870e-002 -2.7743990e-002 +v -8.5992100e-003 7.9338730e-002 -3.7905180e-002 +v -2.3200110e-002 7.6568500e-002 -3.8386500e-002 +v -3.8117820e-002 7.6390120e-002 -1.8644360e-002 +v -4.4231130e-002 7.7664130e-002 -1.9026580e-002 +v -5.1025500e-002 7.5705070e-002 -1.8186900e-002 +v -7.0595130e-002 1.2994832e-001 -8.7629200e-003 +v 2.8147660e-002 7.8785370e-002 -2.2432450e-002 +v 7.6016000e-003 7.9435920e-002 -3.3714560e-002 +v 4.9502400e-003 7.8027250e-002 -3.4409750e-002 +v -1.5858350e-002 8.1165550e-002 -3.9185590e-002 +v -1.8502080e-002 8.3343870e-002 -3.9010720e-002 +v -7.9739350e-002 1.3606854e-001 -4.1482100e-003 +v -3.0980180e-002 1.6634656e-001 -1.6241160e-002 +v -3.5749800e-002 7.7248350e-002 -1.9374020e-002 +v -4.8944740e-002 7.9086360e-002 -1.9575700e-002 +v -5.5065860e-002 7.8089190e-002 -1.9755480e-002 +v 2.3706000e-002 8.0240410e-002 -2.5450120e-002 +v 1.2254110e-002 8.3456700e-002 -3.0771580e-002 +v 1.8549900e-003 8.4692790e-002 -3.4838500e-002 +v -2.0857000e-004 7.8941410e-002 -3.5782080e-002 +v -4.2710000e-004 8.2947370e-002 -3.6380660e-002 +v -4.4101600e-003 8.2794510e-002 -3.7467250e-002 +v -3.3202320e-002 1.0578320e-001 -2.0647590e-002 +v -3.9206970e-002 8.1536380e-002 -2.0571000e-002 +v -6.0355410e-002 7.9766610e-002 -1.9375540e-002 +v -4.1771830e-002 1.0396706e-001 -2.0832940e-002 +v -1.1204010e-002 8.2713320e-002 -3.8489610e-002 +v -2.3181500e-002 8.1686990e-002 -3.8329160e-002 +v -2.7233190e-002 8.0570950e-002 -3.6620670e-002 +v -3.5470180e-002 8.0196070e-002 -2.2325910e-002 +v -4.4864210e-002 8.1997900e-002 -2.0473520e-002 +v -5.0647890e-002 8.2309430e-002 -2.1365890e-002 +v -5.5522610e-002 8.1927600e-002 -2.1353790e-002 +v -8.8089610e-002 1.1135484e-001 1.8516150e-002 +v -7.2036080e-002 1.1107918e-001 4.5361400e-002 +v -3.3359780e-002 1.6986395e-001 -1.5448990e-002 +v -6.6839030e-002 6.2170510e-002 2.1576840e-002 +v 3.0730560e-002 8.1968990e-002 -2.0040460e-002 +v 1.6224320e-002 8.6480380e-002 -2.8952010e-002 +v -6.9855630e-002 1.0027892e-001 -1.4847830e-002 +v -6.3836170e-002 8.1704600e-002 -1.8908860e-002 +v -6.7914820e-002 8.0136290e-002 -1.7128200e-002 +v -4.5752080e-002 1.6340754e-001 -8.1780500e-003 +v 1.1727540e-002 8.8010780e-002 -3.0860110e-002 +v 7.3334800e-003 8.5270000e-002 -3.2829380e-002 +v -3.4356500e-003 8.7017890e-002 -3.6461000e-002 +v -2.6964110e-002 8.4512810e-002 -3.6361740e-002 +v -3.6553370e-002 8.5316190e-002 -2.2576200e-002 +v -3.8791090e-002 8.5232710e-002 -2.1917600e-002 +v -5.7676940e-002 8.6258340e-002 -2.1098320e-002 +v -6.2581810e-002 8.6394530e-002 -1.9169290e-002 +v -7.1395340e-002 1.2468846e-001 -8.5944200e-003 +v 1.4801570e-002 9.9040900e-002 -2.2842920e-002 +v -2.1162860e-002 1.7491852e-001 -2.1977110e-002 +v -1.4824250e-002 8.7288840e-002 -3.8317070e-002 +v -2.3285750e-002 8.9468030e-002 -3.6027250e-002 +v -5.1595650e-002 8.4422070e-002 -2.1600960e-002 +v -6.9481040e-002 8.5656460e-002 -1.7198420e-002 +v -7.0917210e-002 1.0754846e-001 -1.1496630e-002 +v 3.0145320e-002 8.6284000e-002 -2.0408140e-002 +v -5.5578110e-002 1.1567692e-001 -1.4645990e-002 +v -8.0981100e-003 8.9070080e-002 -3.6552200e-002 +v -8.1206310e-002 1.1205088e-001 -8.8299000e-004 +v -1.8772170e-002 8.9838040e-002 -3.6991710e-002 +v -2.1100420e-002 8.6587670e-002 -3.7849050e-002 +v -2.5809910e-002 8.8889590e-002 -3.5082250e-002 +v -4.8984800e-002 9.0731760e-002 -2.1817170e-002 +v -3.5874870e-002 3.4776000e-002 -3.0845200e-002 +v -3.3164390e-002 3.3606540e-002 -2.9721880e-002 +v -2.5964020e-002 3.3487000e-002 -2.6321120e-002 +v -1.6717530e-002 3.3611640e-002 -2.4625420e-002 +v -5.3486300e-003 3.3829010e-002 -2.2600430e-002 +v 6.4843500e-003 3.4293000e-002 -2.0854930e-002 +v 1.3950350e-002 3.4880000e-002 -1.8612870e-002 +v -4.2465980e-002 3.4189100e-002 -2.7260650e-002 +v -3.3241100e-002 3.3578760e-002 -2.6719450e-002 +v 6.2813500e-003 3.4165800e-002 -1.8764230e-002 +v -4.4265790e-002 3.3663660e-002 -2.1914420e-002 +v -2.3671460e-002 3.3630970e-002 -2.3217760e-002 +v -1.1558580e-002 3.3895430e-002 -2.1054260e-002 +v -2.0406400e-003 3.4053940e-002 -1.9331070e-002 +v 1.7323900e-003 3.4459660e-002 -1.6607870e-002 +v -2.7316070e-002 3.3910070e-002 -2.1353750e-002 +v -1.3371080e-002 3.4361580e-002 -1.9023720e-002 +v 9.5887300e-003 3.4207220e-002 -1.5424050e-002 +v -1.4981540e-002 3.5878180e-002 -1.7992380e-002 +v -2.3474300e-003 3.5903130e-002 -1.5929740e-002 +v 2.2544300e-003 3.6411540e-002 -1.4783970e-002 +v -3.5199130e-002 3.3835210e-002 -2.0508290e-002 +v -2.6075450e-002 3.5918600e-002 -1.9405170e-002 +v 8.2740600e-003 3.5645200e-002 -1.2648700e-002 +v 1.0473640e-002 3.4742600e-002 -1.1262870e-002 +v 1.4055380e-002 3.4483430e-002 -1.4495730e-002 +v -3.6970520e-002 3.5680360e-002 -1.5007790e-002 +v -2.4719500e-003 3.8408770e-002 -1.4159030e-002 +v -3.9481890e-002 3.3618220e-002 -2.3612470e-002 +v -4.1091510e-002 3.4006000e-002 -1.1997540e-002 +v -3.1589810e-002 3.5592330e-002 -1.9204150e-002 +v -2.0086310e-002 3.8064450e-002 -1.7220790e-002 +v -1.1113250e-002 3.8290290e-002 -1.5646360e-002 +v 4.4522600e-003 3.7705190e-002 -1.2957650e-002 +v 1.5870480e-002 3.4416230e-002 -2.9666500e-003 +v -4.7872000e-002 3.4136300e-002 -1.5418250e-002 +v -4.7521640e-002 3.3622720e-002 -1.2804590e-002 +v -3.3407340e-002 3.7577040e-002 -1.6158190e-002 +v -2.7851470e-002 3.8404330e-002 -1.7210420e-002 +v -8.5065300e-003 3.9028950e-002 -1.3000800e-002 +v 6.4552500e-003 3.8165190e-002 -1.0164860e-002 +v 7.4147100e-003 3.4659190e-002 -3.0116800e-003 +v 1.1966200e-002 3.4335400e-002 -5.9571300e-003 +v 2.0414820e-002 3.5567580e-002 -3.7806900e-003 +v -1.9288780e-002 3.8762570e-002 -1.4202620e-002 +v -1.1390100e-003 3.9176760e-002 -1.0381370e-002 +v 3.8149200e-003 3.9024470e-002 -8.0827300e-003 +v 7.5208200e-003 3.6733400e-002 -6.7614300e-003 +v 1.9968120e-002 3.4843990e-002 -1.8984900e-003 +v -4.5058400e-002 3.3600490e-002 -1.2527510e-002 +v -3.0754850e-002 3.8639810e-002 -1.4050770e-002 +v -5.1499810e-002 3.3729110e-002 -1.2082510e-002 +v -2.3756860e-002 3.8585750e-002 -1.1093270e-002 +v 3.9734700e-003 3.8208550e-002 -3.7963500e-003 +v 9.5485400e-003 3.4232620e-002 1.7162000e-003 +v 2.9086550e-002 3.5799990e-002 3.5630900e-003 +v -5.5965200e-002 3.3529910e-002 -9.1246200e-003 +v -1.9523510e-002 3.8505210e-002 -4.5434500e-003 +v 1.6363470e-002 3.4394790e-002 2.2948600e-003 +v 2.1324740e-002 3.4624040e-002 5.6444000e-003 +v -3.9670300e-002 3.6174000e-002 -7.3397700e-003 +v -1.4251730e-002 3.8648030e-002 -4.3030400e-003 +v 2.3262300e-003 3.5348200e-002 2.3246000e-003 +v 1.4014300e-002 3.5703800e-002 3.8878900e-003 +v 1.5322800e-002 3.6239700e-002 3.6628500e-003 +v 2.3753130e-002 3.4670710e-002 3.9885300e-003 +v 3.2369180e-002 3.5816010e-002 7.0246300e-003 +v -6.3715900e-002 3.3776930e-002 -8.0065600e-003 +v -6.4266880e-002 3.3562500e-002 -5.1253200e-003 +v -3.8066600e-002 3.8518600e-002 -7.3079600e-003 +v -9.4308800e-003 3.8887690e-002 -7.4848700e-003 +v 3.9677800e-003 3.4200210e-002 4.9754500e-003 +v 9.4292600e-003 3.6030400e-002 4.5275100e-003 +v 2.9859020e-002 3.4980130e-002 9.8349300e-003 +v -5.2730060e-002 3.3497900e-002 -1.8117500e-003 +v -4.1271000e-002 3.3855400e-002 -1.8800800e-003 +v -3.1105000e-003 3.8946190e-002 -2.7793900e-003 +v 6.2194100e-003 3.5134100e-002 6.5492800e-003 +v 2.0897900e-002 3.5937100e-002 8.7849000e-003 +v 3.5606010e-002 3.6526640e-002 9.8155300e-003 +v -6.7078340e-002 3.3840100e-002 -6.1688300e-003 +v -8.1140000e-004 3.7424170e-002 4.7721500e-003 +v 3.1492300e-003 3.4125310e-002 1.1762220e-002 +v 4.9172000e-003 3.3997100e-002 9.1666100e-003 +v 2.5130800e-002 3.4546910e-002 1.1012580e-002 +v 2.8248620e-002 3.5046370e-002 1.6016700e-002 +v -6.7032970e-002 6.5145960e-002 2.7292860e-002 +v -4.6380170e-002 3.3605230e-002 -8.9435000e-004 +v -3.3163400e-002 3.8195400e-002 -5.2520000e-004 +v -3.2074200e-002 3.8323400e-002 -4.2109000e-004 +v -2.1692690e-002 3.8266010e-002 4.5100800e-003 +v 2.3930750e-002 3.4816710e-002 1.7739160e-002 +v 4.2719120e-002 3.9977070e-002 8.9321600e-003 +v -5.8604080e-002 3.3462230e-002 -2.1667000e-004 +v -3.7314400e-002 3.3633000e-002 4.5724700e-003 +v -1.0423990e-002 3.8488570e-002 6.2292700e-003 +v -1.3896900e-003 3.8651360e-002 2.3966500e-003 +v -3.0845000e-004 3.5462480e-002 8.2607200e-003 +v -1.4089000e-003 3.6193080e-002 1.2944550e-002 +v 2.2252900e-002 3.6583300e-002 1.3979700e-002 +v -7.0961830e-002 3.4345730e-002 -7.8374000e-004 +v -6.9066180e-002 3.3717630e-002 -1.9761000e-004 +v -6.4825640e-002 3.3505860e-002 2.8222500e-003 +v -4.7059660e-002 3.3501860e-002 3.5646400e-003 +v -3.6953800e-003 3.8172780e-002 1.3046800e-002 +v 3.3475850e-002 3.6447340e-002 1.6266960e-002 +v 3.7249610e-002 3.7509920e-002 1.4815820e-002 +v -4.5675940e-002 3.3703640e-002 6.4300300e-003 +v -3.8639270e-002 3.3937310e-002 8.5506500e-003 +v -9.5064100e-003 3.8352640e-002 1.5570660e-002 +v 2.1499800e-002 3.5807100e-002 1.8169400e-002 +v 4.4876460e-002 4.1230990e-002 1.6008250e-002 +v -7.2474010e-002 3.6255930e-002 1.5532600e-003 +v -7.1498130e-002 3.4452970e-002 4.2026500e-003 +v -2.7790900e-002 3.8062900e-002 7.9376100e-003 +v -1.6556410e-002 3.8286470e-002 1.0215790e-002 +v 8.1043500e-003 3.4842900e-002 1.8134600e-002 +v 2.3589460e-002 3.5890600e-002 2.5337690e-002 +v 4.1261350e-002 4.0585070e-002 2.0751930e-002 +v -5.1350870e-002 3.3645700e-002 8.0329400e-003 +v -4.7104300e-002 3.5549500e-002 8.0803900e-003 +v -1.4103500e-003 3.6999940e-002 1.6982030e-002 +v 9.1714000e-004 3.4803380e-002 1.5634690e-002 +v 2.8887900e-003 3.4636250e-002 1.8849770e-002 +v 1.3279200e-002 3.4379500e-002 2.1423700e-002 +v 1.4322700e-002 3.4425500e-002 2.1593200e-002 +v 1.7490100e-002 3.4646300e-002 2.2040900e-002 +v 2.9868460e-002 3.6248820e-002 1.9872200e-002 +v -3.9222000e-002 3.6326200e-002 1.0789900e-002 +v -3.0307100e-002 3.3995400e-002 1.4706400e-002 +v 2.0081230e-002 3.5172700e-002 2.8018770e-002 +v 2.4989010e-002 3.8104580e-002 2.9429570e-002 +v 3.3584130e-002 3.8303930e-002 2.2928670e-002 +v 4.9015720e-002 4.4573630e-002 2.0659450e-002 +v -5.8225970e-002 6.6607310e-002 3.5050280e-002 +v -6.7330830e-002 3.3846440e-002 8.7266300e-003 +v -3.4692330e-002 3.3828710e-002 1.2438580e-002 +v -2.9803200e-002 3.4287000e-002 1.6353100e-002 +v 1.7023800e-003 3.6310890e-002 2.1179600e-002 +v 4.5137020e-002 4.4625440e-002 2.5516510e-002 +v -6.8876490e-002 1.1022176e-001 3.9004630e-002 +v -5.7680560e-002 3.3622690e-002 1.4040310e-002 +v -5.3210500e-002 3.3585300e-002 1.3987000e-002 +v -3.5711600e-002 3.5891600e-002 1.5502900e-002 +v -2.8861500e-002 3.5396700e-002 1.7350000e-002 +v -2.6580500e-002 3.7742600e-002 1.5705300e-002 +v -1.0974400e-003 3.8147840e-002 2.0427010e-002 +v 3.5047710e-002 4.0973940e-002 2.6970390e-002 +v -6.9685460e-002 3.4478780e-002 9.7984300e-003 +v -5.4019000e-002 3.3309900e-002 1.5848000e-002 +v 4.4816800e-003 3.7117830e-002 2.4755300e-002 +v 6.6605500e-003 3.5204730e-002 2.4315930e-002 +v 8.3833000e-003 3.4748700e-002 2.4057310e-002 +v 3.8883100e-002 4.1032980e-002 2.4976570e-002 +v -2.6441900e-003 3.8727070e-002 2.5131260e-002 +v 3.2222300e-003 3.8708440e-002 2.5898750e-002 +v 9.0016500e-003 3.6890930e-002 2.8482190e-002 +v 1.3196980e-002 3.4835790e-002 3.1630980e-002 +v 2.2291600e-002 3.7053310e-002 3.3101020e-002 +v 2.8948390e-002 3.9160020e-002 2.7234810e-002 +v -8.7773470e-002 1.1181412e-001 3.7144310e-002 +v -1.7870490e-002 3.8203890e-002 2.0243220e-002 +v 1.0087420e-002 3.7047690e-002 3.0822500e-002 +v 4.2296550e-002 4.5435770e-002 2.9040920e-002 +v -8.4341340e-002 1.1388013e-001 4.6513480e-002 +v -7.3795710e-002 1.0895629e-001 3.9217250e-002 +v -5.1243340e-002 6.4239200e-002 3.4258040e-002 +v -6.1777390e-002 3.4017860e-002 1.6900580e-002 +v -3.6665100e-002 3.5304200e-002 2.3032000e-002 +v -1.4930180e-002 3.8643510e-002 2.9378330e-002 +v -8.0894520e-002 1.0967225e-001 3.7910230e-002 +v -8.9822620e-002 1.1387199e-001 3.2845310e-002 +v -6.9655510e-002 6.8728370e-002 3.1127880e-002 +v -7.8449800e-002 1.0988832e-001 4.2517920e-002 +v -7.5824140e-002 1.0794900e-001 3.7128750e-002 +v -5.5740630e-002 3.4128050e-002 2.6674360e-002 +v -3.8279600e-002 3.5429000e-002 2.4380600e-002 +v -3.5283340e-002 3.4179780e-002 2.2744860e-002 +v -2.5798070e-002 3.7865000e-002 1.9981460e-002 +v 6.9064300e-003 3.9004270e-002 2.9548510e-002 +v 1.5448990e-002 3.4852440e-002 3.6984890e-002 +v 1.9128230e-002 3.5640640e-002 3.6642280e-002 +v -6.3664970e-002 6.6047840e-002 3.1828080e-002 +v 3.9604800e-002 4.4939530e-002 2.9992360e-002 +v -8.0294310e-002 7.1702430e-002 1.5995300e-002 +v -5.4185430e-002 6.7322700e-002 3.6935610e-002 +v -7.3110210e-002 1.4847168e-001 -2.8748470e-002 +v -5.8999980e-002 7.3751550e-002 4.1197080e-002 +v -5.9520730e-002 6.1040260e-002 -2.3753800e-003 +v -6.2791800e-002 3.4596760e-002 2.3505640e-002 +v -4.1895500e-002 3.3668300e-002 2.6940000e-002 +v 8.9808200e-003 3.7639400e-002 3.3900800e-002 +v 8.5287800e-003 3.4888000e-002 3.6265100e-002 +v -8.9803890e-002 1.1498106e-001 4.2771650e-002 +v -6.5545420e-002 7.4430370e-002 3.9168070e-002 +v -6.4644190e-002 6.1723230e-002 2.2552000e-004 +v 5.2496900e-003 3.9507100e-002 3.3271200e-002 +v 2.0250320e-002 3.7033170e-002 3.9327190e-002 +v -6.7006400e-002 6.3292870e-002 -1.7493900e-003 +v -6.4479770e-002 6.0651470e-002 4.2343200e-003 +v -5.7219630e-002 5.7000470e-002 4.9175800e-003 +v -7.4362810e-002 7.2437050e-002 3.1430040e-002 +v -6.2019000e-002 3.4343180e-002 3.1883280e-002 +v -4.6870820e-002 3.4444130e-002 3.0513130e-002 +v -2.0814280e-002 3.8400960e-002 2.7868430e-002 +v 1.6439350e-002 3.5635110e-002 4.1281040e-002 +v -6.9087160e-002 1.1205014e-001 4.5320060e-002 +v -7.1811570e-002 1.4861318e-001 -3.4639490e-002 +v -6.9538770e-002 6.3074750e-002 3.5758200e-003 +v -8.4863890e-002 7.8392100e-002 1.6462010e-002 +v -9.1188780e-002 1.1588893e-001 2.4705540e-002 +v -8.8827760e-002 1.1359169e-001 2.3873640e-002 +v -7.1302830e-002 1.1325363e-001 4.9444530e-002 +v -5.4876950e-002 7.0282330e-002 3.8828200e-002 +v -7.7208880e-002 1.0715887e-001 3.4738290e-002 +v -6.1241780e-002 5.9007440e-002 8.0916600e-003 +v -6.5885650e-002 3.5025080e-002 2.9416520e-002 +v -5.7889430e-002 3.4419570e-002 3.6265760e-002 +v -5.1847710e-002 3.4470270e-002 3.4635180e-002 +v -3.4834600e-002 3.4721400e-002 3.4578200e-002 +v -3.0984700e-002 3.8191900e-002 3.2390100e-002 +v -4.9613100e-003 3.9364900e-002 3.6702200e-002 +v 1.2224170e-002 3.5177480e-002 4.2620580e-002 +v -7.4898220e-002 1.1458863e-001 5.0776480e-002 +v -8.0469100e-002 1.1357963e-001 4.6643440e-002 +v -7.4107560e-002 6.9586030e-002 2.7264400e-002 +v -7.9002620e-002 7.6339320e-002 2.9248090e-002 +v -6.5297080e-002 3.4778970e-002 3.3744340e-002 +v -3.3656400e-002 3.4344100e-002 3.6914100e-002 +v 4.9318500e-003 3.4814800e-002 4.3462110e-002 +v 1.1347440e-002 3.6213020e-002 4.4652280e-002 +v -6.0569260e-002 7.1154540e-002 3.8653760e-002 +v -8.8979470e-002 1.1450869e-001 2.8446030e-002 +v -6.8543520e-002 6.1090480e-002 1.0557760e-002 +v -8.2710960e-002 1.1648975e-001 4.8518530e-002 +v -4.1913210e-002 3.4467720e-002 3.3200040e-002 +v -1.1289800e-002 3.9529200e-002 3.8844100e-002 +v -2.8261900e-003 3.4885340e-002 4.5611410e-002 +v -6.4561210e-002 5.9484140e-002 1.3061680e-002 +v -5.8581440e-002 5.7801460e-002 1.3429540e-002 +v -2.3320000e-002 3.9169500e-002 3.8473300e-002 +v -1.8159900e-002 3.9322300e-002 3.9402900e-002 +v -1.6471400e-002 3.4812800e-002 4.3684700e-002 +v 3.2906600e-003 3.5833470e-002 4.6024610e-002 +v -8.5229630e-002 1.1200712e-001 3.0416940e-002 +v -8.5644730e-002 1.1131719e-001 3.4234780e-002 +v -7.4530360e-002 6.6680690e-002 4.6953300e-003 +v -7.1112970e-002 6.2751470e-002 8.7995500e-003 +v -6.1149380e-002 5.8834410e-002 1.6539440e-002 +v -4.6912270e-002 3.4627180e-002 3.9739710e-002 +v -4.0760350e-002 3.4668230e-002 4.0492530e-002 +v -2.6323100e-002 3.4658000e-002 4.3473500e-002 +v -3.1836600e-003 3.6229910e-002 4.7873100e-002 +v -7.9940490e-002 1.0916678e-001 3.4119800e-002 +v -5.9712170e-002 6.3165280e-002 2.8789180e-002 +v -5.1176600e-002 6.8061880e-002 3.7398330e-002 +v -5.0126580e-002 7.0933150e-002 3.9481010e-002 +v -7.2790130e-002 6.4399880e-002 1.5205950e-002 +v -6.8511230e-002 6.1214650e-002 1.5354080e-002 +v -3.9343210e-002 3.5440180e-002 4.2492560e-002 +v -8.1305900e-003 3.5008350e-002 4.7502400e-002 +v -6.6080670e-002 7.0202740e-002 3.5552860e-002 +v -6.8602600e-002 1.4992277e-001 -4.0051350e-002 +v -7.1722100e-002 6.7023040e-002 2.4959750e-002 +v -7.5115010e-002 6.6557040e-002 1.0244090e-002 +v -6.5146650e-002 3.5945650e-002 3.9775080e-002 +v -3.6898600e-002 3.5924640e-002 4.4794170e-002 +v -9.4780400e-003 3.5977600e-002 4.9434210e-002 +v -8.5175960e-002 1.1706809e-001 4.8139420e-002 +v -6.3366400e-002 6.2790260e-002 2.5647610e-002 +v -6.6633330e-002 6.1001700e-002 1.8101240e-002 +v -5.8167590e-002 5.9985190e-002 2.2606060e-002 +v -6.4212210e-002 3.4992560e-002 3.9401920e-002 +v -5.3425790e-002 3.4560020e-002 4.2782420e-002 +v -1.8031490e-002 3.4859970e-002 4.9264760e-002 +v -1.1440410e-002 3.7640770e-002 5.0275730e-002 +v -7.5165320e-002 1.1154286e-001 4.6707180e-002 +v -7.7168390e-002 6.9826450e-002 5.0605600e-003 +v -7.2801360e-002 6.4382590e-002 1.2089080e-002 +v -7.8022000e-002 7.0995160e-002 2.1322150e-002 +v -6.1263370e-002 3.4690410e-002 4.1994900e-002 +v -5.4403750e-002 3.5007310e-002 4.4874590e-002 +v -4.5754280e-002 3.5206980e-002 4.3518120e-002 +v -3.3832440e-002 3.5168820e-002 4.6957890e-002 +v -2.8657630e-002 3.5083380e-002 5.0549440e-002 +v -1.5306440e-002 3.5246410e-002 5.0133810e-002 +v -6.5283650e-002 1.5592447e-001 -4.9865930e-002 +v -6.6467860e-002 1.4871539e-001 -3.1579300e-002 +v -6.2095980e-002 1.6388324e-001 -5.8385930e-002 +v -6.3274890e-002 1.5245731e-001 -3.2221730e-002 +v -4.3755720e-002 1.4773408e-001 -2.1433200e-003 +v -6.5696940e-002 1.4561631e-001 -1.8974710e-002 +v -6.6713650e-002 1.5358824e-001 -4.9097100e-002 +v -1.0482810e-002 1.6668287e-001 -2.1746090e-002 +v -6.2744510e-002 1.6397531e-001 -5.9398280e-002 +v -7.0413230e-002 1.4129200e-001 -8.4590800e-003 +v -6.1530380e-002 1.4037628e-001 -6.2734700e-003 +v -1.1452460e-002 1.7220633e-001 -2.6844980e-002 +v -6.3731140e-002 1.6577037e-001 -6.0103610e-002 +v -2.8218820e-002 1.5758144e-001 -1.0999490e-002 +v -1.8471270e-002 1.5967716e-001 -1.1169510e-002 +v -6.6700710e-002 1.5236775e-001 -4.5266390e-002 +v -4.9896410e-002 1.4670859e-001 -1.8614200e-003 +v -3.1449640e-002 1.5460463e-001 -7.6802300e-003 +v -6.7447660e-002 1.5507675e-001 -5.1594250e-002 +v -1.0906650e-002 1.7649301e-001 -2.9246300e-002 +v -7.2083600e-002 1.4965550e-001 -3.9265860e-002 +v -6.4230830e-002 1.4877806e-001 -2.5899710e-002 +v -6.3056640e-002 1.4341650e-001 -7.4907700e-003 +v -5.3043350e-002 1.4092550e-001 -4.7408000e-004 +v -3.9269410e-002 1.5205232e-001 -6.6203800e-003 +v -6.4796930e-002 1.5210615e-001 -3.6185520e-002 +v -6.4400320e-002 1.5834400e-001 -5.4256370e-002 +v -6.6178120e-002 1.4218350e-001 -9.3766300e-003 +v -6.7751430e-002 1.4605207e-001 -2.3333300e-002 +v -6.4731580e-002 1.5410067e-001 -4.0464820e-002 +v -2.4265590e-002 1.5687690e-001 -7.8509300e-003 +v -1.5723180e-002 1.6312344e-001 -1.6396570e-002 +v -7.0887660e-002 1.4404618e-001 -1.4908480e-002 +v -4.4341830e-002 1.5113809e-001 -5.6859800e-003 +v -6.2896810e-002 1.4694778e-001 -1.3098620e-002 +v -6.3755400e-002 1.4428875e-001 -1.1395730e-002 +v -6.8214560e-002 1.4390932e-001 -1.4984170e-002 +v -5.0271440e-002 1.4336563e-001 1.5153000e-003 +v -2.8535590e-002 1.6208479e-001 -1.4786030e-002 +v -6.5810700e-002 1.4359119e-001 -1.2585380e-002 +v -5.6179200e-002 1.3774406e-001 -4.0674300e-003 +v -6.8866880e-002 1.4723338e-001 -2.8739870e-002 +v -6.0965420e-002 1.7002113e-001 -6.0839390e-002 +v -1.3895490e-002 1.6787168e-001 -2.1897230e-002 +v -6.9413000e-002 1.5121847e-001 -4.4538540e-002 +v -5.5039800e-002 5.7309700e-002 1.6990900e-002 +f 1069 1647 1578 +f 1058 909 939 +f 421 1176 238 +f 1055 1101 1042 +f 238 1059 1126 +f 1254 30 1261 +f 1065 1071 1 +f 1037 1130 1120 +f 1570 2381 1585 +f 2434 2502 2473 +f 1632 1654 1646 +f 1144 1166 669 +f 1202 1440 305 +f 1071 1090 1 +f 1555 1570 1584 +f 1184 1174 404 +f 65 432 12 +f 1032 1085 574 +f 1789 2207 2223 +f 1154 1118 1184 +f 1141 1086 1154 +f 99 1117 342 +f 404 1174 419 +f 489 2000 1998 +f 1118 1174 1184 +f 1196 403 136 +f 1495 717 1490 +f 1804 402 1207 +f 2272 1398 891 +f 1100 1002 804 +f 1596 1595 2381 +f 208 420 1207 +f 402 208 1207 +f 1455 1935 1925 +f 1176 1059 238 +f 1150 1040 348 +f 1957 1537 2051 +f 1124 1189 939 +f 1804 1207 1823 +f 1381 1300 1109 +f 383 384 1182 +f 1085 1086 1141 +f 1040 1046 132 +f 220 1495 1188 +f 420 261 1207 +f 261 420 1065 +f 1055 1133 1101 +f 1054 421 403 +f 182 1109 2 +f 1181 1207 320 +f 545 1570 1561 +f 35 342 432 +f 1024 574 1141 +f 432 342 12 +f 1489 1081 1547 +f 1181 320 1805 +f 1516 1683 1507 +f 357 1117 1047 +f 1561 1570 1555 +f 1090 1196 1206 +f 1047 1203 1051 +f 1165 202 1121 +f 1099 341 301 +f 1174 240 419 +f 922 921 833 +f 1121 1080 385 +f 815 21 1183 +f 35 99 342 +f 1083 398 262 +f 106 94 1317 +f 94 292 1317 +f 292 95 1317 +f 940 1039 1033 +f 1300 1306 433 +f 21 212 471 +f 1120 1131 1037 +f 833 921 688 +f 1117 357 342 +f 106 271 94 +f 386 227 1375 +f 1130 1044 1053 +f 419 240 219 +f 1255 1244 32 +f 1557 1081 1489 +f 2062 2120 2109 +f 2034 2110 430 +f 23 315 1111 +f 291 94 271 +f 291 292 94 +f 50 386 95 +f 964 734 665 +f 1616 1585 1611 +f 445 1084 402 +f 574 1085 1141 +f 1654 341 1653 +f 220 1188 1640 +f 342 69 12 +f 417 261 328 +f 292 50 95 +f 204 227 386 +f 50 204 386 +f 1276 1471 1311 +f 1206 1196 136 +f 1033 1055 1042 +f 1037 1044 1130 +f 1180 320 417 +f 1121 202 1080 +f 325 203 271 +f 291 76 292 +f 292 237 50 +f 2159 1696 1767 +f 583 929 850 +f 1584 1585 1616 +f 1495 1490 1188 +f 1557 1489 1660 +f 1078 1069 1494 +f 1972 1992 1971 +f 183 1226 2000 +f 325 429 203 +f 292 76 237 +f 1152 227 1143 +f 1488 1412 1489 +f 1638 1646 1653 +f 1947 1869 2468 +f 203 306 291 +f 306 76 291 +f 237 248 50 +f 204 1143 227 +f 2395 14 429 +f 1502 881 2500 +f 1 1090 202 +f 1652 1653 1099 +f 2117 1863 2496 +f 50 248 204 +f 160 792 994 +f 884 888 857 +f 544 2117 2496 +f 1090 1206 202 +f 2463 879 2492 +f 429 306 203 +f 498 188 418 +f 865 884 857 +f 994 998 1014 +f 884 897 888 +f 1795 948 1802 +f 208 1035 1071 +f 1065 1 1066 +f 377 435 1377 +f 304 429 14 +f 304 306 429 +f 73 60 74 +f 248 592 204 +f 846 2264 829 +f 897 912 906 +f 1004 991 992 +f 1422 1421 1233 +f 980 10 303 +f 1058 922 909 +f 2436 2449 2418 +f 394 435 377 +f 435 475 446 +f 475 474 446 +f 336 337 361 +f 338 235 372 +f 624 148 129 +f 812 306 596 +f 1726 992 1019 +f 945 1514 1511 +f 1069 1627 1628 +f 1812 1823 1181 +f 1165 1121 169 +f 447 475 435 +f 2487 2458 901 +f 42 59 46 +f 401 7 187 +f 1010 970 797 +f 1513 220 1640 +f 2474 2491 2462 +f 594 307 1014 +f 398 1513 1640 +f 307 594 1026 +f 545 2381 1570 +f 403 421 238 +f 445 402 127 +f 1611 1631 1616 +f 1805 1180 1148 +f 394 447 435 +f 2341 2413 2376 +f 75 74 60 +f 541 47 42 +f 47 59 42 +f 541 42 28 +f 917 931 1103 +f 897 906 883 +f 2484 2068 779 +f 888 883 857 +f 261 1065 328 +f 363 1307 349 +f 377 363 394 +f 444 747 464 +f 323 338 362 +f 92 116 74 +f 592 634 97 +f 982 1027 1004 +f 1020 982 1004 +f 1084 1054 1035 +f 208 402 1084 +f 421 1119 1176 +f 1207 1181 1823 +f 1179 1187 1160 +f 263 296 1343 +f 1298 296 1307 +f 1307 296 349 +f 405 363 349 +f 405 394 363 +f 405 447 394 +f 362 372 384 +f 338 372 362 +f 983 1004 987 +f 122 134 139 +f 415 440 414 +f 75 92 74 +f 226 186 246 +f 796 787 700 +f 1119 1059 1176 +f 122 114 91 +f 624 129 116 +f 641 558 631 +f 1311 1318 1487 +f 100 1162 1170 +f 1653 341 1099 +f 1316 1983 273 +f 263 277 296 +f 296 358 349 +f 436 447 405 +f 109 554 570 +f 504 1385 2501 +f 115 122 91 +f 2068 2460 779 +f 43 777 163 +f 378 405 349 +f 358 378 349 +f 448 447 436 +f 448 476 447 +f 78 77 108 +f 75 60 47 +f 1764 2481 1795 +f 717 714 1512 +f 1490 717 1501 +f 238 1126 168 +f 1878 1866 826 +f 2025 2360 2367 +f 251 278 263 +f 278 277 263 +f 277 318 296 +f 296 318 358 +f 318 350 358 +f 378 436 405 +f 384 372 1182 +f 454 440 415 +f 987 1004 992 +f 493 476 448 +f 323 788 338 +f 403 238 136 +f 1565 1503 1474 +f 297 277 278 +f 297 318 277 +f 358 350 378 +f 378 388 436 +f 476 493 500 +f 73 105 60 +f 323 337 312 +f 953 1573 2358 +f 142 161 119 +f 454 443 440 +f 1862 1871 1405 +f 297 319 318 +f 560 47 541 +f 170 1323 111 +f 357 1047 1050 +f 1119 98 1059 +f 1838 1877 1900 +f 2359 230 251 +f 350 364 378 +f 449 448 436 +f 449 493 448 +f 185 186 226 +f 443 469 479 +f 874 165 2480 +f 463 444 464 +f 64 105 91 +f 1182 440 1129 +f 1958 1651 2502 +f 1238 2034 191 +f 251 279 278 +f 278 279 297 +f 364 388 378 +f 483 493 449 +f 134 148 139 +f 244 268 259 +f 910 942 930 +f 105 115 91 +f 24 30 18 +f 1132 487 1059 +f 1869 1947 2021 +f 2497 2494 2463 +f 2359 2385 230 +f 230 280 251 +f 251 280 279 +f 279 308 297 +f 297 308 319 +f 319 364 318 +f 364 350 318 +f 388 395 436 +f 436 395 449 +f 493 472 500 +f 122 129 134 +f 125 142 124 +f 373 400 393 +f 24 557 30 +f 2264 2278 2251 +f 1261 30 1269 +f 1730 1862 1877 +f 252 280 230 +f 343 364 319 +f 364 343 388 +f 63 64 91 +f 399 393 416 +f 416 444 463 +f 162 189 142 +f 768 373 326 +f 189 661 177 +f 189 199 661 +f 847 887 864 +f 533 747 444 +f 1744 1022 1418 +f 1170 524 729 +f 121 1342 128 +f 1236 1244 26 +f 280 281 279 +f 281 308 279 +f 343 319 308 +f 343 365 388 +f 388 365 395 +f 365 406 395 +f 406 449 395 +f 483 477 493 +f 477 491 472 +f 493 477 472 +f 78 109 77 +f 166 174 196 +f 481 150 814 +f 63 59 64 +f 326 373 393 +f 643 260 43 +f 230 253 252 +f 449 441 483 +f 441 477 483 +f 415 416 463 +f 226 246 245 +f 464 470 454 +f 323 362 337 +f 52 37 1283 +f 253 281 252 +f 281 280 252 +f 309 308 281 +f 330 343 308 +f 366 365 343 +f 441 449 406 +f 464 814 15 +f 883 906 887 +f 337 362 371 +f 479 498 290 +f 247 746 1003 +f 25 37 557 +f 640 930 669 +f 2486 2499 2459 +f 309 330 308 +f 343 330 366 +f 441 437 477 +f 290 498 418 +f 124 119 108 +f 77 124 108 +f 589 125 109 +f 570 589 109 +f 125 162 142 +f 1045 433 1034 +f 1207 261 320 +f 2004 2474 2495 +f 1215 1228 2285 +f 365 396 406 +f 396 422 406 +f 422 437 441 +f 406 422 441 +f 59 47 60 +f 51 78 66 +f 361 371 383 +f 196 215 214 +f 463 454 415 +f 27 41 535 +f 53 1283 37 +f 84 1299 1283 +f 1805 320 1180 +f 254 253 222 +f 254 281 253 +f 309 366 330 +f 396 365 366 +f 456 477 437 +f 484 491 477 +f 2480 2485 2493 +f 418 188 187 +f 53 85 1283 +f 85 84 1283 +f 420 1071 1065 +f 264 281 254 +f 298 309 281 +f 368 366 367 +f 368 396 366 +f 1639 1564 1139 +f 560 48 47 +f 82 471 212 +f 25 38 37 +f 202 1206 1080 +f 264 298 281 +f 298 331 309 +f 309 331 366 +f 331 367 366 +f 396 368 422 +f 422 456 437 +f 491 1192 313 +f 1699 2064 1710 +f 462 443 479 +f 371 362 384 +f 2502 2476 2464 +f 371 384 383 +f 21 732 212 +f 1571 1629 1627 +f 38 39 53 +f 37 38 53 +f 39 85 53 +f 1173 1184 404 +f 1006 2142 1674 +f 201 255 254 +f 255 264 254 +f 368 407 422 +f 450 456 422 +f 450 484 456 +f 456 484 477 +f 314 1192 491 +f 2027 2501 2489 +f 2475 2471 2488 +f 551 492 732 +f 464 481 814 +f 1081 1494 1547 +f 201 231 255 +f 407 450 422 +f 484 494 491 +f 494 327 491 +f 327 314 491 +f 876 797 995 +f 847 856 829 +f 125 143 162 +f 134 129 148 +f 1564 1571 1627 +f 417 320 261 +f 328 1065 1066 +f 170 156 201 +f 156 231 201 +f 231 282 255 +f 282 264 255 +f 450 485 484 +f 484 485 494 +f 2463 2486 2479 +f 159 185 167 +f 492 68 212 +f 732 492 212 +f 68 82 212 +f 1311 1471 1296 +f 101 156 111 +f 332 264 282 +f 332 298 264 +f 332 331 298 +f 331 332 367 +f 407 423 450 +f 450 423 485 +f 804 1002 1443 +f 2484 779 946 +f 689 443 462 +f 440 689 1129 +f 166 167 174 +f 38 31 39 +f 112 145 101 +f 101 145 156 +f 156 256 231 +f 332 423 368 +f 367 332 368 +f 368 423 407 +f 946 779 920 +f 1432 1261 1449 +f 461 478 453 +f 464 15 470 +f 31 54 39 +f 39 54 85 +f 86 101 85 +f 145 210 156 +f 282 283 332 +f 283 369 332 +f 369 423 332 +f 423 408 485 +f 854 876 965 +f 78 108 66 +f 440 443 689 +f 374 2465 961 +f 929 519 979 +f 54 86 85 +f 156 241 256 +f 256 282 231 +f 256 283 282 +f 389 423 369 +f 389 408 423 +f 408 457 485 +f 457 49 485 +f 485 49 494 +f 494 135 327 +f 175 83 314 +f 1167 1140 1483 +f 196 174 215 +f 697 16 68 +f 1038 82 16 +f 140 117 141 +f 1654 1653 1646 +f 1234 54 31 +f 86 112 101 +f 210 241 156 +f 923 917 911 +f 697 34 16 +f 145 193 210 +f 256 265 283 +f 265 310 283 +f 283 310 369 +f 310 344 369 +f 344 370 369 +f 370 389 369 +f 409 408 389 +f 409 466 408 +f 466 457 408 +f 466 49 457 +f 49 135 494 +f 174 225 215 +f 1014 766 602 +f 826 2220 2215 +f 1078 1494 1081 +f 1273 70 86 +f 120 112 86 +f 146 145 112 +f 146 193 145 +f 265 256 241 +f 223 265 241 +f 486 49 466 +f 175 327 135 +f 105 122 115 +f 480 15 681 +f 225 234 215 +f 731 34 697 +f 86 54 1273 +f 70 120 86 +f 193 241 210 +f 299 310 265 +f 310 333 344 +f 344 351 370 +f 424 466 409 +f 135 49 175 +f 214 215 234 +f 48 75 47 +f 34 9 1038 +f 16 34 1038 +f 203 291 271 +f 9 558 754 +f 1195 397 1120 +f 120 146 112 +f 146 194 193 +f 266 265 223 +f 266 299 265 +f 299 333 310 +f 333 351 344 +f 382 383 392 +f 399 416 415 +f 266 333 299 +f 351 352 370 +f 424 486 466 +f 487 175 49 +f 7 117 187 +f 1182 414 440 +f 41 42 46 +f 290 289 497 +f 2502 2464 2473 +f 372 399 414 +f 1570 1585 1584 +f 1066 1 1165 +f 1 202 1165 +f 120 70 102 +f 157 146 120 +f 194 223 193 +f 223 241 193 +f 352 379 370 +f 370 379 389 +f 410 409 389 +f 2478 1409 1958 +f 806 945 1002 +f 157 194 146 +f 267 266 223 +f 267 333 266 +f 379 410 389 +f 410 438 409 +f 438 424 409 +f 190 205 143 +f 337 371 361 +f 2215 830 826 +f 1631 1646 1638 +f 102 157 120 +f 157 195 194 +f 195 223 194 +f 195 211 223 +f 223 211 267 +f 267 300 333 +f 300 334 351 +f 333 300 351 +f 351 334 352 +f 410 411 438 +f 438 486 424 +f 487 49 486 +f 875 594 989 +f 108 581 66 +f 225 245 244 +f 312 336 335 +f 151 754 107 +f 274 1386 300 +f 352 334 379 +f 923 1729 1096 +f 244 245 268 +f 463 464 454 +f 414 399 415 +f 15 480 470 +f 1647 1069 1078 +f 909 922 833 +f 387 417 328 +f 133 157 102 +f 1314 133 102 +f 133 195 157 +f 1148 1179 1160 +f 1046 1167 182 +f 379 411 410 +f 792 339 229 +f 391 7 668 +f 185 226 174 +f 461 290 497 +f 2027 504 2501 +f 1196 1054 403 +f 728 1019 752 +f 2459 2483 2461 +f 1291 1264 55 +f 133 1356 195 +f 195 1356 211 +f 412 438 411 +f 4 486 438 +f 458 4 438 +f 4 487 486 +f 1720 1572 1771 +f 245 275 268 +f 1869 2021 2059 +f 235 399 372 +f 64 60 105 +f 836 2492 879 +f 1315 133 1314 +f 1331 1382 1356 +f 1310 926 1128 +f 7 1121 117 +f 119 161 611 +f 380 379 334 +f 379 380 411 +f 467 4 458 +f 495 487 4 +f 495 1126 487 +f 416 400 533 +f 479 469 498 +f 74 116 73 +f 478 461 497 +f 393 400 416 +f 61 1291 55 +f 505 1999 2474 +f 1999 2491 2474 +f 199 189 36 +f 1164 1165 169 +f 1179 387 249 +f 390 411 380 +f 411 390 412 +f 458 438 412 +f 495 168 1126 +f 480 469 470 +f 116 122 105 +f 418 187 140 +f 185 174 167 +f 166 148 167 +f 470 469 443 +f 40 55 32 +f 61 71 1291 +f 71 103 1291 +f 1184 1173 1154 +f 634 514 97 +f 425 458 412 +f 917 923 931 +f 2472 2489 853 +f 754 641 567 +f 44 567 1163 +f 454 470 443 +f 40 32 1249 +f 33 40 1249 +f 56 55 40 +f 56 61 55 +f 451 1265 439 +f 1180 417 1179 +f 1099 301 1077 +f 1189 1058 939 +f 1059 221 1132 +f 598 1074 1075 +f 412 426 425 +f 650 186 185 +f 234 244 259 +f 226 245 225 +f 1033 1042 1030 +f 2492 836 247 +f 7 169 1121 +f 1462 1322 1482 +f 425 467 458 +f 496 4 467 +f 1751 2468 2480 +f 290 418 140 +f 326 789 762 +f 142 177 161 +f 165 1751 2480 +f 87 103 71 +f 103 87 104 +f 1180 1179 1148 +f 417 387 1179 +f 2081 2060 2031 +f 1154 1173 1141 +f 181 131 197 +f 442 425 426 +f 614 144 143 +f 876 1010 797 +f 40 45 56 +f 56 45 61 +f 87 71 61 +f 1563 1437 1590 +f 1121 385 117 +f 1148 1160 1137 +f 1449 1459 1439 +f 1028 2462 929 +f 442 459 425 +f 459 467 425 +f 168 495 4 +f 496 168 4 +f 1763 1403 1444 +f 140 187 117 +f 244 234 225 +f 246 740 269 +f 372 414 1182 +f 40 547 45 +f 45 62 61 +f 62 87 61 +f 87 88 104 +f 1084 517 1054 +f 387 328 1064 +f 2467 2497 2485 +f 286 1363 302 +f 205 189 162 +f 290 140 289 +f 214 234 224 +f 393 399 809 +f 315 1131 397 +f 302 321 353 +f 1164 169 391 +f 427 459 442 +f 217 496 467 +f 217 168 496 +f 978 969 2074 +f 361 383 382 +f 269 276 245 +f 1440 11 305 +f 62 88 87 +f 328 1066 1064 +f 1066 1165 1164 +f 242 287 302 +f 1363 242 302 +f 287 321 302 +f 1179 249 1187 +f 983 1020 1004 +f 464 747 481 +f 788 323 276 +f 269 245 246 +f 88 89 1325 +f 171 172 242 +f 360 353 321 +f 360 1354 353 +f 1057 1064 1164 +f 2184 2188 2183 +f 460 459 451 +f 460 467 459 +f 149 168 217 +f 149 136 168 +f 116 129 122 +f 109 124 77 +f 159 167 148 +f 28 42 41 +f 57 88 62 +f 45 57 62 +f 1336 1325 89 +f 89 72 1336 +f 147 172 171 +f 172 258 242 +f 258 257 242 +f 257 287 242 +f 257 321 287 +f 345 360 321 +f 360 381 1354 +f 1069 938 1655 +f 387 473 249 +f 270 217 467 +f 130 136 149 +f 851 847 829 +f 983 987 975 +f 189 177 142 +f 88 72 89 +f 184 258 172 +f 257 288 321 +f 1265 451 459 +f 270 149 217 +f 226 225 174 +f 27 28 41 +f 109 125 124 +f 547 57 45 +f 57 58 88 +f 88 58 72 +f 2476 2484 2458 +f 147 184 172 +f 184 213 258 +f 258 243 257 +f 243 288 257 +f 345 321 288 +f 391 169 7 +f 468 460 451 +f 468 488 460 +f 270 467 460 +f 488 270 460 +f 1206 136 130 +f 481 793 150 +f 143 205 162 +f 142 119 124 +f 58 90 72 +f 90 128 72 +f 147 173 184 +f 173 213 184 +f 213 233 258 +f 258 233 243 +f 354 360 345 +f 354 381 360 +f 1026 991 307 +f 268 312 259 +f 1206 130 1080 +f 116 105 73 +f 139 148 166 +f 275 312 268 +f 188 401 187 +f 2479 2459 2461 +f 58 63 90 +f 1064 1066 1164 +f 1064 473 387 +f 288 311 345 +f 311 354 345 +f 996 994 307 +f 452 468 439 +f 452 478 468 +f 478 488 468 +f 141 130 149 +f 1564 1639 1563 +f 547 41 57 +f 2081 2107 2060 +f 382 381 354 +f 497 270 488 +f 289 149 270 +f 289 141 149 +f 114 122 139 +f 59 60 64 +f 275 323 312 +f 401 668 7 +f 41 46 57 +f 57 46 58 +f 1459 1345 1269 +f 1342 121 158 +f 166 173 158 +f 213 224 233 +f 233 259 243 +f 243 322 288 +f 322 311 288 +f 453 478 452 +f 497 289 270 +f 912 911 906 +f 276 323 275 +f 276 275 245 +f 46 63 58 +f 90 121 128 +f 173 214 213 +f 213 214 224 +f 259 322 243 +f 336 311 322 +f 336 354 311 +f 361 382 354 +f 1043 439 1290 +f 497 488 478 +f 385 130 141 +f 385 1080 130 +f 144 190 143 +f 535 41 547 +f 121 166 158 +f 335 336 322 +f 354 336 361 +f 2004 2481 1764 +f 698 439 1043 +f 289 140 141 +f 923 1096 931 +f 650 185 159 +f 46 59 63 +f 63 91 90 +f 90 114 121 +f 121 139 166 +f 173 196 214 +f 259 335 322 +f 2478 2502 2434 +f 312 337 336 +f 90 91 114 +f 114 139 121 +f 166 196 173 +f 224 234 233 +f 234 259 233 +f 259 312 335 +f 1124 916 1189 +f 542 541 530 +f 462 479 290 +f 269 783 276 +f 813 567 641 +f 276 783 788 +f 82 1038 1333 +f 816 701 703 +f 672 137 603 +f 625 635 624 +f 2457 2439 1973 +f 767 533 529 +f 2468 1869 2480 +f 662 190 639 +f 711 720 719 +f 630 639 614 +f 161 654 638 +f 781 991 982 +f 1227 31 516 +f 648 639 630 +f 630 614 590 +f 2098 544 1899 +f 578 579 586 +f 697 492 551 +f 529 533 400 +f 869 859 870 +f 1732 924 914 +f 1004 1027 991 +f 801 591 603 +f 636 676 651 +f 876 949 965 +f 2207 1789 1859 +f 76 739 237 +f 188 681 15 +f 578 604 599 +f 797 616 995 +f 510 2035 1365 +f 76 812 617 +f 617 739 76 +f 1468 93 1765 +f 596 546 812 +f 1457 1305 1477 +f 760 197 150 +f 671 773 765 +f 586 609 604 +f 591 700 632 +f 476 2312 474 +f 2084 2027 2489 +f 582 590 571 +f 1555 2449 1996 +f 674 546 596 +f 812 655 617 +f 161 177 661 +f 599 604 636 +f 700 787 576 +f 776 675 572 +f 776 674 675 +f 617 634 739 +f 591 632 649 +f 612 546 674 +f 617 655 634 +f 728 752 706 +f 571 2311 2305 +f 775 674 776 +f 775 612 674 +f 612 628 546 +f 546 628 812 +f 812 628 655 +f 620 630 615 +f 620 648 630 +f 667 653 646 +f 810 782 785 +f 150 197 814 +f 534 1517 2000 +f 702 572 2378 +f 748 776 572 +f 655 613 634 +f 911 917 905 +f 648 679 662 +f 727 771 713 +f 750 807 799 +f 639 190 144 +f 662 679 200 +f 702 748 572 +f 775 776 748 +f 628 718 655 +f 626 658 645 +f 791 778 790 +f 612 811 628 +f 613 514 634 +f 1380 1756 1673 +f 570 590 614 +f 720 741 719 +f 1074 795 835 +f 614 639 144 +f 612 775 811 +f 718 735 655 +f 655 735 613 +f 798 338 788 +f 636 652 676 +f 571 590 555 +f 528 730 687 +f 690 702 2312 +f 476 690 2312 +f 811 718 628 +f 721 778 727 +f 748 702 690 +f 735 686 613 +f 1517 2002 2127 +f 654 685 667 +f 569 588 606 +f 513 531 538 +f 538 549 548 +f 549 553 548 +f 550 588 549 +f 1903 869 870 +f 691 775 748 +f 691 600 775 +f 600 811 775 +f 811 563 718 +f 563 736 718 +f 718 736 735 +f 736 647 735 +f 735 647 686 +f 686 745 613 +f 745 514 613 +f 569 606 605 +f 654 667 638 +f 851 857 847 +f 588 569 549 +f 690 691 748 +f 680 514 745 +f 2127 2002 2094 +f 747 701 481 +f 400 373 529 +f 600 536 811 +f 536 563 811 +f 1306 227 1152 +f 522 24 18 +f 523 24 522 +f 865 857 851 +f 2031 2060 1540 +f 767 701 747 +f 618 652 609 +f 652 636 609 +f 573 22 710 +f 642 699 730 +f 1522 1518 2476 +f 500 629 691 +f 690 500 691 +f 691 629 600 +f 780 644 641 +f 579 578 561 +f 131 668 197 +f 197 668 814 +f 789 809 798 +f 622 760 150 +f 621 563 536 +f 673 745 686 +f 673 818 745 +f 818 680 745 +f 680 96 514 +f 2495 2462 1028 +f 1028 583 575 +f 663 794 664 +f 629 761 600 +f 761 757 600 +f 600 757 536 +f 621 696 563 +f 755 736 563 +f 696 755 563 +f 633 736 755 +f 633 647 736 +f 623 686 647 +f 633 623 647 +f 686 623 673 +f 819 680 818 +f 680 819 96 +f 1729 1677 1096 +f 2482 1899 2471 +f 537 536 757 +f 536 537 621 +f 673 819 818 +f 2428 222 230 +f 25 24 523 +f 25 557 24 +f 38 25 19 +f 710 22 272 +f 663 759 794 +f 1120 878 1195 +f 537 696 621 +f 696 633 755 +f 822 2215 2220 +f 97 96 1053 +f 750 784 743 +f 887 905 864 +f 768 784 373 +f 512 513 548 +f 573 664 22 +f 696 715 633 +f 673 521 819 +f 2454 2453 2445 +f 883 887 847 +f 306 812 76 +f 642 528 759 +f 798 809 235 +f 994 792 998 +f 587 626 586 +f 1900 1918 1937 +f 645 652 618 +f 537 786 696 +f 521 593 819 +f 515 19 523 +f 741 749 719 +f 789 326 809 +f 539 581 550 +f 657 777 723 +f 684 713 660 +f 692 712 720 +f 652 666 692 +f 507 761 629 +f 472 507 629 +f 507 757 761 +f 623 633 673 +f 724 521 673 +f 515 516 19 +f 304 675 674 +f 178 778 721 +f 947 1447 2358 +f 626 645 618 +f 586 626 618 +f 784 768 742 +f 753 537 757 +f 537 753 786 +f 724 981 521 +f 521 981 593 +f 979 559 850 +f 637 660 677 +f 787 631 576 +f 141 117 385 +f 809 399 235 +f 641 754 558 +f 542 553 561 +f 742 768 762 +f 444 416 533 +f 528 687 796 +f 813 598 566 +f 1490 1501 1557 +f 753 757 507 +f 786 715 696 +f 633 724 673 +f 2090 2062 2109 +f 646 653 660 +f 660 694 683 +f 677 660 683 +f 1872 839 838 +f 1224 18 30 +f 326 393 809 +f 799 529 373 +f 313 507 472 +f 715 774 633 +f 974 699 841 +f 703 820 816 +f 692 711 676 +f 1014 355 766 +f 875 752 1019 +f 627 646 660 +f 711 692 720 +f 652 692 676 +f 799 373 784 +f 813 566 567 +f 2462 2482 2475 +f 764 644 780 +f 1479 1924 1916 +f 753 738 786 +f 738 607 786 +f 786 607 715 +f 715 524 774 +f 633 774 724 +f 559 979 672 +f 758 798 783 +f 683 694 705 +f 820 703 562 +f 764 687 644 +f 744 743 725 +f 313 753 507 +f 607 524 715 +f 664 801 22 +f 646 627 610 +f 800 820 562 +f 750 769 807 +f 767 747 533 +f 578 586 604 +f 862 593 981 +f 688 2382 1083 +f 306 304 674 +f 738 584 607 +f 168 136 238 +f 773 552 765 +f 2473 2464 2458 +f 773 793 552 +f 626 619 658 +f 1007 1139 1013 +f 562 529 799 +f 744 750 743 +f 659 683 693 +f 677 683 659 +f 313 737 753 +f 753 737 738 +f 607 729 524 +f 27 518 28 +f 553 569 580 +f 657 163 777 +f 580 569 605 +f 789 798 758 +f 769 562 807 +f 820 671 816 +f 638 646 611 +f 1074 598 644 +f 750 799 784 +f 1931 907 898 +f 2483 2487 2461 +f 737 584 738 +f 1439 1438 1431 +f 2098 1213 544 +f 48 578 75 +f 796 631 787 +f 815 732 21 +f 581 588 550 +f 625 636 651 +f 778 1011 810 +f 693 705 725 +f 693 683 705 +f 236 1921 1966 +f 584 729 607 +f 2237 1866 2227 +f 530 541 28 +f 237 739 248 +f 512 530 28 +f 727 778 771 +f 684 727 713 +f 2237 2220 826 +f 542 561 560 +f 528 796 700 +f 808 785 671 +f 739 592 248 +f 895 905 896 +f 740 246 186 +f 272 137 979 +f 770 769 744 +f 712 742 720 +f 1213 2026 544 +f 1888 1235 2438 +f 555 554 2311 +f 737 313 1192 +f 1585 1612 1611 +f 695 721 685 +f 518 17 28 +f 769 770 562 +f 719 749 740 +f 648 669 679 +f 773 657 723 +f 606 637 619 +f 2072 2062 2042 +f 606 619 626 +f 549 569 553 +f 161 638 611 +f 910 917 942 +f 917 1103 942 +f 991 1026 992 +f 979 137 672 +f 785 163 657 +f 710 2488 2472 +f 611 581 119 +f 808 671 820 +f 1820 1900 1870 +f 759 700 591 +f 637 677 619 +f 2494 2490 2463 +f 671 765 816 +f 687 764 780 +f 1019 992 1026 +f 1726 1719 987 +f 713 771 694 +f 51 2355 78 +f 510 526 525 +f 525 526 1249 +f 526 33 1249 +f 2311 554 2335 +f 827 848 840 +f 603 591 649 +f 758 269 740 +f 1595 1612 1586 +f 1694 1048 1699 +f 682 740 186 +f 22 801 603 +f 555 570 554 +f 1053 110 97 +f 615 582 601 +f 814 668 188 +f 725 705 744 +f 528 700 759 +f 640 648 620 +f 703 701 562 +f 886 892 582 +f 631 731 576 +f 1087 1835 1747 +f 882 864 895 +f 956 950 1103 +f 1502 2500 2470 +f 205 190 200 +f 815 878 616 +f 616 878 995 +f 1183 878 815 +f 1601 1827 881 +f 527 535 526 +f 2184 2183 2175 +f 1142 1125 1133 +f 235 338 798 +f 160 339 792 +f 599 92 75 +f 598 1116 566 +f 631 558 731 +f 771 770 744 +f 730 528 642 +f 841 699 642 +f 668 401 188 +f 510 527 526 +f 749 758 740 +f 706 721 695 +f 694 726 705 +f 694 744 726 +f 906 911 905 +f 661 695 161 +f 708 815 616 +f 535 547 33 +f 794 759 591 +f 778 808 790 +f 269 758 783 +f 771 744 694 +f 800 808 820 +f 571 886 582 +f 854 948 1010 +f 906 905 887 +f 625 651 635 +f 2000 1226 534 +f 2140 1504 2016 +f 601 620 615 +f 620 601 640 +f 648 640 669 +f 698 452 439 +f 671 785 657 +f 1561 2356 545 +f 685 653 667 +f 685 727 684 +f 568 616 797 +f 708 732 815 +f 93 229 339 +f 865 851 839 +f 942 1103 950 +f 589 614 125 +f 606 610 627 +f 951 834 873 +f 92 599 625 +f 1878 830 1902 +f 2482 2098 1899 +f 568 708 616 +f 708 551 732 +f 2434 2487 2483 +f 160 964 665 +f 2316 2391 2309 +f 762 758 749 +f 570 614 589 +f 888 897 883 +f 2000 1517 1388 +f 685 721 727 +f 588 610 606 +f 653 685 684 +f 651 650 635 +f 760 1151 6 +f 793 622 150 +f 651 676 650 +f 744 769 750 +f 541 542 560 +f 476 500 690 +f 473 1064 1057 +f 561 578 560 +f 636 625 599 +f 876 995 949 +f 829 856 846 +f 682 704 740 +f 791 790 770 +f 2466 2500 2460 +f 579 587 586 +f 1352 1208 1095 +f 1684 1479 1916 +f 604 609 636 +f 751 721 706 +f 810 608 782 +f 672 603 649 +f 475 447 476 +f 794 591 801 +f 682 186 650 +f 808 800 790 +f 644 598 813 +f 704 719 740 +f 1011 608 810 +f 1192 584 737 +f 687 780 796 +f 2337 474 2312 +f 638 667 646 +f 706 1186 728 +f 733 575 568 +f 595 551 708 +f 595 540 551 +f 1308 501 1852 +f 665 339 160 +f 527 2447 535 +f 558 9 731 +f 723 793 773 +f 660 713 694 +f 693 725 666 +f 562 767 529 +f 550 538 531 +f 2267 2287 2233 +f 996 964 160 +f 2068 2470 2466 +f 704 711 719 +f 741 762 749 +f 605 606 626 +f 548 542 530 +f 995 878 709 +f 1898 1684 1916 +f 778 791 771 +f 782 163 785 +f 789 758 762 +f 857 883 847 +f 733 970 1028 +f 838 829 825 +f 2447 511 535 +f 22 603 137 +f 705 726 744 +f 605 587 580 +f 512 548 530 +f 743 784 742 +f 790 800 770 +f 778 810 808 +f 1014 998 355 +f 708 568 595 +f 656 697 551 +f 540 656 551 +f 143 125 614 +f 1000 1020 983 +f 778 178 1011 +f 676 704 682 +f 637 627 660 +f 606 627 637 +f 701 552 481 +f 808 810 785 +f 590 570 555 +f 716 595 568 +f 2355 2335 554 +f 912 1729 911 +f 1076 1456 1546 +f 697 68 492 +f 676 711 704 +f 839 851 838 +f 1028 575 733 +f 1020 844 982 +f 716 568 575 +f 844 781 982 +f 1238 2156 2034 +f 553 580 561 +f 580 579 561 +f 452 461 453 +f 560 578 48 +f 564 540 595 +f 632 656 540 +f 564 632 540 +f 75 578 599 +f 518 27 535 +f 511 518 535 +f 783 798 788 +f 642 759 663 +f 720 742 741 +f 605 626 587 +f 580 587 579 +f 725 712 666 +f 562 701 767 +f 1729 923 911 +f 712 743 742 +f 619 677 658 +f 161 695 654 +f 770 800 562 +f 2084 2489 2472 +f 575 559 716 +f 716 564 595 +f 654 695 685 +f 843 855 2064 +f 34 731 9 +f 527 510 1973 +f 723 622 793 +f 992 1726 987 +f 693 666 652 +f 2472 853 573 +f 624 159 148 +f 671 657 773 +f 681 188 498 +f 797 970 733 +f 565 656 632 +f 565 697 656 +f 565 731 697 +f 1949 951 920 +f 85 111 84 +f 662 200 190 +f 44 324 754 +f 33 547 40 +f 658 693 652 +f 658 652 645 +f 664 794 801 +f 666 712 692 +f 639 648 662 +f 611 646 610 +f 850 559 575 +f 1447 2490 1106 +f 1972 1955 1935 +f 582 615 590 +f 66 581 539 +f 780 641 631 +f 796 780 631 +f 1049 1192 83 +f 1348 13 1519 +f 799 807 562 +f 581 611 588 +f 687 795 644 +f 663 8 642 +f 1936 1972 1935 +f 650 676 682 +f 615 630 590 +f 730 795 687 +f 742 762 741 +f 548 553 542 +f 1048 1692 1074 +f 658 659 693 +f 37 52 30 +f 611 610 588 +f 649 632 564 +f 565 576 731 +f 2138 922 1058 +f 1204 854 965 +f 725 743 712 +f 644 813 641 +f 660 653 684 +f 771 791 770 +f 644 795 1074 +f 469 480 681 +f 559 672 564 +f 716 559 564 +f 672 649 564 +f 2161 1378 2171 +f 474 475 476 +f 816 765 701 +f 765 552 701 +f 513 538 548 +f 754 324 107 +f 609 586 618 +f 25 523 19 +f 677 659 658 +f 689 452 698 +f 1334 1115 1353 +f 700 565 632 +f 700 576 565 +f 481 552 793 +f 763 901 2458 +f 550 549 538 +f 781 964 996 +f 1596 1634 1595 +f 198 916 1124 +f 198 1124 341 +f 842 973 1025 +f 842 1025 836 +f 1009 1024 934 +f 573 710 2472 +f 1100 971 1002 +f 1501 1081 1557 +f 1225 1219 955 +f 413 2138 284 +f 955 1630 522 +f 341 1124 301 +f 2333 2376 2350 +f 1107 218 284 +f 398 925 1513 +f 1513 1442 1495 +f 1935 1455 1744 +f 1723 1935 1744 +f 825 1872 838 +f 1495 1442 1496 +f 963 1024 1009 +f 1511 1514 966 +f 1775 1729 912 +f 688 262 1067 +f 714 1007 1512 +f 919 1732 914 +f 2319 2331 2304 +f 2400 2407 2391 +f 1674 2164 1780 +f 843 927 899 +f 1660 988 1188 +f 1067 262 1640 +f 1381 1109 1483 +f 1437 1381 1483 +f 2495 1010 948 +f 1514 1289 1313 +f 899 374 961 +f 1438 1430 1422 +f 1634 1095 1632 +f 2487 973 2461 +f 1003 499 874 +f 849 848 827 +f 1430 1462 1453 +f 2496 2084 2471 +f 909 10 980 +f 730 927 835 +f 2031 1540 1536 +f 831 849 2178 +f 881 834 951 +f 1841 1722 1803 +f 1005 670 1020 +f 1021 670 1005 +f 1869 2059 2467 +f 903 902 1939 +f 2476 2502 1651 +f 853 8 573 +f 1850 831 2178 +f 934 746 247 +f 934 65 746 +f 301 285 1077 +f 968 944 977 +f 970 2495 1028 +f 974 2465 374 +f 899 927 374 +f 1882 1898 1916 +f 1613 1634 1596 +f 909 833 1396 +f 2492 247 1003 +f 919 914 1931 +f 1459 1299 1458 +f 1634 1632 1633 +f 844 670 228 +f 2494 2497 2467 +f 901 973 2487 +f 228 1772 734 +f 1701 1709 1666 +f 963 574 1024 +f 847 864 856 +f 1730 1736 2239 +f 870 859 848 +f 2074 2111 2103 +f 1140 1590 1483 +f 927 730 974 +f 2103 978 2074 +f 756 1745 1718 +f 848 859 840 +f 1296 1482 1320 +f 2331 51 66 +f 1067 988 962 +f 1396 833 1445 +f 1001 1005 1000 +f 901 1009 973 +f 1099 1077 817 +f 933 944 936 +f 952 958 1828 +f 988 1660 986 +f 833 1067 1445 +f 1067 1640 988 +f 218 413 284 +f 1843 180 347 +f 1846 1708 1798 +f 2469 2477 855 +f 1006 1021 1005 +f 381 382 250 +f 2369 828 531 +f 968 977 1001 +f 2460 1949 779 +f 1194 1441 1115 +f 1001 1000 968 +f 756 678 1745 +f 963 1009 901 +f 2471 2084 2472 +f 841 642 8 +f 982 991 1027 +f 670 844 1020 +f 1289 1514 945 +f 869 904 890 +f 1161 1115 1639 +f 823 2178 849 +f 746 12 499 +f 263 428 2366 +f 1685 1075 1692 +f 1002 926 806 +f 1799 1755 216 +f 944 968 993 +f 943 944 993 +f 31 38 19 +f 531 828 550 +f 1501 1078 1081 +f 1921 1149 431 +f 936 943 932 +f 1660 1489 1412 +f 301 980 285 +f 903 918 902 +f 869 890 868 +f 890 903 867 +f 1003 746 499 +f 951 1949 2500 +f 990 841 853 +f 1595 1634 1611 +f 374 927 974 +f 836 1025 247 +f 1653 1652 1638 +f 1303 1545 1142 +f 1616 1631 1638 +f 1629 1546 1628 +f 936 932 913 +f 513 506 531 +f 868 890 867 +f 2330 2369 2353 +f 924 918 914 +f 907 914 904 +f 1258 1421 1267 +f 301 939 980 +f 1472 1482 1296 +f 868 867 859 +f 472 491 313 +f 272 519 2488 +f 1471 1472 1296 +f 1025 934 247 +f 1634 1633 1611 +f 2176 1847 2177 +f 1310 1289 806 +f 924 933 918 +f 1969 1968 902 +f 2107 2128 2118 +f 1428 1436 1287 +f 1139 1564 1617 +f 2378 572 2384 +f 853 841 8 +f 2501 961 2465 +f 1221 1240 1408 +f 1069 1578 1627 +f 1006 1005 1001 +f 1617 1564 1578 +f 828 539 550 +f 1791 2168 2160 +f 1829 1718 1739 +f 1968 1939 902 +f 756 1718 665 +f 1998 2000 1388 +f 2451 545 2356 +f 178 997 1011 +f 1275 325 1270 +f 1709 872 1666 +f 2176 1959 1847 +f 944 943 936 +f 2424 518 511 +f 1445 1067 962 +f 2007 952 1828 +f 2052 2061 2081 +f 828 2303 539 +f 835 1699 1048 +f 1709 1706 872 +f 885 574 963 +f 1318 1296 1320 +f 859 867 1902 +f 1452 1448 1421 +f 943 993 976 +f 993 1000 983 +f 854 1010 876 +f 988 986 962 +f 2031 2052 2081 +f 924 1732 1828 +f 965 949 1060 +f 781 228 734 +f 1718 1765 665 +f 943 976 932 +f 1680 1794 1783 +f 1448 1471 1276 +f 1276 1267 1421 +f 1931 914 907 +f 991 781 996 +f 1276 1421 1448 +f 10 909 1396 +f 831 860 849 +f 1523 1762 1774 +f 924 1828 937 +f 307 994 1014 +f 946 963 901 +f 978 2103 977 +f 977 1006 1001 +f 1007 1161 1639 +f 1639 1294 1437 +f 885 1032 574 +f 1294 1381 1437 +f 733 568 797 +f 792 229 1112 +f 119 581 108 +f 843 835 927 +f 1889 860 831 +f 2211 2216 2204 +f 2400 2431 2422 +f 2103 1006 977 +f 840 1902 830 +f 827 840 830 +f 827 830 822 +f 1003 874 2492 +f 1432 1439 1431 +f 781 734 964 +f 1937 1936 1723 +f 918 913 902 +f 958 977 944 +f 1850 2178 2177 +f 1005 1020 1000 +f 991 996 307 +f 1396 1445 340 +f 2179 1763 889 +f 939 909 980 +f 1828 958 937 +f 978 977 958 +f 1590 1571 1563 +f 779 1949 920 +f 1551 1362 1573 +f 2103 2142 1006 +f 920 885 963 +f 946 920 963 +f 1584 1616 1583 +f 1453 1472 1452 +f 1647 1617 1578 +f 1578 1564 1627 +f 1628 938 1069 +f 869 868 859 +f 993 983 976 +f 912 1762 1775 +f 752 751 706 +f 1628 1546 938 +f 844 228 781 +f 840 859 1902 +f 898 907 904 +f 1025 973 1009 +f 663 664 573 +f 763 946 901 +f 898 904 869 +f 2172 889 1763 +f 1128 926 971 +f 860 848 849 +f 904 903 890 +f 2486 2459 2479 +f 577 782 608 +f 933 936 918 +f 2177 1847 1851 +f 665 1765 339 +f 937 958 944 +f 894 981 724 +f 968 1000 993 +f 2192 2195 2205 +f 1652 1099 817 +f 997 608 1011 +f 997 577 608 +f 577 163 782 +f 1112 998 792 +f 2177 1851 1850 +f 1257 1421 1258 +f 951 873 920 +f 822 830 2215 +f 1899 2496 2471 +f 1773 1668 1558 +f 904 914 903 +f 932 1671 913 +f 873 885 920 +f 1013 1617 1647 +f 873 1032 885 +f 894 862 981 +f 2469 855 961 +f 913 1671 1969 +f 2477 2064 855 +f 918 936 913 +f 860 870 848 +f 937 944 933 +f 1501 1013 1647 +f 824 178 751 +f 824 997 178 +f 824 577 997 +f 643 163 577 +f 863 856 882 +f 2128 2153 2134 +f 722 774 880 +f 722 894 774 +f 864 905 895 +f 850 575 583 +f 914 918 903 +f 924 937 933 +f 1501 717 1013 +f 1587 1324 928 +f 717 1512 1013 +f 602 577 824 +f 766 643 577 +f 894 709 862 +f 709 878 862 +f 976 975 932 +f 1324 1596 928 +f 880 524 1060 +f 2434 2459 2499 +f 1324 1613 1596 +f 752 824 751 +f 602 766 577 +f 1014 602 594 +f 1387 1226 2152 +f 2153 1387 2152 +f 669 930 950 +f 1710 1694 1699 +f 768 326 762 +f 582 892 601 +f 974 990 2465 +f 624 116 625 +f 835 795 730 +f 2458 2484 763 +f 989 602 824 +f 2064 2477 1710 +f 976 983 975 +f 949 722 880 +f 996 160 994 +f 2305 863 556 +f 556 863 886 +f 601 910 640 +f 2264 825 829 +f 989 824 752 +f 856 864 882 +f 1595 1586 2381 +f 1627 1629 1628 +f 2174 2180 2173 +f 2128 2134 2118 +f 137 272 22 +f 949 880 1060 +f 995 894 722 +f 894 995 709 +f 894 724 774 +f 886 895 892 +f 640 910 930 +f 871 870 860 +f 846 856 863 +f 1026 875 1019 +f 838 851 829 +f 1024 1171 934 +f 36 189 205 +f 863 882 886 +f 886 882 895 +f 875 1026 594 +f 52 1459 1269 +f 896 917 910 +f 1025 1009 934 +f 949 995 722 +f 2152 1226 1636 +f 895 896 892 +f 892 910 601 +f 942 950 930 +f 875 989 752 +f 594 602 989 +f 766 355 643 +f 355 260 643 +f 905 917 896 +f 965 1060 1162 +f 892 896 910 +f 1101 1052 1042 +f 1029 1031 834 +f 1101 1133 1118 +f 342 357 376 +f 516 515 2454 +f 1656 2494 2467 +f 1056 1303 1133 +f 1120 1130 862 +f 69 342 376 +f 1055 1056 1133 +f 499 69 165 +f 85 101 111 +f 1031 1032 834 +f 200 679 1166 +f 1031 1042 1032 +f 1171 65 934 +f 1822 1204 1177 +f 1096 956 1103 +f 514 96 97 +f 956 1145 1144 +f 1185 1166 1144 +f 1145 1185 1144 +f 1185 200 1166 +f 375 132 1041 +f 1153 1202 305 +f 32 1244 1249 +f 1096 1087 956 +f 554 78 2355 +f 1191 138 110 +f 65 35 432 +f 1087 1110 956 +f 1110 1146 956 +f 956 1146 1145 +f 1146 1156 1145 +f 1145 1156 1185 +f 950 956 1144 +f 2481 2495 948 +f 1156 1193 1185 +f 1050 1047 1051 +f 239 151 107 +f 1185 1193 36 +f 1747 1110 1087 +f 1134 1146 1110 +f 1146 1157 1156 +f 1156 1157 1193 +f 1041 1045 1034 +f 1397 1134 1110 +f 1157 1146 1134 +f 1157 1175 1193 +f 1193 199 36 +f 1090 1035 1196 +f 1456 1150 1051 +f 1175 199 1193 +f 1186 695 199 +f 1186 199 1175 +f 1175 1157 1134 +f 728 1186 1175 +f 197 760 6 +f 1130 593 862 +f 1167 1109 182 +f 1194 1115 1161 +f 2140 1928 1504 +f 921 922 2138 +f 1147 1134 1397 +f 1719 1147 1397 +f 1147 1175 1134 +f 1175 1147 728 +f 341 1654 1208 +f 754 151 9 +f 284 2138 1058 +f 1188 1557 1660 +f 1191 110 1053 +f 916 284 1189 +f 284 1058 1189 +f 2094 1465 2127 +f 1726 1019 1147 +f 1147 1019 728 +f 593 1130 96 +f 239 305 1038 +f 1036 1131 315 +f 397 1131 1120 +f 1053 96 1130 +f 2467 2485 1869 +f 517 1089 421 +f 834 1827 1029 +f 419 1047 1117 +f 1034 433 1306 +f 2239 1862 1730 +f 1453 1462 1472 +f 1408 1422 1399 +f 471 23 1111 +f 1205 1150 1456 +f 1205 1040 1150 +f 1131 1036 293 +f 293 1068 1044 +f 375 1041 138 +f 1205 1140 1046 +f 1040 1205 1046 +f 1140 1167 1046 +f 1104 1049 83 +f 1052 1085 1032 +f 1044 1068 1191 +f 1167 1483 1109 +f 208 1084 1035 +f 1040 132 375 +f 1834 20 3 +f 1050 1051 1070 +f 1133 1125 1174 +f 11 1440 1401 +f 420 208 1071 +f 1135 1079 1094 +f 1086 1101 1118 +f 1029 1030 1031 +f 1200 1061 294 +f 1191 1068 138 +f 1171 1141 65 +f 1141 1172 65 +f 1172 35 65 +f 1172 404 35 +f 404 99 35 +f 221 1104 1063 +f 802 398 1083 +f 20 1089 3 +f 2064 1699 835 +f 1042 1052 1032 +f 1433 1261 1432 +f 1323 2338 155 +f 1076 1205 1456 +f 1088 1402 1056 +f 1150 348 1070 +f 1200 1089 20 +f 1097 1162 100 +f 1032 873 834 +f 21 471 1111 +f 294 1097 1104 +f 1072 100 584 +f 1151 760 622 +f 132 1045 1041 +f 1050 1070 1135 +f 1088 1039 940 +f 650 159 635 +f 100 1170 729 +f 729 584 100 +f 1103 931 1096 +f 925 1443 1513 +f 138 1102 110 +f 1034 1306 1152 +f 1071 1035 1090 +f 100 1072 1097 +f 23 1158 315 +f 1068 375 138 +f 1586 1612 1585 +f 1819 1030 1029 +f 1041 1034 1102 +f 232 375 1068 +f 348 1079 1070 +f 1061 1097 294 +f 1513 1443 1442 +f 1200 294 1119 +f 376 1050 1062 +f 1094 1036 315 +f 1200 1119 1089 +f 1111 1183 21 +f 1044 1191 1053 +f 698 295 689 +f 1079 232 1036 +f 404 1117 99 +f 1495 1496 717 +f 1119 294 98 +f 3 1089 517 +f 1132 1063 83 +f 1132 83 175 +f 132 1046 182 +f 1111 1195 1183 +f 1131 1044 1037 +f 127 402 1804 +f 219 1272 1047 +f 1697 1135 1094 +f 2140 1854 2117 +f 1111 397 1195 +f 1177 1162 1097 +f 1061 1177 1097 +f 717 1509 714 +f 2 1300 433 +f 462 290 461 +f 98 294 221 +f 294 1104 221 +f 714 1161 1007 +f 1073 1152 1143 +f 1697 1094 1360 +f 1223 1423 1218 +f 836 2479 842 +f 1097 1072 1049 +f 348 1040 375 +f 3 517 316 +f 180 1061 1201 +f 348 375 232 +f 1432 1431 1415 +f 220 1513 1495 +f 1104 1097 1049 +f 306 674 596 +f 777 455 723 +f 2170 2151 1641 +f 1047 419 219 +f 1102 1034 1073 +f 1073 1034 1152 +f 1035 1054 1196 +f 1177 1204 1162 +f 746 65 12 +f 751 178 721 +f 1054 517 421 +f 1051 1150 1070 +f 1102 1073 110 +f 998 1136 355 +f 567 566 1163 +f 1111 315 397 +f 1048 1074 835 +f 1158 1094 315 +f 1374 1107 1252 +f 1112 1136 998 +f 472 629 500 +f 355 1136 260 +f 260 118 43 +f 1104 83 1063 +f 376 357 1050 +f 1463 1142 1545 +f 1036 232 293 +f 1030 1042 1031 +f 1079 348 232 +f 221 1063 1132 +f 1094 1079 1036 +f 1076 1629 1205 +f 1136 1197 260 +f 260 1197 118 +f 1204 965 1162 +f 293 232 1068 +f 1590 1205 1629 +f 1205 1590 1140 +f 250 382 392 +f 1296 1318 1311 +f 347 1201 20 +f 1201 1200 20 +f 132 182 1045 +f 1101 1086 1052 +f 1033 1039 1055 +f 138 1041 1102 +f 970 1010 2495 +f 455 777 43 +f 1992 1948 2023 +f 20 1834 347 +f 1072 584 1049 +f 584 1192 1049 +f 182 2 1045 +f 1163 324 44 +f 1360 1094 1158 +f 1450 1360 1158 +f 1091 1112 229 +f 509 723 455 +f 207 509 455 +f 1251 1257 1266 +f 1488 1489 1547 +f 2157 1541 1875 +f 305 107 324 +f 1045 2 433 +f 1070 1079 1135 +f 1136 1168 1197 +f 1197 359 118 +f 118 359 43 +f 359 356 43 +f 356 455 43 +f 356 207 455 +f 1240 1422 1408 +f 1163 1153 324 +f 1201 1061 1200 +f 1052 1086 1085 +f 1024 1141 1171 +f 1112 1105 1136 +f 1050 1135 1062 +f 1105 1168 1136 +f 1168 1178 1197 +f 1197 1178 359 +f 1173 404 1172 +f 465 356 359 +f 1174 1125 240 +f 1240 1431 1422 +f 1098 1113 1105 +f 1112 1098 1105 +f 1105 1178 1168 +f 1178 465 359 +f 1091 1098 1112 +f 1133 1174 1118 +f 98 221 1059 +f 487 1132 175 +f 980 1017 285 +f 465 207 356 +f 180 1201 347 +f 1060 524 1170 +f 445 127 316 +f 1431 1438 1422 +f 498 469 681 +f 940 1807 1759 +f 381 250 1290 +f 1113 1122 1105 +f 1105 1122 1178 +f 1151 509 207 +f 1236 2035 525 +f 1131 293 1044 +f 346 207 465 +f 346 1151 207 +f 1822 1796 1204 +f 1143 204 97 +f 123 1128 971 +f 2153 2152 2134 +f 126 1151 346 +f 517 445 316 +f 1450 1158 23 +f 1458 1462 1430 +f 1129 152 1182 +f 1122 1159 1178 +f 1178 1198 465 +f 79 346 465 +f 126 1155 1151 +f 1151 1155 6 +f 295 1129 689 +f 1073 1143 97 +f 1098 1123 1113 +f 1113 1123 1122 +f 1123 1169 1122 +f 1178 1159 1198 +f 1198 79 465 +f 392 383 152 +f 1822 1061 180 +f 116 92 625 +f 421 1089 1119 +f 1129 295 152 +f 110 1073 97 +f 1173 1172 1141 +f 1122 1169 1159 +f 79 126 346 +f 1155 181 6 +f 971 926 1002 +f 295 1043 152 +f 1039 1088 1056 +f 1428 1266 1436 +f 404 419 1117 +f 836 879 2479 +f 2464 2476 2458 +f 1198 317 79 +f 1124 939 301 +f 44 754 567 +f 1039 1056 1055 +f 1439 1459 1458 +f 1660 1412 986 +f 1169 1160 1159 +f 179 1155 126 +f 1155 131 181 +f 1061 1822 1177 +f 1153 305 324 +f 175 314 327 +f 1160 1187 1159 +f 1159 1187 1198 +f 1198 1187 317 +f 79 179 126 +f 1043 250 392 +f 152 1043 392 +f 96 819 593 +f 1123 1127 1169 +f 317 179 79 +f 1057 1155 179 +f 1155 391 131 +f 131 391 668 +f 2381 1586 1585 +f 12 69 499 +f 262 398 1640 +f 2107 2118 2060 +f 2130 2094 2002 +f 1187 249 317 +f 1155 1057 391 +f 1290 439 1265 +f 305 239 107 +f 1127 1160 1169 +f 317 473 179 +f 473 1057 179 +f 83 1192 314 +f 1043 1290 250 +f 1807 940 1030 +f 517 1084 445 +f 1057 1164 391 +f 2492 2480 2493 +f 163 643 43 +f 1056 1545 1303 +f 1069 1655 1023 +f 249 473 317 +f 1162 1060 1170 +f 1086 1118 1154 +f 82 68 16 +f 1989 1990 1536 +f 1633 1632 1611 +f 1487 2372 1305 +f 1494 1069 1023 +f 1137 1160 1127 +f 669 1166 679 +f 390 1285 426 +f 1955 1972 1971 +f 1219 1223 2437 +f 1254 1261 1223 +f 1319 1545 1056 +f 1320 1328 2443 +f 1261 1433 1223 +f 1219 1254 1223 +f 254 222 2428 +f 1237 1290 1265 +f 1284 1273 1263 +f 1277 1291 1301 +f 1314 102 1301 +f 1280 363 377 +f 1313 1353 1514 +f 468 451 439 +f 1918 1964 1956 +f 2026 29 2140 +f 1354 381 1279 +f 1224 30 1254 +f 147 158 173 +f 1247 1253 274 +f 1271 380 334 +f 2043 2072 2042 +f 274 300 267 +f 1356 1392 211 +f 13 240 1142 +f 1382 1330 1392 +f 1312 1323 155 +f 240 1125 1142 +f 2358 1573 1362 +f 1236 1249 1244 +f 1272 219 1348 +f 1271 1274 380 +f 191 2034 1982 +f 1992 2052 1990 +f 462 452 689 +f 2262 2286 2261 +f 183 489 1642 +f 2485 2480 1869 +f 84 111 1323 +f 1190 353 1354 +f 446 434 435 +f 1336 171 1341 +f 2021 430 2059 +f 862 878 1120 +f 1263 1273 1248 +f 1966 1921 2144 +f 1312 84 1323 +f 240 13 1348 +f 1359 1274 1271 +f 1392 1330 1247 +f 1520 1333 11 +f 1368 1253 1247 +f 1279 1285 1190 +f 2465 990 2489 +f 1272 1519 805 +f 1369 1272 805 +f 1317 95 1344 +f 1242 1248 1234 +f 1368 242 1363 +f 274 1262 1386 +f 532 597 1886 +f 2117 2026 2140 +f 1392 1247 274 +f 2162 508 985 +f 1964 1469 1965 +f 1315 104 1331 +f 1392 1356 1382 +f 128 1342 1336 +f 1285 427 426 +f 1219 1224 1254 +f 1320 1322 1321 +f 1320 1321 1328 +f 153 2443 1328 +f 1321 153 1328 +f 1235 1244 1243 +f 1225 1224 1219 +f 1359 353 1190 +f 1312 1473 1458 +f 1336 1342 147 +f 305 1333 1038 +f 1336 147 171 +f 516 31 19 +f 2479 2461 842 +f 1237 1265 427 +f 1263 1278 1284 +f 881 1827 834 +f 1237 427 1285 +f 1299 1312 1458 +f 1190 1285 1274 +f 1363 286 1253 +f 2330 2303 828 +f 427 442 426 +f 2493 2463 2492 +f 1285 380 1274 +f 522 18 1225 +f 2471 2472 2488 +f 2338 154 1321 +f 1423 1415 1218 +f 1225 18 1224 +f 1253 286 1262 +f 286 353 1359 +f 171 1368 1383 +f 1273 54 1234 +f 1973 2447 527 +f 1322 155 1321 +f 1203 1369 1413 +f 1307 363 1298 +f 1364 1375 1329 +f 1329 227 1306 +f 296 1298 1343 +f 947 2499 1447 +f 1203 1047 1272 +f 1098 1748 1123 +f 1519 1272 1348 +f 1277 70 1273 +f 1282 1337 1361 +f 286 302 353 +f 103 104 1315 +f 1377 435 434 +f 1449 1261 1345 +f 926 1310 806 +f 1263 1248 1242 +f 985 508 597 +f 1415 1222 1218 +f 88 1325 104 +f 170 111 156 +f 1384 1282 1361 +f 274 1253 1262 +f 1371 1317 1344 +f 1371 1366 1337 +f 1345 1459 1449 +f 171 1383 1341 +f 2438 1235 1227 +f 2134 1582 2118 +f 428 1260 1379 +f 1336 1341 1325 +f 1235 1242 1227 +f 1228 1687 2284 +f 1854 2140 2016 +f 1866 1887 1873 +f 1343 1298 1370 +f 1384 1361 2440 +f 171 242 1368 +f 1344 1309 1366 +f 1371 1344 1366 +f 1280 1377 1293 +f 200 1185 205 +f 1330 1383 1368 +f 1255 1264 1263 +f 543 1367 1876 +f 1343 1370 1260 +f 1293 1326 1370 +f 2440 1361 1302 +f 1282 1384 2406 +f 271 1337 1282 +f 170 2338 1323 +f 1528 1503 2470 +f 515 1347 2453 +f 1997 1705 1998 +f 2285 1228 2284 +f 1229 1250 1228 +f 1330 1368 1247 +f 1919 1619 2045 +f 1344 1364 1335 +f 1222 1240 1221 +f 1212 858 1741 +f 2388 1222 1221 +f 1528 2470 2068 +f 501 1308 2171 +f 1295 1311 1487 +f 2116 1619 1655 +f 1220 1229 1228 +f 8 663 573 +f 1343 1260 428 +f 1337 1366 1361 +f 1298 1280 1293 +f 1269 1345 1261 +f 1279 381 1290 +f 1230 1229 1220 +f 1230 1245 1229 +f 1245 1250 1229 +f 1227 1234 31 +f 1302 1361 1350 +f 1245 1266 1428 +f 1992 2023 2052 +f 2482 2471 2475 +f 452 462 461 +f 271 1282 1275 +f 1991 1989 1934 +f 1366 1309 1350 +f 1344 1335 1309 +f 730 699 974 +f 1374 1252 1208 +f 597 508 1912 +f 1363 1253 1368 +f 1386 1271 300 +f 1211 1218 1222 +f 1376 1377 434 +f 2399 2437 1211 +f 1284 1291 1277 +f 1230 1251 1245 +f 1251 1266 1245 +f 1317 1371 1337 +f 1288 1286 1095 +f 1095 1286 1352 +f 1241 1208 1352 +f 1241 1374 1208 +f 1284 1278 1291 +f 211 1392 267 +f 1344 1375 1364 +f 929 583 1028 +f 1361 1366 1350 +f 1115 1294 1639 +f 1291 103 1301 +f 1220 1231 1230 +f 1231 1251 1230 +f 1234 1248 1273 +f 1255 55 1264 +f 1360 1450 1702 +f 363 1280 1298 +f 1369 1203 1272 +f 1415 1240 1222 +f 1216 1231 1220 +f 1243 1263 1235 +f 1375 227 1329 +f 1264 1278 1263 +f 855 899 961 +f 1286 1241 1352 +f 2081 2128 2107 +f 1223 1433 1423 +f 1473 1312 155 +f 154 153 1321 +f 1377 1376 1293 +f 1392 274 267 +f 334 300 1271 +f 1955 1991 1934 +f 1613 1327 1288 +f 1327 1286 1288 +f 1349 1374 1241 +f 2370 2025 2367 +f 1315 1331 133 +f 434 446 1256 +f 1232 1251 1231 +f 1243 1244 1255 +f 1286 1304 1241 +f 1349 1107 1374 +f 1359 1271 1386 +f 1227 516 2431 +f 219 240 1348 +f 1270 271 1275 +f 1255 1263 1243 +f 2026 1926 29 +f 1683 2157 1212 +f 1326 1293 1376 +f 1255 32 55 +f 104 1325 1341 +f 519 2462 2475 +f 2154 2161 2137 +f 1376 434 1246 +f 1246 434 1256 +f 1257 1251 1232 +f 1262 1359 1386 +f 2195 2192 2186 +f 1308 534 1226 +f 2026 2117 544 +f 1327 1613 1324 +f 1327 1326 1286 +f 1286 1326 1304 +f 104 1341 1331 +f 774 524 880 +f 837 1517 534 +f 1127 1123 1567 +f 1279 1237 1285 +f 1297 1381 1294 +f 1217 1232 1216 +f 1142 1519 13 +f 1436 1267 1287 +f 1324 1372 1327 +f 1304 1246 1241 +f 1246 1349 1241 +f 1246 1373 1349 +f 286 1359 1262 +f 1382 1383 1330 +f 1284 1277 1273 +f 489 1998 1799 +f 1675 1116 1075 +f 106 1317 1337 +f 1311 1295 1281 +f 1292 1364 1329 +f 1335 1364 1292 +f 1334 1294 1115 +f 1334 1297 1294 +f 1300 1381 1297 +f 973 842 2461 +f 1217 1239 1232 +f 1232 1239 1257 +f 1258 1267 1436 +f 1359 1190 1274 +f 1862 1405 1877 +f 1372 1339 1327 +f 1339 1326 1327 +f 1373 1351 1349 +f 1276 1311 1281 +f 1256 2386 1351 +f 2 1109 1300 +f 482 1731 520 +f 803 1604 2022 +f 1223 1218 1211 +f 1341 1383 1382 +f 1298 1293 1370 +f 1190 1354 1279 +f 1324 2398 1372 +f 1714 1700 2173 +f 183 2000 489 +f 1701 1666 192 +f 1227 1242 1234 +f 1332 1289 1310 +f 1517 2005 2130 +f 1331 1341 1382 +f 525 1249 1236 +f 23 1268 1450 +f 1264 1291 1278 +f 1281 1287 1267 +f 1295 1305 1287 +f 1281 1295 1287 +f 1487 1305 1295 +f 1605 2097 2058 +f 1326 1376 1304 +f 1304 1376 1246 +f 1316 1919 1984 +f 2500 1949 2460 +f 1332 1313 1289 +f 2189 2181 2177 +f 1335 1334 1353 +f 1292 1297 1334 +f 1428 1250 1245 +f 969 958 952 +f 1217 1233 1239 +f 1233 1257 1239 +f 1876 1367 1338 +f 1379 1260 1372 +f 1372 1260 1339 +f 1128 1302 1310 +f 1310 1302 1332 +f 1335 1353 1313 +f 1292 1334 1335 +f 1297 1329 1300 +f 1279 1290 1237 +f 1301 103 1314 +f 70 1301 102 +f 23 1333 1268 +f 380 1285 390 +f 772 325 1275 +f 1314 103 1315 +f 2473 2458 2487 +f 1276 1281 1267 +f 1344 95 1375 +f 2053 1771 1572 +f 1246 1256 1373 +f 1373 1256 1351 +f 1340 1302 1128 +f 1350 1313 1332 +f 1329 1297 1292 +f 2434 2473 2487 +f 106 1337 271 +f 23 471 1333 +f 622 723 509 +f 1388 1517 2127 +f 1991 1990 1989 +f 183 1636 1226 +f 2133 1605 2151 +f 1260 1370 1339 +f 1339 1370 1326 +f 867 1894 1902 +f 390 426 412 +f 1235 1263 1242 +f 1399 1422 1233 +f 305 11 1333 +f 1300 1329 1306 +f 1302 1350 1332 +f 1350 1309 1313 +f 1309 1335 1313 +f 2470 2102 1502 +f 1787 1531 1599 +f 1724 1725 1691 +f 1827 1601 1927 +f 1678 1358 1476 +f 1823 1812 1846 +f 1805 1824 1708 +f 1746 1676 1797 +f 325 2395 429 +f 1835 1677 1826 +f 1507 1790 1722 +f 1526 1672 858 +f 158 147 1342 +f 1462 1473 1322 +f 1474 1414 1565 +f 1761 1900 1877 +f 940 1759 1008 +f 1565 1015 1008 +f 1924 1533 1933 +f 1878 826 830 +f 1565 1414 1015 +f 1402 1088 1008 +f 1538 1532 1651 +f 1015 1552 1008 +f 1538 1591 1474 +f 1532 1538 1474 +f 1474 1591 1414 +f 1484 1402 1008 +f 1552 1484 1008 +f 1414 1460 1015 +f 1015 1460 1552 +f 806 1289 945 +f 1597 1538 1659 +f 1484 1319 1402 +f 1056 1402 1319 +f 1538 1597 1591 +f 1591 960 1414 +f 1414 960 1460 +f 1925 1466 1455 +f 1552 1400 1484 +f 1484 1400 1319 +f 1400 113 1319 +f 1597 1580 1591 +f 1460 1400 1552 +f 1514 1441 966 +f 1597 1659 1409 +f 1657 113 1400 +f 1460 1657 1400 +f 1288 1095 1634 +f 1551 1597 1409 +f 1580 1598 1591 +f 1591 1598 960 +f 1536 1990 2031 +f 960 1657 1460 +f 1809 1746 1797 +f 1423 1433 1432 +f 2478 1362 1409 +f 1463 1545 113 +f 1657 1463 113 +f 1457 1287 1305 +f 1682 1716 1746 +f 1434 1761 1885 +f 1013 1139 1617 +f 2379 1362 2478 +f 1420 1597 1551 +f 1420 1580 1597 +f 1664 1808 1712 +f 2256 2250 2231 +f 1362 1551 1409 +f 2196 2214 2213 +f 1691 1725 1777 +f 1626 192 1666 +f 1534 1574 2058 +f 1574 1600 1605 +f 1600 1606 1605 +f 1606 1641 1605 +f 1573 1420 1551 +f 1657 1485 1463 +f 678 1806 1742 +f 1534 1553 1574 +f 1574 1575 1600 +f 1810 2170 585 +f 1623 1641 1606 +f 1407 1657 960 +f 1598 1407 960 +f 1485 1142 1463 +f 1716 1581 1676 +f 1738 1743 1733 +f 843 2064 835 +f 1539 1575 1574 +f 1553 1539 1574 +f 1575 1592 1600 +f 1592 1624 1606 +f 1600 1592 1606 +f 1642 585 1641 +f 1623 1642 1641 +f 1485 164 1142 +f 1738 1516 1743 +f 1809 1720 1798 +f 1533 1535 1534 +f 1592 1607 1624 +f 1624 1623 1606 +f 1163 566 1116 +f 1407 1485 1657 +f 1432 1449 1439 +f 1100 802 2382 +f 1743 1516 1722 +f 1746 1716 1676 +f 1535 1539 1534 +f 1534 1539 1553 +f 1642 1623 1624 +f 1095 1208 1654 +f 967 1407 1598 +f 1580 967 1598 +f 1809 1797 1720 +f 1924 1524 1535 +f 1533 1924 1535 +f 1539 1576 1575 +f 1642 216 585 +f 1407 1529 1485 +f 1485 1529 164 +f 1472 1462 1482 +f 1415 1431 1240 +f 966 1194 714 +f 383 1182 152 +f 474 2337 446 +f 1743 1841 1757 +f 1486 1524 1924 +f 1535 1525 1539 +f 1575 1576 1592 +f 1420 967 1580 +f 1288 1634 1613 +f 459 427 1265 +f 1404 2179 1393 +f 1404 1403 1800 +f 1404 1410 1403 +f 1410 1749 1403 +f 1349 1351 218 +f 1486 1498 1524 +f 1535 1524 1525 +f 1607 1636 1624 +f 183 1642 1624 +f 1636 183 1624 +f 1107 1349 218 +f 1351 845 218 +f 164 1519 1142 +f 845 413 218 +f 1525 1576 1539 +f 1576 1582 1592 +f 1592 2134 1607 +f 2134 1636 1607 +f 2147 1491 1401 +f 1407 1589 1529 +f 1529 1519 164 +f 1693 1763 1444 +f 1924 1479 1486 +f 1592 1582 2134 +f 499 165 874 +f 2176 1857 1959 +f 2327 2368 2326 +f 2358 821 953 +f 953 821 1573 +f 1824 1704 1464 +f 1731 1358 1678 +f 1394 1410 1404 +f 1394 1418 1410 +f 1466 1479 1839 +f 1486 1479 1498 +f 1498 1525 1524 +f 1576 2080 1582 +f 1785 1684 1898 +f 804 398 802 +f 804 925 398 +f 1447 1562 2358 +f 2358 1562 821 +f 821 1620 1573 +f 1620 1420 1573 +f 1420 1556 967 +f 1393 1394 1404 +f 1525 2080 1576 +f 1621 1420 1620 +f 1621 1556 1420 +f 967 1589 1407 +f 1505 5 1357 +f 1266 1258 1436 +f 1393 1395 1394 +f 2176 2175 1848 +f 1455 1466 1839 +f 1525 1540 2080 +f 1582 2080 2118 +f 1100 804 802 +f 1556 1589 967 +f 1589 1082 1529 +f 1093 1685 1357 +f 1504 1093 1357 +f 1425 1418 1394 +f 1475 1479 1466 +f 1479 1506 1498 +f 1789 1784 1730 +f 2501 2465 2489 +f 1438 1458 1430 +f 1462 1458 1473 +f 1454 805 1529 +f 1082 1454 1529 +f 1529 805 1519 +f 1425 1394 1395 +f 1425 1744 1418 +f 1479 1475 1506 +f 1540 2060 2080 +f 1556 1082 1589 +f 1443 945 1511 +f 1506 1536 1498 +f 1498 1536 1525 +f 1525 1536 1540 +f 1670 852 1672 +f 1998 1388 1389 +f 1511 966 1509 +f 1509 966 714 +f 1442 1443 1496 +f 1562 1635 821 +f 155 1322 1473 +f 1439 1458 1438 +f 1426 1425 1395 +f 1475 1499 1506 +f 1735 1588 1776 +f 2422 2454 2421 +f 1423 1432 1415 +f 1559 2101 2073 +f 845 866 413 +f 1429 1620 821 +f 1620 1429 1621 +f 1228 1250 1687 +f 1002 945 1443 +f 2382 802 1083 +f 1859 1411 1395 +f 1411 1426 1395 +f 1426 1744 1425 +f 1590 1437 1483 +f 1480 1475 1466 +f 1480 1499 1475 +f 1510 1733 1743 +f 1663 1696 1658 +f 1430 1453 1452 +f 1452 1472 1471 +f 1452 1471 1448 +f 1430 1452 1421 +f 1430 1421 1422 +f 1429 1082 1556 +f 1621 1429 1556 +f 1351 2386 845 +f 1126 1059 487 +f 1639 1437 1563 +f 1504 1928 1093 +f 1499 1536 1506 +f 1588 1770 1727 +f 1110 1747 1397 +f 1776 1588 1531 +f 1322 1320 1482 +f 1590 1629 1571 +f 1730 1877 1838 +f 1429 935 1082 +f 1082 935 1454 +f 804 1443 925 +f 1139 1007 1639 +f 1925 1480 1466 +f 1934 1989 1480 +f 1499 1989 1536 +f 1727 1526 1531 +f 1593 1614 502 +f 2455 2431 2400 +f 1755 1680 908 +f 1563 1571 1564 +f 1647 1078 1501 +f 2490 1635 1106 +f 1496 1511 717 +f 2454 2431 516 +f 1478 1153 1093 +f 1870 1426 1411 +f 1426 1723 1744 +f 962 986 1412 +f 717 1511 1509 +f 1825 1704 1824 +f 2225 2234 2253 +f 1490 1557 1188 +f 1635 80 821 +f 805 1454 935 +f 1186 706 695 +f 1194 1161 714 +f 1512 1007 1013 +f 592 97 204 +f 1258 1266 1257 +f 82 1333 471 +f 1694 1710 1505 +f 1643 490 1661 +f 1661 490 1114 +f 1518 2068 2484 +f 1750 1808 1664 +f 1656 1635 2490 +f 935 1521 805 +f 1546 1629 1076 +f 1301 70 1277 +f 966 1441 1194 +f 1148 1825 1824 +f 1614 1609 1643 +f 1114 1092 1921 +f 1770 1739 1670 +f 1631 1632 1646 +f 821 1016 1429 +f 1429 1016 935 +f 1632 1095 1654 +f 1083 262 688 +f 1724 1686 1725 +f 1644 490 1643 +f 1092 1149 1921 +f 3 893 1832 +f 988 1640 1188 +f 916 1107 284 +f 1656 80 1635 +f 1016 821 80 +f 1016 1521 935 +f 1478 1202 1153 +f 1401 1928 29 +f 1440 1478 1928 +f 1849 1700 1865 +f 1595 1611 1612 +f 1208 198 341 +f 1464 1704 1746 +f 2143 984 1721 +f 1848 1849 1868 +f 1662 1114 490 +f 1669 1787 1682 +f 1656 1618 80 +f 198 1208 916 +f 1440 1928 1401 +f 1521 1369 805 +f 1252 1107 916 +f 1745 678 1672 +f 1703 1779 1721 +f 1750 1465 1808 +f 1609 1644 1643 +f 1092 1114 1662 +f 1826 1523 1793 +f 2262 2261 2224 +f 1696 2166 1767 +f 1016 1648 1521 +f 1208 1252 916 +f 833 688 1067 +f 1794 1803 1558 +f 28 17 512 +f 1750 861 1566 +f 1594 1644 1609 +f 1644 1645 490 +f 490 1645 1662 +f 2229 2262 2224 +f 1602 861 1760 +f 1530 1777 1760 +f 872 1706 1673 +f 1696 1668 2166 +f 1708 1809 1798 +f 1581 1716 1814 +f 1709 1794 1680 +f 1233 1421 1257 +f 1724 1476 1686 +f 1469 1481 1965 +f 1965 1481 1492 +f 2073 1549 1559 +f 1594 1615 1644 +f 1799 1706 1755 +f 1725 1686 1837 +f 1720 1797 1572 +f 1618 2467 2022 +f 1618 1579 80 +f 1648 1016 80 +f 2134 2152 1636 +f 1611 1632 1631 +f 1761 1434 1470 +f 1559 1577 1594 +f 1603 1615 1594 +f 1615 1645 1644 +f 1637 1662 1645 +f 1662 1199 1092 +f 1199 1149 1092 +f 1451 1108 1149 +f 665 734 756 +f 1865 1700 1714 +f 1709 1841 1794 +f 1618 2022 1579 +f 1648 1413 1369 +f 1521 1648 1369 +f 1520 11 1401 +f 1446 1470 1434 +f 1798 1691 1754 +f 2063 1544 2073 +f 2073 1544 1549 +f 1594 1577 1603 +f 1615 1637 1645 +f 1637 1199 1662 +f 1427 1149 1199 +f 2167 1108 1451 +f 1997 1673 1705 +f 1706 1799 1705 +f 1841 1709 1757 +f 1604 1579 2022 +f 1579 707 80 +f 80 707 1648 +f 1520 1401 1491 +f 1649 1520 1491 +f 1435 1434 1885 +f 1470 1469 1461 +f 1481 1508 2024 +f 2370 1544 2063 +f 1549 1568 1559 +f 1559 1568 1577 +f 1603 1610 1615 +f 1615 1610 1637 +f 999 1199 1637 +f 1451 1149 1427 +f 1137 1825 1148 +f 1706 1705 1673 +f 1138 1604 2116 +f 1138 1579 1604 +f 1413 1648 707 +f 2360 2024 1508 +f 598 1075 1116 +f 229 93 1468 +f 1839 1479 1684 +f 2216 2229 2224 +f 1610 1625 1637 +f 329 999 1637 +f 1199 1017 1427 +f 1017 303 1427 +f 303 1451 1427 +f 1792 1754 1777 +f 2309 2391 2301 +f 1655 1138 2116 +f 1138 707 1579 +f 1649 1491 206 +f 1406 1885 1398 +f 1406 1419 1885 +f 1419 1435 1885 +f 1434 1435 1446 +f 1470 1481 1469 +f 1577 1583 1603 +f 999 1017 1199 +f 81 67 941 +f 67 1650 941 +f 1259 1815 2164 +f 1619 2116 2045 +f 1424 707 1138 +f 1702 1649 206 +f 1687 1406 1398 +f 1477 1481 1470 +f 1568 1569 1577 +f 1577 1569 1583 +f 1603 1583 1610 +f 1625 329 1637 +f 2167 340 273 +f 81 273 340 +f 81 962 67 +f 1547 1619 1488 +f 1830 1739 1770 +f 938 1424 1138 +f 1424 1413 707 +f 1527 1649 1702 +f 1527 1520 1649 +f 1527 1268 1520 +f 1250 1406 1687 +f 1441 1353 1115 +f 1203 1413 1051 +f 1250 1419 1406 +f 1477 2372 1481 +f 1481 2372 1508 +f 2449 1560 1568 +f 1549 2449 1568 +f 1568 1560 1569 +f 1569 1584 1583 +f 1652 329 1625 +f 329 817 999 +f 285 1017 999 +f 303 10 1451 +f 10 2167 1451 +f 1412 1650 67 +f 1412 1488 1650 +f 1547 1023 1619 +f 1023 1655 1619 +f 1655 938 1138 +f 1456 1413 1424 +f 1457 1470 1446 +f 1457 1477 1470 +f 329 1652 817 +f 10 340 2167 +f 938 1546 1424 +f 1546 1456 1424 +f 1259 1548 1779 +f 2052 2031 1990 +f 1440 1202 1478 +f 1428 1419 1250 +f 1428 1435 1419 +f 1428 1446 1435 +f 1934 1935 1955 +f 1560 1584 1569 +f 1610 1638 1625 +f 1638 1652 1625 +f 817 1077 999 +f 1077 285 999 +f 980 303 1017 +f 962 1412 67 +f 1494 1023 1547 +f 325 271 1270 +f 1443 1511 1496 +f 1450 1268 1527 +f 1514 1353 1441 +f 1287 1446 1428 +f 1446 1287 1457 +f 1305 2372 1477 +f 1992 1990 1991 +f 1992 1991 1971 +f 1971 1991 1955 +f 2449 1549 2418 +f 1583 1616 1610 +f 1610 1616 1638 +f 10 1396 340 +f 340 1445 81 +f 1445 962 81 +f 1790 984 1753 +f 984 2148 1753 +f 1588 1713 1770 +f 969 978 958 +f 1741 1779 1703 +f 1758 1846 1754 +f 1827 1819 1029 +f 1818 1530 1712 +f 1750 1566 2127 +f 2459 2434 2483 +f 1798 1720 1771 +f 1794 1841 1803 +f 216 1755 1810 +f 1098 1735 1748 +f 1735 1497 1748 +f 1502 2102 1601 +f 881 1502 1601 +f 1455 1839 1744 +f 1706 1709 1680 +f 1212 1741 1703 +f 1788 1969 1671 +f 1075 1074 1692 +f 951 2500 881 +f 2490 2486 2463 +f 1748 1497 1781 +f 1721 984 1840 +f 1815 1259 1741 +f 1626 1756 1837 +f 975 987 1542 +f 2230 2236 2235 +f 1772 678 734 +f 1542 1671 975 +f 1806 1772 1780 +f 678 1772 1806 +f 2218 2225 2268 +f 1828 1732 2007 +f 1526 1688 1531 +f 1752 1526 1554 +f 1844 1818 1712 +f 1823 1846 1804 +f 1781 1669 1704 +f 1721 1779 2143 +f 1770 1670 1526 +f 1497 1669 1781 +f 1098 1713 1735 +f 1742 1815 1741 +f 1526 858 1875 +f 1599 1531 1688 +f 1803 1790 1558 +f 1703 1721 1683 +f 1832 1766 957 +f 1542 1679 1671 +f 1679 1788 1671 +f 1927 1819 1827 +f 1718 1745 1739 +f 1684 1022 1839 +f 1459 1283 1299 +f 1022 1410 1418 +f 2368 2393 2326 +f 1669 1497 1776 +f 1875 858 1212 +f 1739 1745 852 +f 1964 1918 1461 +f 1356 133 1331 +f 1765 1829 1468 +f 858 1742 1741 +f 1006 1674 1021 +f 1723 1936 1935 +f 1468 1713 1098 +f 1724 1678 1476 +f 1680 1783 908 +f 1731 1543 520 +f 1683 1721 1840 +f 1467 1679 1542 +f 1812 1708 1846 +f 1679 1975 1788 +f 1713 1830 1770 +f 1803 1722 1790 +f 2301 2391 2349 +f 1713 1588 1735 +f 1836 1530 1818 +f 1837 1756 861 +f 886 571 556 +f 1181 1805 1812 +f 1706 1680 1755 +f 1677 1729 1775 +f 1776 1787 1669 +f 1526 1670 1672 +f 1727 1770 1526 +f 987 1467 1542 +f 1567 1704 1137 +f 1693 1865 1714 +f 897 1762 912 +f 1135 1697 1062 +f 1697 376 1062 +f 1543 1731 1678 +f 1793 1679 1467 +f 1777 1602 1760 +f 1846 1798 1754 +f 1835 1096 1677 +f 1033 1030 940 +f 1450 1527 1702 +f 1717 376 1697 +f 1711 1717 1697 +f 1717 165 376 +f 1840 984 1790 +f 1669 1746 1704 +f 1669 1682 1746 +f 2301 2349 2308 +f 1882 1444 1898 +f 1820 1789 1730 +f 861 1380 1566 +f 2301 2308 2266 +f 1771 1543 1691 +f 1958 1659 1651 +f 1697 1360 1711 +f 1711 1737 1717 +f 1717 1737 165 +f 1790 1753 1558 +f 1668 1696 1663 +f 1360 1702 1711 +f 1702 1707 1711 +f 1707 1737 1711 +f 1737 1751 165 +f 1444 1782 1693 +f 1716 1787 1599 +f 1744 1839 1022 +f 1898 1444 1785 +f 206 1707 1702 +f 1764 2468 1751 +f 316 1844 893 +f 893 1844 915 +f 1845 1804 1758 +f 1380 861 1756 +f 1780 670 1021 +f 1714 2172 1763 +f 1783 1558 1663 +f 1750 2127 1465 +f 1798 1771 1691 +f 1691 1543 1724 +f 1872 1910 839 +f 1737 2044 1751 +f 1751 2044 1764 +f 1757 1701 482 +f 1725 1602 1777 +f 1836 1845 1530 +f 2102 2470 1503 +f 2496 1899 544 +f 763 2484 946 +f 987 1719 1467 +f 1845 1758 1792 +f 1725 1837 1602 +f 1872 1866 1873 +f 1712 1530 1760 +f 489 1799 216 +f 1760 861 1750 +f 2068 2466 2460 +f 1696 2159 2168 +f 377 1377 1280 +f 1797 1676 1572 +f 1581 2053 1572 +f 1676 1581 1572 +f 1764 2498 2468 +f 2468 2498 1994 +f 1861 1695 1860 +f 2481 2004 2495 +f 1826 1677 1523 +f 1670 1739 852 +f 2234 2269 2253 +f 1724 1543 1678 +f 1658 2168 1791 +f 1397 1747 1719 +f 1696 2168 1658 +f 979 519 272 +f 1774 1975 1679 +f 975 1671 932 +f 1787 1716 1682 +f 1835 1826 1747 +f 2501 2469 961 +f 1810 908 1791 +f 1982 1768 191 +f 1137 1704 1825 +f 1804 1846 1758 +f 2004 2044 1737 +f 913 1969 902 +f 2498 1795 1801 +f 915 1844 1712 +f 1689 915 1712 +f 1740 1752 1541 +f 695 661 199 +f 1865 1693 1782 +f 1824 1464 1809 +f 1829 1765 1718 +f 1816 1768 1982 +f 1816 1622 1768 +f 1622 2165 1681 +f 1768 1622 1681 +f 670 1772 228 +f 1283 1459 52 +f 1785 1444 1749 +f 1675 1075 1685 +f 1567 1781 1704 +f 1858 1857 1848 +f 1526 1752 1688 +f 1791 2160 1810 +f 908 1658 1791 +f 1813 1773 1558 +f 1845 1792 1530 +f 69 376 165 +f 3 1832 1834 +f 1722 1516 1507 +f 1801 1821 1994 +f 1833 1982 2046 +f 1821 1833 2046 +f 1833 1816 1982 +f 1022 1785 1749 +f 2160 2170 1810 +f 1147 1719 1726 +f 1683 1840 1507 +f 1467 1719 1793 +f 1795 1802 1801 +f 1802 1811 1801 +f 1801 1811 1821 +f 1690 2165 1622 +f 1934 1480 1925 +f 229 1468 1091 +f 1780 2164 1742 +f 1672 1742 858 +f 1833 1417 1816 +f 1417 1622 1816 +f 1831 2165 1690 +f 1668 1663 1558 +f 1719 1747 1826 +f 1760 1750 1664 +f 1817 1690 1622 +f 1530 1792 1777 +f 948 1796 1802 +f 1796 1811 1802 +f 1515 1817 1622 +f 1695 1861 1831 +f 1783 1663 1658 +f 1749 1410 1022 +f 854 1796 948 +f 1811 1842 1833 +f 1821 1811 1833 +f 1833 1842 1417 +f 1622 1417 1515 +f 127 1804 1845 +f 1686 1626 1837 +f 1608 1690 1817 +f 1523 1775 1762 +f 127 1845 1836 +f 1812 1805 1708 +f 1523 1677 1775 +f 1780 1772 670 +f 1758 1754 1792 +f 1204 1796 854 +f 1822 1842 1811 +f 1608 1831 1690 +f 1822 1811 1796 +f 1842 1416 1417 +f 1417 1416 1515 +f 1515 1608 1817 +f 1728 1831 1608 +f 908 1783 1658 +f 127 1836 316 +f 1805 1148 1824 +f 852 1745 1672 +f 1478 1093 1928 +f 1822 1843 1842 +f 1843 959 1842 +f 1842 959 1416 +f 1728 1695 1831 +f 1728 1860 1695 +f 2346 446 2337 +f 1602 1837 861 +f 1087 1096 1835 +f 1708 1824 1809 +f 2004 1737 505 +f 1567 1748 1781 +f 520 1543 1883 +f 1760 1664 1712 +f 128 1336 72 +f 2053 1883 1543 +f 1822 180 1843 +f 1786 1608 1515 +f 929 2462 519 +f 512 2402 506 +f 1212 1703 1683 +f 1830 1829 1739 +f 2053 1543 1771 +f 1416 1769 1515 +f 1769 1786 1515 +f 1786 1728 1608 +f 1712 1808 1689 +f 1794 1558 1783 +f 1497 1735 1776 +f 1127 1567 1137 +f 1123 1748 1567 +f 36 205 1185 +f 959 1734 1416 +f 1738 1733 1541 +f 1774 1762 1974 +f 1752 1554 1541 +f 1752 1740 1688 +f 1526 1875 1554 +f 1468 1829 1830 +f 1755 908 1810 +f 1716 1599 1814 +f 1806 1780 1742 +f 2308 2349 2340 +f 1832 915 1689 +f 1713 1468 1830 +f 1814 1599 1346 +f 1832 1689 1766 +f 1022 1684 1785 +f 1093 1153 1116 +f 1672 678 1742 +f 1675 1685 1093 +f 1841 1743 1722 +f 1814 2053 1581 +f 1464 1746 1809 +f 2485 2497 2493 +f 1416 1734 1769 +f 1665 1728 1786 +f 1665 1951 1728 +f 1951 1860 1728 +f 1951 2094 1860 +f 1844 1836 1818 +f 316 1836 1844 +f 1776 1531 1787 +f 1719 1826 1793 +f 2147 1401 29 +f 2111 2121 1548 +f 1741 1259 1779 +f 1843 347 1834 +f 1843 1734 959 +f 1766 1769 1734 +f 957 1766 1734 +f 1766 1786 1769 +f 1766 1689 1786 +f 1689 1665 1786 +f 1754 1691 1777 +f 1507 1840 1790 +f 1761 1470 1461 +f 1523 1679 1793 +f 1091 1468 1098 +f 1820 1730 1838 +f 1843 1834 1734 +f 1808 1951 1665 +f 1588 1727 1531 +f 893 915 1832 +f 1523 1774 1679 +f 272 2488 710 +f 1093 1116 1675 +f 2340 2349 2348 +f 1832 1734 1834 +f 1832 957 1734 +f 1951 1808 2094 +f 1685 1692 1505 +f 1043 295 698 +f 2143 1779 2121 +f 1689 1808 1665 +f 1693 1714 1763 +f 1738 2157 1516 +f 1114 1921 236 +f 1268 1333 1520 +f 1149 1108 431 +f 508 2144 1912 +f 1957 1108 1537 +f 431 1108 1957 +f 1018 1108 2167 +f 1338 1957 1681 +f 2163 1957 1338 +f 1983 1390 2093 +f 30 557 37 +f 1714 2173 2172 +f 1983 1984 1390 +f 1984 2065 1390 +f 884 1762 897 +f 2065 1984 1214 +f 1950 1974 1762 +f 884 1950 1762 +f 2012 1698 1861 +f 1214 2116 803 +f 1950 1938 1974 +f 1938 1967 1974 +f 1900 1761 1461 +f 865 1929 884 +f 884 1929 1950 +f 2062 2071 2042 +f 919 1985 1732 +f 1593 502 2146 +f 1995 1213 2098 +f 1522 2476 1651 +f 2174 1849 2175 +f 1480 1989 1499 +f 1929 1938 1950 +f 1605 2058 1574 +f 2097 1605 2133 +f 1912 2014 1886 +f 2092 2082 2083 +f 206 1930 505 +f 2101 2100 2092 +f 2073 2101 2092 +f 839 1910 865 +f 1910 1901 1929 +f 865 1910 1929 +f 1967 1788 1975 +f 2073 2092 2063 +f 2101 1593 2100 +f 2015 1876 1698 +f 1853 1884 2014 +f 1831 1698 2165 +f 1316 273 81 +f 1901 1920 1929 +f 1929 1920 1938 +f 1920 1968 1967 +f 1938 1920 1967 +f 1849 2174 1700 +f 2173 1700 2174 +f 2062 2072 2091 +f 803 2467 2059 +f 2239 1736 2240 +f 1505 1357 1685 +f 1358 1686 1476 +f 1967 1968 1788 +f 1968 1969 1788 +f 2065 2110 2156 +f 2065 1214 2110 +f 2110 1214 503 +f 273 2093 1018 +f 273 1983 2093 +f 532 1886 2155 +f 2034 2021 1947 +f 216 1810 585 +f 1912 543 2014 +f 1390 2051 1537 +f 1872 1873 1910 +f 1984 2045 1214 +f 597 1912 1886 +f 1593 2146 2100 +f 2071 2062 2090 +f 2034 2046 1982 +f 2034 1947 2046 +f 1214 2045 2116 +f 1873 1887 1910 +f 1887 1901 1910 +f 1562 1447 1106 +f 2163 431 1957 +f 1948 1972 1936 +f 1972 1948 1992 +f 2014 2015 2013 +f 1853 2014 2013 +f 1550 1884 1853 +f 1947 2468 1994 +f 1355 1550 2154 +f 1355 1884 1550 +f 2081 2108 2128 +f 2024 1965 1492 +f 2024 2032 1965 +f 2116 1604 803 +f 1901 1911 1920 +f 1939 1968 1920 +f 1911 1939 1920 +f 872 1626 1666 +f 2062 2091 2120 +f 1819 1927 1759 +f 1021 1674 1780 +f 872 1673 1756 +f 1550 501 2171 +f 1378 1550 2171 +f 2146 2162 2145 +f 1358 482 192 +f 2109 2120 2119 +f 1866 1872 2227 +f 1391 2012 1860 +f 2136 2137 2161 +f 2162 1661 236 +f 1887 1894 1901 +f 1901 1894 1911 +f 505 1707 206 +f 2120 2137 2136 +f 2142 2164 1674 +f 1860 2012 1861 +f 1894 1939 1911 +f 2080 2060 2118 +f 2162 236 508 +f 2164 1815 1742 +f 1018 2093 1537 +f 2154 1378 2161 +f 2041 2098 2491 +f 2043 2042 2032 +f 1108 1018 1537 +f 1465 2094 1808 +f 502 1643 1661 +f 2467 1618 1656 +f 2119 2136 2135 +f 2119 2108 2071 +f 878 1183 1195 +f 2101 1594 1593 +f 2033 2370 2063 +f 2482 2491 2098 +f 1282 2406 1275 +f 2003 1948 1956 +f 2043 2032 2024 +f 2025 2043 2024 +f 2154 1550 1378 +f 1795 2498 1764 +f 2142 1548 2164 +f 2431 2454 2422 +f 1981 2011 1993 +f 2349 2391 2362 +f 502 2162 2146 +f 2025 2024 2360 +f 2129 2120 2091 +f 1732 1985 2007 +f 2171 1308 209 +f 1930 1995 2041 +f 1390 1238 2051 +f 1866 1878 1887 +f 1878 1894 1887 +f 1965 2032 2011 +f 874 2480 2492 +f 2071 2108 2069 +f 1358 1731 482 +f 430 2021 2034 +f 1965 2003 1964 +f 1855 1889 831 +f 1668 1773 2150 +f 1390 2156 1238 +f 898 869 1903 +f 2391 2407 2362 +f 2121 2111 2074 +f 1548 1259 2164 +f 2099 2129 2091 +f 1550 1853 501 +f 1853 1852 501 +f 952 2017 969 +f 2085 2121 2074 +f 2130 2006 1391 +f 2144 1367 543 +f 2100 2146 2099 +f 1545 1319 113 +f 1903 1922 898 +f 1922 1931 898 +f 585 2170 1641 +f 2007 2017 952 +f 2017 2074 969 +f 1558 1753 1813 +f 837 2005 1517 +f 2005 2006 2130 +f 1532 1474 1528 +f 2003 1981 1948 +f 2070 2071 2069 +f 1922 919 1931 +f 2017 2085 2074 +f 2085 2104 2121 +f 2100 2099 2082 +f 2156 2110 2034 +f 505 2474 2004 +f 1903 871 1922 +f 1922 1952 919 +f 919 1952 1985 +f 1985 2001 2007 +f 2001 2036 2017 +f 2007 2001 2017 +f 2017 2036 2085 +f 2036 2047 2085 +f 2047 2075 2085 +f 2075 2104 2085 +f 1948 1993 2023 +f 2400 2422 2407 +f 2011 2070 1993 +f 2033 2043 2025 +f 2012 2015 1698 +f 1876 1338 2165 +f 871 1940 1922 +f 1985 1976 2001 +f 2121 2104 2143 +f 1051 1413 1456 +f 2358 1362 2379 +f 1859 1789 1870 +f 2090 2109 2071 +f 1405 1398 1885 +f 1886 1884 1355 +f 1922 1960 1952 +f 1952 1960 1985 +f 1960 1976 1985 +f 1956 1948 1936 +f 2135 209 2128 +f 2157 1875 1212 +f 2160 2168 2169 +f 1900 1461 1918 +f 2001 2018 2036 +f 2075 2086 2104 +f 2111 2142 2103 +f 1937 1956 1936 +f 2023 2070 2061 +f 2135 2128 2108 +f 2042 2071 2011 +f 2138 413 2383 +f 2033 2072 2043 +f 1922 1940 1960 +f 2070 2069 2061 +f 2069 2108 2061 +f 2108 2119 2135 +f 1855 1904 1889 +f 1889 1904 871 +f 871 1904 1940 +f 1976 2018 2001 +f 2036 2018 2047 +f 2122 2143 2104 +f 216 1642 489 +f 2148 984 2143 +f 1975 1974 1967 +f 2157 1683 1516 +f 1614 1593 1594 +f 2269 2270 2276 +f 1926 2147 29 +f 2082 2091 2072 +f 430 503 2059 +f 1904 1905 1940 +f 1940 1961 1960 +f 1961 1976 1960 +f 2087 2086 2075 +f 2065 2156 1390 +f 1820 1838 1900 +f 534 1308 837 +f 2167 273 1018 +f 831 1850 1855 +f 2019 2037 2018 +f 2018 2037 2047 +f 2037 2075 2047 +f 2086 2095 2104 +f 2095 2122 2104 +f 2122 2148 2143 +f 1926 1213 1995 +f 1405 1885 1761 +f 2006 2013 2012 +f 2211 2233 2216 +f 1855 1890 1904 +f 1904 1895 1905 +f 1905 1932 1940 +f 1961 1977 1976 +f 1976 1986 2018 +f 2484 2476 1518 +f 1870 1411 1859 +f 1548 2142 2111 +f 1904 1890 1895 +f 1895 1932 1905 +f 1940 1932 1961 +f 1976 1977 1986 +f 1986 2008 2018 +f 2018 2008 2019 +f 2087 2075 2037 +f 2087 2095 2086 +f 2094 1391 1860 +f 1852 1853 2006 +f 1853 2013 2006 +f 929 979 850 +f 1855 1874 1890 +f 2008 2028 2019 +f 1993 2070 2023 +f 1705 1799 1998 +f 1491 2147 206 +f 1851 1856 1855 +f 1895 1890 1874 +f 2038 2019 2028 +f 2038 2048 2037 +f 2019 2038 2037 +f 2048 2067 2087 +f 2037 2048 2087 +f 2087 2067 2095 +f 2095 2149 2122 +f 2149 2148 2122 +f 1308 2005 837 +f 209 1308 1387 +f 1601 2102 1927 +f 254 170 201 +f 1800 1403 1763 +f 1510 1346 1740 +f 870 871 1903 +f 1919 1650 1619 +f 2148 1667 1753 +f 1932 1923 1961 +f 1977 1953 1986 +f 2067 2112 2095 +f 2112 2149 2095 +f 2148 2149 1667 +f 2422 2421 2407 +f 1926 2026 1213 +f 1912 2144 543 +f 2128 1387 2153 +f 1733 1510 1740 +f 990 853 2489 +f 503 1214 803 +f 1921 431 2163 +f 2146 2145 2129 +f 2144 1921 2163 +f 1855 1856 1874 +f 1895 1923 1932 +f 1923 1941 1961 +f 1961 1941 1977 +f 2048 2076 2067 +f 2076 2113 2067 +f 2067 2113 2112 +f 1723 1900 1937 +f 1870 1900 1723 +f 1367 2163 1338 +f 520 1346 1510 +f 1698 1831 1861 +f 1984 1919 2045 +f 1895 1891 1923 +f 2008 1986 2028 +f 1948 1981 1993 +f 1883 1346 520 +f 1883 1814 1346 +f 1930 206 2147 +f 2499 2486 1447 +f 1891 1906 1923 +f 1923 1953 1941 +f 1953 1977 1941 +f 1953 1987 1986 +f 2113 2123 2112 +f 2123 2149 2112 +f 1387 1308 1226 +f 1599 1688 1346 +f 2093 1390 1537 +f 2003 2011 1981 +f 1987 2028 1986 +f 2038 2049 2048 +f 2048 2049 2076 +f 1813 1667 2149 +f 2123 1813 2149 +f 1461 1469 1964 +f 1757 1510 1743 +f 505 1930 1999 +f 2223 1784 1789 +f 1532 1522 1651 +f 1906 1913 1923 +f 1913 1943 1923 +f 1943 1942 1923 +f 1923 1942 1953 +f 1942 1987 1953 +f 1308 1852 2005 +f 2053 1814 1883 +f 1733 1740 1541 +f 2154 1886 1355 +f 1503 1528 1474 +f 1874 1879 1895 +f 1895 1879 1891 +f 2076 2124 2113 +f 2113 2124 2123 +f 1896 1891 1879 +f 1891 1896 1906 +f 1942 1962 1987 +f 1962 2009 2028 +f 1987 1962 2028 +f 2009 2038 2028 +f 2109 2119 2071 +f 1918 1956 1937 +f 1851 1864 1856 +f 1896 1897 1906 +f 1906 1897 1913 +f 1943 1962 1942 +f 2049 2077 2076 +f 2124 2125 2123 +f 1930 2147 1926 +f 1902 1894 1878 +f 482 1510 1757 +f 2129 2137 2120 +f 503 803 2059 +f 1847 1857 1851 +f 1851 1857 1864 +f 2039 2038 2009 +f 2038 2039 2049 +f 2076 2077 2124 +f 2150 1813 2123 +f 482 520 1510 +f 1994 1821 2046 +f 2044 2004 1764 +f 1864 1867 1856 +f 1867 1874 1856 +f 1897 1944 1913 +f 1943 1944 1962 +f 2124 2126 2125 +f 2150 2123 2125 +f 2099 2146 2129 +f 2041 1995 2098 +f 1605 1641 2151 +f 1847 1959 1857 +f 1874 1867 1879 +f 1913 1944 1943 +f 1944 1963 1962 +f 2077 2096 2124 +f 2096 2126 2124 +f 2126 2150 2125 +f 941 1650 1919 +f 2135 2136 209 +f 1884 1886 2014 +f 2049 2029 2077 +f 1388 2127 1389 +f 1389 2127 1566 +f 1930 1926 1995 +f 941 1919 1316 +f 2110 503 430 +f 1867 1880 1879 +f 1879 1880 1896 +f 1897 1907 1944 +f 1963 1978 1962 +f 1962 1978 2009 +f 2039 2029 2049 +f 2077 2078 2096 +f 822 823 827 +f 2166 1668 2150 +f 81 941 1316 +f 2204 2216 2203 +f 2011 2071 2070 +f 1880 1892 1896 +f 1892 1907 1897 +f 1896 1892 1897 +f 1907 1914 1944 +f 1978 2010 2009 +f 2010 2039 2009 +f 1688 1740 1346 +f 1789 1820 1870 +f 2130 1391 2094 +f 1944 1945 1963 +f 2029 2078 2077 +f 1767 2150 2126 +f 1767 2166 2150 +f 803 2022 2467 +f 1503 1927 2102 +f 1914 1954 1944 +f 1944 1954 1945 +f 1963 1970 1978 +f 2078 2105 2096 +f 2105 2126 2096 +f 1965 2011 2003 +f 192 1626 1358 +f 2101 1559 1594 +f 1930 2041 1999 +f 1698 1876 2165 +f 1398 1871 891 +f 2165 1338 1681 +f 1970 2010 1978 +f 2010 2030 2029 +f 2039 2010 2029 +f 2030 2055 2078 +f 2029 2030 2078 +f 1849 1848 2175 +f 1871 1862 891 +f 543 2015 2014 +f 1857 1858 1864 +f 1864 1858 1867 +f 1963 1945 1970 +f 2055 2088 2078 +f 2078 2088 2105 +f 2105 2131 2126 +f 2126 2131 1767 +f 2063 2083 2033 +f 2161 2171 209 +f 2032 2042 2011 +f 1813 2150 1773 +f 1914 1908 1954 +f 1970 1979 2010 +f 2088 2131 2105 +f 2015 543 1876 +f 1694 1692 1048 +f 1395 2207 1859 +f 1395 1393 2207 +f 1730 1784 1736 +f 2500 2466 2470 +f 1709 1701 1757 +f 1945 1979 1970 +f 2030 2050 2055 +f 2350 2317 2286 +f 2154 2155 1886 +f 871 860 1889 +f 2161 209 2136 +f 2497 2463 2493 +f 2190 2204 2203 +f 1800 2179 1404 +f 2477 2469 1385 +f 1385 1715 2477 +f 2128 209 1387 +f 1858 1868 1867 +f 1867 1881 1880 +f 1893 1892 1880 +f 1881 1893 1880 +f 1893 1907 1892 +f 1907 1908 1914 +f 1954 1979 1945 +f 1979 1980 2010 +f 2131 2159 1767 +f 1765 93 339 +f 1761 1877 1405 +f 523 1347 515 +f 1541 2157 1738 +f 2144 2163 1367 +f 1380 1389 1566 +f 2317 2392 2316 +f 1994 2498 1801 +f 1867 1868 1881 +f 1980 2050 2030 +f 2010 1980 2030 +f 2050 2089 2055 +f 2055 2089 2088 +f 2088 2114 2131 +f 1538 1651 1659 +f 2145 2155 2129 +f 2140 29 1928 +f 2370 2033 2025 +f 2252 2239 2240 +f 2239 2252 1862 +f 2392 2391 2316 +f 2469 2501 1385 +f 2477 1715 1710 +f 502 1614 1643 +f 2438 1227 2431 +f 1915 1907 1893 +f 1915 1908 1907 +f 1954 1908 1979 +f 1908 1988 1979 +f 1979 1988 1980 +f 2114 2159 2131 +f 2155 2154 2129 +f 508 1966 2144 +f 872 1756 1626 +f 1710 1715 1505 +f 236 1966 508 +f 2272 2284 1398 +f 2325 2355 2319 +f 1548 2121 1779 +f 1532 1528 1522 +f 1980 2056 2050 +f 2050 2056 2089 +f 2013 2015 2012 +f 1964 2003 1956 +f 2006 2012 1391 +f 1565 1927 1503 +f 2244 2243 2226 +f 5 1715 1385 +f 1858 1848 1868 +f 1915 1946 1908 +f 1946 1988 1908 +f 1980 2020 2056 +f 2115 2159 2114 +f 2092 2083 2063 +f 1398 2284 1687 +f 2162 2155 2145 +f 519 2475 2488 +f 2158 5 1385 +f 5 1505 1715 +f 1692 1694 1505 +f 1988 2020 1980 +f 2115 2169 2159 +f 2169 2168 2159 +f 2083 2082 2072 +f 1316 1984 1983 +f 1488 1619 1650 +f 2083 2072 2033 +f 2361 1210 1233 +f 1933 1946 1915 +f 2056 2079 2089 +f 2088 2115 2114 +f 2099 2091 2082 +f 2162 532 2155 +f 1852 2006 2005 +f 2023 2061 2052 +f 2176 2184 2175 +f 2162 985 532 +f 1909 1893 1881 +f 1909 1915 1893 +f 1988 2040 2020 +f 2040 2056 2020 +f 2089 2079 2088 +f 2088 2079 2115 +f 1782 1444 1882 +f 1216 1215 2320 +f 867 1939 1894 +f 867 903 1939 +f 1372 2398 1379 +f 1863 504 2027 +f 2158 1385 504 +f 1868 1782 1881 +f 1909 1933 1915 +f 2040 1988 1946 +f 1481 2024 1492 +f 2120 2136 2119 +f 1522 1528 1518 +f 1871 1398 1405 +f 1221 1408 1399 +f 1357 5 2158 +f 2179 1800 1763 +f 1868 1865 1782 +f 1882 1881 1782 +f 1882 1909 1881 +f 2040 2057 2056 +f 2106 2079 2056 +f 2057 2106 2056 +f 2106 2132 2079 +f 2132 2115 2079 +f 2115 2132 2169 +f 532 985 597 +f 2092 2100 2082 +f 1210 1221 1399 +f 1399 1233 1210 +f 2130 2002 1517 +f 1849 1865 1868 +f 1933 2040 1946 +f 52 1269 30 +f 1667 1813 1753 +f 1997 1380 1673 +f 940 1008 1088 +f 1947 1994 2046 +f 1882 1916 1909 +f 1924 1933 1909 +f 1533 2040 1933 +f 1533 1534 2040 +f 2058 2040 1534 +f 2058 2057 2040 +f 1238 191 1768 +f 1997 1389 1380 +f 1875 1541 1554 +f 1854 504 1863 +f 1854 2158 504 +f 2396 1275 2406 +f 2426 2443 153 +f 1916 1924 1909 +f 1925 1935 1934 +f 1870 1723 1426 +f 2058 2097 2057 +f 2097 2106 2057 +f 2132 2151 2169 +f 2151 2160 2169 +f 1106 1635 1562 +f 1957 1768 1681 +f 1957 2051 1768 +f 526 535 33 +f 1614 1594 1609 +f 2233 2229 2216 +f 2496 2027 2084 +f 2496 1863 2027 +f 2117 1854 1863 +f 2016 2158 1854 +f 2016 1504 1357 +f 2158 2016 1357 +f 1114 236 1661 +f 2129 2154 2137 +f 2133 2106 2097 +f 2491 1999 2041 +f 2051 1238 1768 +f 2061 2108 2081 +f 2189 2195 2186 +f 2348 2349 2362 +f 1701 192 482 +f 505 1737 1707 +f 2133 2132 2106 +f 2132 2133 2151 +f 2151 2170 2160 +f 502 1661 2162 +f 1998 1389 1997 +f 2297 2352 2329 +f 2352 2364 2329 +f 2394 2414 2364 +f 2352 2394 2364 +f 2402 512 2415 +f 2255 2254 2243 +f 2446 1365 2456 +f 2271 2282 2298 +f 846 2283 2264 +f 2293 2310 2318 +f 2254 2295 2294 +f 2283 2290 2278 +f 2270 2294 2293 +f 2423 2455 2400 +f 2281 2287 2267 +f 2190 2191 2204 +f 2271 2263 2282 +f 2334 2329 2364 +f 2424 2432 2409 +f 2282 2263 2298 +f 1409 1659 1958 +f 2263 2302 2298 +f 2297 2329 2296 +f 1256 446 2346 +f 1958 2502 2478 +f 2437 2399 2444 +f 263 2366 2359 +f 849 827 823 +f 2311 2325 2290 +f 2499 2379 2434 +f 2446 2456 2423 +f 947 2358 2379 +f 2499 947 2379 +f 2205 2195 2212 +f 2245 2237 2227 +f 2245 2256 2237 +f 2256 2263 2271 +f 556 571 2305 +f 1528 2068 1518 +f 2424 2439 2432 +f 2302 2352 2297 +f 1866 2237 826 +f 2248 2242 2211 +f 2334 2364 2363 +f 2235 2244 2226 +f 2255 2295 2254 +f 2329 2324 2296 +f 2439 2447 1973 +f 2329 2334 2324 +f 2409 2432 2414 +f 2293 2318 2276 +f 866 2425 2416 +f 1487 1493 2372 +f 2237 2231 2230 +f 2415 512 17 +f 2035 1236 26 +f 921 2138 688 +f 2491 2482 2462 +f 6 181 197 +f 2481 948 1795 +f 2138 2383 2382 +f 2377 2394 2352 +f 2377 506 2394 +f 2394 506 2402 +f 2401 2402 2415 +f 2394 2402 2401 +f 2318 2326 2276 +f 2439 2457 2432 +f 2298 2302 2297 +f 2244 2249 2243 +f 2404 1100 2382 +f 2238 2245 2227 +f 2245 2257 2256 +f 2257 2263 2256 +f 2324 2334 2328 +f 2257 2289 2263 +f 2289 2302 2263 +f 2236 2231 2250 +f 2138 2382 688 +f 2383 2404 2382 +f 1100 2404 2343 +f 2353 2352 2302 +f 2353 2377 2352 +f 2237 2230 2220 +f 2335 2355 2325 +f 2308 2340 2315 +f 2253 2269 2276 +f 2311 2335 2325 +f 2439 2424 511 +f 2268 2267 2248 +f 2383 413 2404 +f 123 971 832 +f 2234 2243 2269 +f 2225 2213 2234 +f 2219 2213 2225 +f 2195 2196 2212 +f 1544 2418 1549 +f 413 866 2404 +f 2404 866 2416 +f 2416 2417 2404 +f 2404 2417 2343 +f 2415 2409 2401 +f 2196 2219 2212 +f 2268 2248 2218 +f 2206 2214 2197 +f 2417 2332 2343 +f 2343 2332 832 +f 2330 2302 2289 +f 2330 2353 2302 +f 2453 2454 515 +f 2218 2248 2217 +f 2218 2217 2205 +f 2276 2281 2268 +f 2178 2197 2177 +f 2197 2189 2177 +f 2332 2066 832 +f 832 2066 123 +f 2231 2236 2230 +f 669 950 1144 +f 2217 2211 2199 +f 1216 1209 1217 +f 2066 2365 123 +f 2230 2226 2214 +f 2290 2325 2304 +f 2325 2319 2304 +f 2217 2248 2211 +f 2191 2192 2199 +f 510 525 2035 +f 2417 1917 2332 +f 2332 1917 2066 +f 2408 2413 2341 +f 2248 2267 2242 +f 2326 2333 2281 +f 1340 2365 2066 +f 2440 1302 1340 +f 2226 2230 2235 +f 1153 1163 1116 +f 2431 2455 2438 +f 2416 2425 2417 +f 2495 2474 2462 +f 2290 2304 2277 +f 825 2227 1872 +f 151 239 1038 +f 9 151 1038 +f 545 928 2381 +f 2440 2406 1384 +f 928 1596 2381 +f 2186 2188 2185 +f 2456 26 1888 +f 2287 2333 2262 +f 2425 2342 2417 +f 2342 1917 2417 +f 1917 877 2066 +f 2336 1340 2066 +f 2336 2440 1340 +f 2328 2351 2327 +f 825 2238 2227 +f 2351 2368 2327 +f 1222 2388 1211 +f 678 756 734 +f 428 263 1343 +f 2188 2191 2190 +f 2341 2376 2333 +f 2066 877 2336 +f 2290 2277 2278 +f 739 634 592 +f 675 304 14 +f 2384 675 14 +f 2199 2211 2204 +f 2191 2199 2204 +f 2322 2318 2310 +f 2287 2262 2233 +f 2185 2188 2184 +f 2386 2425 845 +f 2384 572 675 +f 1128 123 2365 +f 832 971 2343 +f 2188 2186 2191 +f 2185 2184 2176 +f 2345 1917 2342 +f 2345 877 1917 +f 2336 2406 2440 +f 971 1100 2343 +f 2299 2289 2257 +f 2299 2303 2289 +f 2249 2255 2243 +f 506 513 512 +f 2437 955 1219 +f 1587 2398 1324 +f 877 2396 2336 +f 2336 2396 2406 +f 2463 2479 879 +f 2376 2412 2350 +f 2281 2267 2268 +f 2303 2330 2289 +f 624 635 159 +f 1996 2356 1561 +f 2449 2436 1996 +f 2356 2054 2451 +f 928 2398 1587 +f 2333 2350 2262 +f 2035 26 2456 +f 2346 2342 2425 +f 2346 2345 2342 +f 1544 2380 2418 +f 2412 2392 2350 +f 622 509 1151 +f 2436 2054 1996 +f 545 2451 928 +f 2326 2341 2333 +f 2346 2425 2386 +f 1365 2035 2456 +f 2369 2377 2353 +f 2369 506 2377 +f 2451 900 928 +f 900 2398 928 +f 1235 1888 1244 +f 2337 2345 2346 +f 877 772 2396 +f 772 1275 2396 +f 2432 2446 2414 +f 2294 2295 2310 +f 2369 2330 828 +f 2418 2419 2436 +f 2450 2429 2436 +f 2436 2429 2054 +f 2490 2494 1656 +f 1321 155 2338 +f 1256 2346 2386 +f 2448 877 2345 +f 877 2448 772 +f 2446 2423 2414 +f 2351 2334 2363 +f 2243 2254 2269 +f 2380 2419 2418 +f 2419 2450 2436 +f 2283 2278 2264 +f 822 2197 823 +f 1008 1759 1565 +f 2448 2345 2337 +f 2270 2293 2276 +f 2323 2324 2328 +f 2429 1012 2054 +f 2226 2243 2213 +f 2395 325 772 +f 2370 2367 2380 +f 2054 2435 2451 +f 2435 2397 2451 +f 2451 2397 900 +f 1774 1974 1975 +f 2305 2290 2283 +f 846 2305 2283 +f 2320 1215 2285 +f 2139 2448 2337 +f 2448 2395 772 +f 1232 1231 1216 +f 2272 2285 2284 +f 2367 2371 2380 +f 2371 2405 2380 +f 2380 2405 2419 +f 2419 2429 2450 +f 2429 176 1012 +f 2397 2373 900 +f 2373 2398 900 +f 2373 1379 2398 +f 2372 1500 1508 +f 1133 1303 1142 +f 2252 2273 2272 +f 891 2252 2272 +f 2419 2405 2429 +f 2405 2430 2429 +f 2429 2430 176 +f 2189 2186 2181 +f 2212 2219 2218 +f 2312 2139 2337 +f 2139 2384 2448 +f 2448 2384 2395 +f 899 855 843 +f 2272 2273 2285 +f 2331 2303 2299 +f 176 2435 2054 +f 1012 176 2054 +f 2177 2185 2176 +f 2218 2219 2225 +f 1216 1220 1215 +f 2378 2139 2312 +f 2384 14 2395 +f 2324 2295 2255 +f 2240 2273 2252 +f 2371 2387 2405 +f 2410 2430 2405 +f 2430 2442 176 +f 2435 2344 2397 +f 2397 2344 2373 +f 2456 1888 2455 +f 2242 2267 2233 +f 2233 2262 2229 +f 2378 2384 2139 +f 2323 2310 2295 +f 2323 2322 2310 +f 2240 2274 2273 +f 974 841 990 +f 2490 1447 2486 +f 2387 2410 2405 +f 2442 2141 176 +f 2344 1778 2373 +f 972 1379 2373 +f 1778 972 2373 +f 1379 972 428 +f 1211 2437 1223 +f 1228 1215 1220 +f 702 2378 2312 +f 17 518 2415 +f 1888 26 1244 +f 2324 2323 2295 +f 2305 2311 2290 +f 2307 2285 2273 +f 2274 2307 2273 +f 2307 2320 2285 +f 2369 531 506 +f 2435 2258 2344 +f 2296 2324 2288 +f 1233 1217 2361 +f 2360 2371 2367 +f 2410 2442 2430 +f 176 2141 2258 +f 176 2258 2435 +f 539 2331 66 +f 2350 2392 2317 +f 2268 2225 2253 +f 1508 1500 2371 +f 2360 1508 2371 +f 2371 1500 2387 +f 972 2366 428 +f 1626 1686 1358 +f 1759 1807 1819 +f 2277 2257 2245 +f 2277 2299 2257 +f 1784 2228 1736 +f 2265 2240 1736 +f 2228 2265 1736 +f 2265 2274 2240 +f 1209 2320 2307 +f 2320 1209 1216 +f 1555 1584 1560 +f 2387 1500 2372 +f 2410 2420 2442 +f 2433 972 1778 +f 2433 2366 972 +f 955 522 1225 +f 2339 2307 2274 +f 2372 1493 2387 +f 2411 2420 2410 +f 2420 954 2442 +f 2442 954 2141 +f 2344 2433 1778 +f 2205 2212 2218 +f 2328 2334 2351 +f 2394 2401 2414 +f 2250 2256 2271 +f 2339 1209 2307 +f 2328 2322 2323 +f 866 845 2425 +f 3 316 893 +f 2387 2411 2410 +f 2441 2141 954 +f 2141 2441 2258 +f 2354 2433 2344 +f 2254 2294 2270 +f 2269 2254 2270 +f 863 2305 846 +f 2441 2354 2258 +f 2258 2354 2344 +f 2319 2355 51 +f 2223 2228 1784 +f 1493 2411 2387 +f 1560 2449 1555 +f 2288 2324 2255 +f 825 2251 2238 +f 2251 2245 2238 +f 1299 84 1312 +f 2246 2265 2228 +f 2313 2274 2265 +f 2313 2339 2274 +f 2251 2277 2245 +f 2319 51 2331 +f 891 1862 2252 +f 2443 954 2420 +f 2443 2441 954 +f 511 2447 2439 +f 2242 2233 2211 +f 188 15 814 +f 2443 2426 2441 +f 2426 2354 2441 +f 2306 2403 2433 +f 2433 2403 2366 +f 539 2303 2331 +f 2246 2228 2223 +f 1030 1819 1807 +f 2354 2306 2433 +f 2413 2412 2376 +f 2438 2455 1888 +f 1848 1857 2176 +f 2207 2208 2223 +f 2208 2246 2223 +f 1209 2339 1217 +f 2339 2361 1217 +f 1221 1210 2388 +f 554 109 78 +f 386 1375 95 +f 2327 2326 2318 +f 2179 2182 1393 +f 2182 2208 1393 +f 1393 2208 2207 +f 2361 2399 2388 +f 2388 2399 1211 +f 2306 2354 2426 +f 2403 2359 2366 +f 2214 2226 2213 +f 2268 2253 2276 +f 889 2200 2179 +f 2200 2182 2179 +f 2200 2221 2182 +f 2221 2208 2182 +f 2314 2265 2246 +f 2314 2313 2265 +f 2339 2374 2361 +f 2478 2434 2379 +f 2205 2217 2199 +f 2208 2259 2246 +f 2259 2275 2246 +f 2314 2321 2313 +f 2313 2347 2339 +f 2347 2374 2339 +f 2374 2399 2361 +f 153 154 2426 +f 154 2306 2426 +f 2385 2359 2403 +f 2221 2259 2208 +f 2306 2357 2403 +f 2357 2385 2403 +f 2237 2256 2231 +f 2172 2180 889 +f 2180 2200 889 +f 2200 2201 2221 +f 2246 2291 2314 +f 2374 2444 2399 +f 571 555 2311 +f 2192 2205 2199 +f 2173 2180 2172 +f 2279 2246 2275 +f 2279 2291 2246 +f 2292 2314 2291 +f 2321 2362 2313 +f 2362 2347 2313 +f 2347 2389 2374 +f 2444 955 2437 +f 2292 2291 2279 +f 2452 2444 2374 +f 2054 2356 1996 +f 2338 2306 154 +f 2186 2192 2191 +f 2193 2201 2200 +f 2259 2221 2201 +f 2247 2259 2201 +f 2452 955 2444 +f 2278 2277 2251 +f 2338 2357 2306 +f 2181 2186 2185 +f 2276 2326 2281 +f 2432 2457 2446 +f 2198 2201 2193 +f 2198 2232 2201 +f 2232 2247 2201 +f 2389 2452 2374 +f 2452 1630 955 +f 1403 1749 1444 +f 1555 1996 1561 +f 2357 2427 2385 +f 2385 2428 230 +f 2409 2415 2424 +f 2304 2331 2299 +f 2193 2200 2180 +f 2445 2452 2389 +f 1565 1759 1927 +f 2380 1544 2370 +f 2338 2427 2357 +f 2427 2428 2385 +f 230 222 253 +f 2202 2198 2193 +f 2202 2209 2198 +f 2209 2241 2198 +f 2241 2232 2198 +f 2266 2275 2259 +f 2365 1340 1128 +f 2415 518 2424 +f 2338 170 2427 +f 170 2428 2427 +f 2181 2185 2177 +f 2196 2195 2189 +f 2183 2193 2180 +f 2453 1630 2452 +f 2197 2214 2189 +f 2401 2409 2414 +f 822 2220 2197 +f 1210 2361 2388 +f 2187 2193 2183 +f 2187 2202 2193 +f 2266 2279 2275 +f 2279 2300 2292 +f 2375 2347 2362 +f 2375 2390 2347 +f 2390 2389 2347 +f 2453 2452 2445 +f 1347 1630 2453 +f 1630 1347 522 +f 2220 2206 2197 +f 2262 2350 2286 +f 170 254 2428 +f 2457 1973 2446 +f 1973 1365 2446 +f 2174 2183 2180 +f 2194 2202 2187 +f 2222 2241 2209 +f 2222 2260 2241 +f 2266 2259 2247 +f 2390 2445 2389 +f 2264 2251 825 +f 2363 2368 2351 +f 2326 2393 2341 +f 1855 1850 1851 +f 2210 2209 2202 +f 2210 2222 2209 +f 2261 2260 2222 +f 2280 2279 2266 +f 2280 2300 2279 +f 251 263 2359 +f 2277 2304 2299 +f 2220 2230 2206 +f 2202 2194 2210 +f 2213 2243 2234 +f 2328 2327 2322 +f 2294 2310 2293 +f 2214 2196 2189 +f 2196 2213 2219 +f 2224 2222 2210 +f 2421 2390 2375 +f 2206 2230 2214 +f 2194 2203 2210 +f 2224 2261 2222 +f 2421 2445 2390 +f 2322 2327 2318 +f 2393 2408 2341 +f 1365 1973 510 +f 2216 2210 2203 +f 2216 2224 2210 +f 2266 2308 2280 +f 2280 2308 2300 +f 2407 2421 2375 +f 2175 2183 2174 +f 2194 2190 2203 +f 2454 2445 2421 +f 522 1347 523 +f 2456 2455 2423 +f 823 2197 2178 +f 2281 2333 2287 +f 2188 2187 2183 +f 2188 2190 2194 +f 2187 2188 2194 +f 2308 2315 2300 +f 2407 2375 2362 +f 2443 2420 2503 +f 2420 2411 2503 +f 2411 1493 2503 +f 1493 1487 2503 +f 1487 1318 2503 +f 1318 1320 2503 +f 1320 2443 2503 diff --git a/code/unsorted/3d/bunny.py b/code/unsorted/3d/bunny.py new file mode 100644 index 0000000..3f5989c --- /dev/null +++ b/code/unsorted/3d/bunny.py @@ -0,0 +1,43 @@ +import glm +import plot +import numpy as np +import matplotlib.pyplot as plt + + +# Wavefront loader (only vertices and faces) +def obj_load(filename): + V, Vi = [], [] + with open(filename) as f: + for line in f.readlines(): + if line.startswith("#"): + continue + values = line.split() + if not values: + continue + if values[0] == "v": + V.append([float(x) for x in values[1:4]]) + elif values[0] == "f": + Vi.append([int(x) for x in values[1:4]]) + return np.array(V), np.array(Vi) - 1 + + +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1) +ax.axis("off") + +camera = glm.camera(20, 45, 1.15, "perspective") +plot.axis(ax, camera) + +V, F = obj_load("bunny.obj") +V = glm.fit_unit_cube(V) +plot.mesh( + ax, + camera, + V, + F, + cmap=plt.get_cmap("magma"), + linewidth=0.5, + edgecolor=(0, 0, 0, 0.5), +) + +plt.show() diff --git a/code/unsorted/3d/contour.py b/code/unsorted/3d/contour.py new file mode 100644 index 0000000..5ff6cf0 --- /dev/null +++ b/code/unsorted/3d/contour.py @@ -0,0 +1,34 @@ +import glm +import plot +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + + n = 32 + X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + ) + Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + def f(x, y): + return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2) - y ** 2) + + n = 100 + x = np.linspace(-3, 3, n) + y = np.linspace(-3, 3, n) + X, Y = np.meshgrid(x, y) + Z = 0.5 * f(X, Y) + + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_axes([0, 0, 1, 1]) + + camera = glm.camera(25, 45, 1, "perspective") + plot.axis(ax, camera) + plot.contour(ax, camera, Z) + +plt.show() diff --git a/code/unsorted/3d/glm.py b/code/unsorted/3d/glm.py new file mode 100644 index 0000000..0156edf --- /dev/null +++ b/code/unsorted/3d/glm.py @@ -0,0 +1,138 @@ +import numpy as np + + +def normalize(X): + return X / (1e-16 + np.sqrt((np.array(X) ** 2).sum(axis=-1)))[..., np.newaxis] + + +def rescale(V, vmin=0, vmax=1): + Vmin, Vmax = V.min(), V.max() + return vmin + (vmax - vmin) * (V - Vmin) / (Vmax - Vmin) + + +def center(V): + return V - (V.min(axis=-1) + V.max(axis=-1)) / 2 + + +def clip(V, vmin=0, vmax=1): + return np.minimum(np.maximum(V, vmin), vmax) + + +def degrees(angle): + return 180 * angle / np.pi + + +def radians(angle): + return np.pi * angle / 180 + + +def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + +def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5 * radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + + +def ortho(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 / (right - left) + M[1, 1] = +2.0 / (top - bottom) + M[2, 2] = -2.0 / (zfar - znear) + M[3, 3] = 1.0 + M[0, 2] = -(right + left) / float(right - left) + M[1, 3] = -(top + bottom) / float(top - bottom) + M[2, 3] = -(zfar + znear) / float(zfar - znear) + return M + + +def scale(x=1, y=1, z=1): + return np.array( + [[x, 0, 0, 0], [0, y, 0, 0], [0, 0, z, 0], [0, 0, 0, 1]], dtype=np.float32 + ) + + +def translate(x=0, y=0, z=0): + return np.array( + [[1, 0, 0, x], [0, 1, 0, y], [0, 0, 1, z], [0, 0, 0, 1]], dtype=np.float32 + ) + + +def xrotate(theta=0): + t = radians(theta) + c, s = np.cos(t), np.sin(t) + return np.array( + [[1, 0, 0, 0], [0, c, -s, 0], [0, s, c, 0], [0, 0, 0, 1]], dtype=np.float32 + ) + + +def yrotate(theta=0): + t = radians(theta) + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]], dtype=np.float32 + ) + + +def zrotate(theta=0): + t = radians(theta) + c, s = np.cos(t), np.sin(t) + return np.array( + [[c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=np.float32 + ) + + +def fit_unit_cube(V): + xmin, xmax = V[:, 0].min(), V[:, 0].max() + ymin, ymax = V[:, 1].min(), V[:, 1].max() + zmin, zmax = V[:, 2].min(), V[:, 2].max() + scale = max([xmax - xmin, ymax - ymin, zmax - zmin]) + V /= scale + V[:, 0] -= (xmax + xmin) / 2 / scale + V[:, 1] -= (ymax + ymin) / 2 / scale + V[:, 2] -= (zmax + zmin) / 2 / scale + return V + + +def transform(V, mvp): + V = np.asarray(V) + shape = V.shape + V = V.reshape(-1, 3) + ones = np.ones(len(V), dtype=np.float32) + V = np.c_[V.astype(np.float32), ones] # Homogenous coordinates + V = V @ mvp.T # Transformed coordinates + V = V / V[:, 3].reshape(-1, 1) # Normalization + V = V[:, :3] # Normalized device coordinates + return V.reshape(shape) + + +def frontback(T): + Z = ( + (T[:, 1, 0] - T[:, 0, 0]) * (T[:, 1, 1] + T[:, 0, 1]) + + (T[:, 2, 0] - T[:, 1, 0]) * (T[:, 2, 1] + T[:, 1, 1]) + + (T[:, 0, 0] - T[:, 2, 0]) * (T[:, 0, 1] + T[:, 2, 1]) + ) + return Z < 0, Z >= 0 + + +def camera(xrotation=25, yrotation=45, zoom=1, mode="perspective"): + xrotation = min(max(xrotation, 0), 90) + yrotation = min(max(yrotation, 0), 90) + zoom = max(0.1, zoom) + model = scale(zoom, zoom, zoom) @ xrotate(xrotation) @ yrotate(yrotation) + view = translate(0, 0, -4.5) + if mode == "ortho": + proj = ortho(-1, +1, -1, +1, 1, 100) + else: + proj = perspective(25, 1, 1, 100) + return proj @ view @ model diff --git a/code/unsorted/3d/platonic-solids.pdf b/code/unsorted/3d/platonic-solids.pdf new file mode 100644 index 0000000..7f2bd13 Binary files /dev/null and b/code/unsorted/3d/platonic-solids.pdf differ diff --git a/code/unsorted/3d/platonic-solids.png b/code/unsorted/3d/platonic-solids.png new file mode 100644 index 0000000..620af2e Binary files /dev/null and b/code/unsorted/3d/platonic-solids.png differ diff --git a/code/unsorted/3d/platonic-solids.py b/code/unsorted/3d/platonic-solids.py new file mode 100644 index 0000000..dc6c94c --- /dev/null +++ b/code/unsorted/3d/platonic-solids.py @@ -0,0 +1,334 @@ +import glm +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.collections import PolyCollection + + +def tetrahedron(): + """ Tetrahedron with 4 faces, 6 edges and 4 vertices """ + a = 2 * np.pi / 3 + vertices = [ + (0, 0.5, 0), + (0.5 * np.cos(0 * a), -0.25, 0.5 * np.sin(0 * a)), + (0.5 * np.cos(1 * a), -0.25, 0.5 * np.sin(1 * a)), + (0.5 * np.cos(2 * a), -0.25, 0.5 * np.sin(2 * a)), + ] + faces = [(1, 2, 3), (1, 2, 0), (2, 3, 0), (3, 1, 0)] + return np.array(vertices), np.array(faces) + + +def octahedron(): + """ Octahedron with 8 faces, 12 edges and 6 vertices """ + + r = 0.5 * 1 / np.sqrt(2) + vertices = [ + (0, 0.5, 0), + (0, -0.5, 0), + (-r, 0, -r), + (r, 0, -r), + (r, 0, r), + (-r, 0, r), + ] + faces = [ + (2, 3, 0), + (3, 4, 0), + (4, 5, 0), + (5, 2, 0), + (3, 2, 1), + (4, 3, 1), + (5, 4, 1), + (2, 5, 1), + ] + return np.array(vertices), np.array(faces) + + +def dodecahedron(): + """ Regular dodecahedron with 12 faces, 30 edges and 20 vertices """ + + r = (1 + np.sqrt(5)) / 2 + vertices = [ + (-1, -1, +1), + (r, 1 / r, 0), + (r, -1 / r, 0), + (-r, 1 / r, 0), + (-r, -1 / r, 0), + (0, r, 1 / r), + (0, r, -1 / r), + (1 / r, 0, -r), + (-1 / r, 0, -r), + (0, -r, -1 / r), + (0, -r, 1 / r), + (1 / r, 0, r), + (-1 / r, 0, r), + (+1, +1, -1), + (+1, +1, +1), + (-1, +1, -1), + (-1, +1, +1), + (+1, -1, -1), + (+1, -1, +1), + (-1, -1, -1), + ] + # faces = [ (19, 3, 2), (12, 19, 2), (15, 12, 2), + # (8, 14, 2), (18, 8, 2), (3, 18, 2), + # (20, 5, 4), (9, 20, 4), (16, 9, 4), + # (13, 17, 4), (1, 13, 4), (5, 1, 4), + # (7, 16, 4), (6, 7, 4), (17, 6, 4), + # (6, 15, 2), (7, 6, 2), (14, 7, 2), + # (10, 18, 3), (11, 10, 3), (19, 11, 3), + # (11, 1, 5), (10, 11, 5), (20, 10, 5), + # (20, 9, 8), (10, 20, 8), (18, 10, 8), + # (9, 16, 7), (8, 9, 7), (14, 8, 7), + # (12, 15, 6), (13, 12, 6), (17, 13, 6), + # (13, 1, 11), (12, 13, 11), (19, 12, 11) ] + faces = [ + (19, 3, 2, 15, 12), + (8, 14, 2, 3, 18), + (20, 5, 4, 16, 9), + (13, 17, 4, 5, 1), + (7, 16, 4, 17, 6), + (6, 15, 2, 14, 7), + (10, 18, 3, 19, 11), + (11, 1, 5, 20, 10), + (20, 9, 8, 18, 10), + (9, 16, 7, 14, 8), + (12, 15, 6, 17, 13), + (13, 1, 11, 19, 12), + ] + vertices = np.array(vertices) / np.sqrt(3) / 2 + faces = np.array(faces) - 1 + return vertices, faces + + +def icosahedron(): + """ Regular icosahedron with 20 faces, 30 edges and 12 vertices """ + + a = (1 + np.sqrt(5)) / 2 + vertices = [ + (-1, a, 0), + (1, a, 0), + (-1, -a, 0), + (1, -a, 0), + (0, -1, a), + (0, 1, a), + (0, -1, -a), + (0, 1, -a), + (a, 0, -1), + (a, 0, 1), + (-a, 0, -1), + (-a, 0, 1), + ] + faces = [ + [0, 11, 5], + [0, 5, 1], + [0, 1, 7], + [0, 7, 10], + [0, 10, 11], + [1, 5, 9], + [5, 11, 4], + [11, 10, 2], + [10, 7, 6], + [7, 1, 8], + [3, 9, 4], + [3, 4, 2], + [3, 2, 6], + [3, 6, 8], + [3, 8, 9], + [4, 9, 5], + [2, 4, 11], + [6, 2, 10], + [8, 6, 7], + [9, 8, 1], + ] + vertices = np.array(vertices) / np.sqrt(a + 2) / 2 + faces = np.array(faces) + return vertices, faces + + +def cube(): + vertices = [ + (0, 0, 0), + (1, 0, 0), + (1, 0, 1), + (0, 0, 1), + (0, 1, 0), + (1, 1, 0), + (1, 1, 1), + (0, 1, 1), + ] + faces = [ + [0, 1, 2, 3], + [4, 5, 6, 7], + [0, 1, 5, 4], + [1, 2, 6, 5], + [2, 3, 7, 6], + [3, 0, 4, 7], + ] + return (np.array(vertices) - 0.5) / np.sqrt(2), np.array(faces) + + +def plot(ax, camera, V, F): + ax.axis("off") + + T = glm.transform(V[F], camera) + V, Z = T[..., :2], T[..., 2].mean(axis=-1) + V = V[np.argsort(-Z)] + collection = PolyCollection( + V, + antialiased=True, + linewidth=1.0, + facecolor=(0.9, 0.9, 1, 0.75), + edgecolor=(0, 0, 0.75, 0.25), + ) + ax.add_collection(collection) + + +fig = plt.figure(figsize=(8, 5.5)) +camera = glm.camera(25, 35, 1.5, "perspective") + +ax = plt.subplot(2, 3, 1, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +ax.axis("off") +ax.text( + 0.5, + 0.6, + "PLATONIC", + transform=ax.transAxes, + weight="black", + size=24, + va="bottom", + ha="center", + family="Source Sans Pro", +) +ax.text( + 0.5, + 0.6, + "S O L I D S", + transform=ax.transAxes, + weight="light", + size=22, + va="top", + ha="center", + family="Source Sans Pro", +) +ax.text( + 0.5, + 0.475, + "matplotlib.org", + transform=ax.transAxes, + weight="light", + size=13, + va="top", + ha="center", + family="Source Code Pro", +) + + +# ----------------------------------------------------------------------------- +ax = plt.subplot(2, 3, 2, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +plot(ax, camera, *tetrahedron()) +ax.text( + 0.5, + 1.0, + "Tetrahedron", + transform=ax.transAxes, + weight="bold", + va="bottom", + ha="center", +) +ax.text( + 0.5, + 1.0, + "4 faces, 4 vertices, 6 edges", + transform=ax.transAxes, + va="top", + ha="center", + size="x-small", +) + +# ----------------------------------------------------------------------------- +ax = plt.subplot(2, 3, 3, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +plot(ax, camera, *cube()) +ax.text( + 0.5, 1.0, "Cube", transform=ax.transAxes, weight="bold", va="bottom", ha="center" +) +ax.text( + 0.5, + 1.0, + "6 faces, 8 vertices, 12 edges", + transform=ax.transAxes, + va="top", + ha="center", + size="x-small", +) + +# ----------------------------------------------------------------------------- +ax = plt.subplot(2, 3, 4, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +plot(ax, camera, *octahedron()) +ax.text( + 0.5, + 1.0, + "Octahedron", + transform=ax.transAxes, + weight="bold", + va="bottom", + ha="center", +) +ax.text( + 0.5, + 1.0, + "8 faces, 6 vertices, 12 edges", + transform=ax.transAxes, + va="top", + ha="center", + size="x-small", +) + +# ----------------------------------------------------------------------------- +ax = plt.subplot(2, 3, 5, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +plot(ax, camera, *dodecahedron()) +ax.text( + 0.5, + 1.0, + "Dodecahedron", + transform=ax.transAxes, + weight="bold", + va="bottom", + ha="center", +) +ax.text( + 0.5, + 1.0, + "12 faces, 20 vertices, 30 edges", + transform=ax.transAxes, + va="top", + ha="center", + size="x-small", +) + +# ----------------------------------------------------------------------------- +ax = plt.subplot(2, 3, 6, xlim=[-1, +1], ylim=[-1, +1], aspect=1) +plot(ax, camera, *icosahedron()) +ax.text( + 0.5, + 1.0, + "Icosahedron", + transform=ax.transAxes, + weight="bold", + va="bottom", + ha="center", +) +ax.text( + 0.5, + 1.0, + "20 faces, 12 vertices, 30 edges", + transform=ax.transAxes, + va="top", + ha="center", + size="x-small", +) + + +plt.tight_layout() +plt.savefig("platonic-solids.png", dpi=300) +plt.savefig("platonic-solids.pdf") +plt.show() diff --git a/code/unsorted/3d/plot.py b/code/unsorted/3d/plot.py new file mode 100644 index 0000000..01b08f8 --- /dev/null +++ b/code/unsorted/3d/plot.py @@ -0,0 +1,595 @@ +import glm +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.textpath import TextPath +from matplotlib.patches import PathPatch +from matplotlib.collections import PolyCollection + + +# ----------------------------------------------------------------------------- +def axis(ax, camera, ticks=True): + """ + Draw three dimension axis with ticks and tick labels. + + Parameters: + ----------- + + ax : matplotlib.axes instance + The regulmat axes where to draw + + camera : 4x4 numpy array + A transformation matrix in homogenous coordinates (4x4) + + ticks : bool + Whether to draw tick and tick labels + + Note: + ----- + + The axis is drawn inside the unit cube centered on the origin: + + - XZ lies on the plane Y = -0.5 + - XY lies on the plane Z = -0.5 + - YZ lies on the plane X = +0.5 + - xticks goes from (-0.5, -0.5, +0.5) to (+0.5, -0.5, +0.5) + - zticks goes from (-0.5, -0.5, -0.5) to (-0.5, -0.5, +0.5) + - yticks goes from (-0.5, -0.5, -0.5) to (-0.5, +0.5, -0.5) + """ + + # Mandatory settings for matplotlib axes + # -------------------------------------- + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + ax.set_aspect(1) + ax.axis("off") + + # Parameters + # --------------------------------- + axis_linewidth = 1.00 + grid_linewidth = 0.50 + ticks_linewidth = 0.75 + + grid_color = "0.5" + axis_color = "0.0" + ticks_color = "0.0" + ticks_length = 0.025 + ticks_pad = 0.05 + + # ticks and ticklabels + nx = 11 + xticks = np.linspace(-0.5, +0.5, nx, endpoint=True) + xticklabels = ["%.1f" % x for x in xticks] + + ny = 11 + yticks = np.linspace(-0.5, +0.5, ny, endpoint=True) + yticklabels = ["%.1f" % y for y in yticks] + + nz = 11 + zticks = np.linspace(-0.5, +0.5, nz, endpoint=True) + zticklabels = ["%.1f" % z for z in zticks] + + colors = [] + segments = [] + linewidths = [] + + # XZ axis + # --------------------------------- + X0 = np.linspace([0, 0, 0], [1, 0, 0], nx) - 0.5 + X1 = X0 + [0, 0, 1] + X2 = X1 + [0, 0, ticks_length] + X3 = X2 + [0, 0, ticks_pad] + X0 = glm.transform(X0, camera)[..., :2] + X1 = glm.transform(X1, camera)[..., :2] + X2 = glm.transform(X2, camera)[..., :2] + X3 = glm.transform(X3, camera)[..., :2] + + Z0 = np.linspace([0, 0, 0], [0, 0, 1], nz) - 0.5 + Z1 = Z0 + [1, 0, 0] + Z2 = Z0 - [ticks_length, 0, 0] + Z3 = Z2 - [ticks_pad, 0, 0] + Z0 = glm.transform(Z0, camera)[..., :2] + Z1 = glm.transform(Z1, camera)[..., :2] + Z2 = glm.transform(Z2, camera)[..., :2] + Z3 = glm.transform(Z3, camera)[..., :2] + + for p0, p1, p2, p3, label in zip(X0, X1, X2, X3, xticklabels): + # grid line + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + if not ticks: + continue + + # tick + segments.append([p1, p2]) + linewidths.append(ticks_linewidth) + colors.append(ticks_color) + + # label + ax.text(p3[0], p3[1], label, ha="center", va="center", size="x-small") + + for p0, p1, p2, p3, label in zip(Z0, Z1, Z2, Z3, zticklabels): + # grid lines + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + if not ticks: + continue + + # ticks + segments.append([p0, p2]) + linewidths.append(ticks_linewidth) + colors.append(ticks_color) + + # label + ax.text(p3[0], p3[1], label, ha="center", va="center", size="x-small") + + # axis border + segments.append([X0[0], X0[-1], X1[-1], X1[0], X0[0]]) + linewidths.append(axis_linewidth) + colors.append(axis_color) + + # XY axis + # --------------------------------- + X0 = np.linspace([0, 0, 0], [1, 0, 0], nx) - 0.5 + X1 = X0 + [0, 1, 0] + X0 = glm.transform(X0, camera)[..., :2] + X1 = glm.transform(X1, camera)[..., :2] + Y0 = np.linspace([0, 0, 0], [0, 1, 0], ny) - 0.5 + Y1 = Y0 + [1, 0, 0] + Y2 = Y0 - [ticks_length, 0, 0] + Y3 = Y2 - [ticks_pad, 0, 0] + Y0 = glm.transform(Y0, camera)[..., :2] + Y1 = glm.transform(Y1, camera)[..., :2] + Y2 = glm.transform(Y2, camera)[..., :2] + Y3 = glm.transform(Y3, camera)[..., :2] + + for p0, p1 in zip(X0, X1): + # grid lines + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + for p0, p1, p2, p3, label in zip(Y0, Y1, Y2, Y3, yticklabels): + # grid lines + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + if not ticks: + continue + # ticks + segments.append([p0, p2]) + linewidths.append(ticks_linewidth) + colors.append(ticks_color) + + # label + ax.text(p3[0], p3[1], label, ha="center", va="center", size="x-small") + + # axis border + segments.append([X0[0], X0[-1], X1[-1], X1[0], X0[0]]) + linewidths.append(axis_linewidth) + colors.append(axis_color) + + # ZY axis + # --------------------------------- + Z0 = np.linspace([1, 0, 0], [1, 0, 1], nx) - 0.5 + Z1 = Z0 + [0, 1, 0] + Z0 = glm.transform(Z0, camera)[..., :2] + Z1 = glm.transform(Z1, camera)[..., :2] + + Y0 = np.linspace([1, 0, 0], [1, 1, 0], ny) - 0.5 + Y1 = Y0 + [0, 0, 1] + Y0 = glm.transform(Y0, camera)[..., :2] + Y1 = glm.transform(Y1, camera)[..., :2] + + for p0, p1 in zip(Z0, Z1): + # grid lines + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + for p0, p1 in zip(Y0, Y1): + # grid lines + segments.append([p0, p1]) + linewidths.append(grid_linewidth) + colors.append(grid_color) + + # axis border + segments.append([Y0[0], Y0[-1], Y1[-1], Y1[0], Y0[0]]) + linewidths.append(axis_linewidth) + colors.append(axis_color) + + # Actual rendering + collection = PolyCollection( + segments, + closed=False, + clip_on=False, + linewidths=linewidths, + facecolors="None", + edgecolor=colors, + ) + ax.add_collection(collection) + + +# ----------------------------------------------------------------------------- +def mesh( + ax, + camera, + vertices, + faces, + cmap=None, + facecolor="white", + edgecolor="none", + linewidth=1.0, + mode="all", +): + + # Mandatory settings for matplotlib axes + # -------------------------------------- + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + ax.set_aspect(1) + ax.axis("off") + + facecolor = mpl.colors.to_rgba_array(facecolor) + edgecolor = mpl.colors.to_rgba_array(edgecolor) + + T = glm.transform(vertices, camera)[faces] + Z = -T[:, :, 2].mean(axis=1) + + # Facecolor using depth buffer + if cmap is not None: + cmap = plt.get_cmap("magma") + norm = mpl.colors.Normalize(vmin=Z.min(), vmax=Z.max()) + facecolor = cmap(norm(Z)) + + # Back face culling + if mode == "front": + front, back = glm.frontback(T) + T, Z = T[front], Z[front] + if len(facecolor) == len(faces): + facecolor = facecolor[front] + if len(edgecolor) == len(faces): + facecolor = edgecolor[front] + # Front face culling + elif mode == "back": + front, back = glm.frontback(T) + T, Z = T[back], Z[back] + if len(facecolor) == len(faces): + facecolor = facecolor[back] + if len(edgecolor) == len(faces): + facecolor = edgecolor[back] + + # Separate 2d triangles from zbuffer + triangles = T[:, :, :2] + antialiased = True + if linewidth == 0.0: + antialiased = False + + # Sort triangles according to z buffer + I = np.argsort(Z) + triangles = triangles[I, :] + if len(facecolor) == len(I): + facecolor = facecolor[I, :] + if len(edgecolor) == len(I): + edgecolor = edgecolor[I, :] + + collection = PolyCollection( + triangles, + linewidth=linewidth, + antialiased=antialiased, + facecolor=facecolor, + edgecolor=edgecolor, + ) + ax.add_collection(collection) + + +# ----------------------------------------------------------------------------- +def surf( + ax, + camera, + Y, + facecolor="white", + edgecolor="black", + facecolors=None, + edgecolors=None, + linewidth=0.5, + shading=(1.00, 1.00, 1.00, 0.75, 0.50, 1.00), +): + + # Mandatory settings for matplotlib axes + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + ax.set_aspect(1) + ax.axis("off") + + # Facecolor + if facecolors is None: + facecolors = np.zeros((Y.shape[0], Y.shape[1], 3)) + facecolors[...] = mpl.colors.to_rgb(facecolor) + facecolors[...] = facecolors[..., :3] + + # Facecolor + if edgecolors is None: + edgecolors = np.zeros((Y.shape[0], Y.shape[1], 3)) + edgecolors[...] = mpl.colors.to_rgb(edgecolor) + edgecolors[...] = edgecolors[..., :3] + + # Surface + n = Y.shape[0] + T = np.linspace(-0.5, +0.5, n) + X, Z = np.meshgrid(T, T) + V = np.c_[X.ravel(), Y.ravel() - 0.5, Z.ravel()] + F = (np.arange((n - 1) * (n)).reshape(n - 1, n))[:, :-1].T + F = np.repeat(F.ravel(), 6).reshape(n - 1, n - 1, 6) + F[:, :] += np.array([0, n + 1, 1, 0, n, n + 1]) + F = F.reshape(-1, 3) + V = glm.transform(V, camera) + T = V[F] + + # Create list from array such that we can add lines + polys = T[..., :2].tolist() + zbuffer = (-T[..., 2].mean(axis=1)).tolist() + edgecolors = ["none",] * len(polys) + antialiased = [False,] * len(polys) + linewidths = [1.0,] * len(polys) + facecolors = ((facecolors.reshape(-1, 3))[F].mean(axis=1)).tolist() + + # Helper function for creating a line segment between two points + def segment(p0, p1, linewidth=1.5, epsilon=0.01): + polys.append([p0[:2], p1[:2]]) + facecolors.append("none") + edgecolors.append("black") + antialiased.append(True) + linewidths.append(linewidth) + zbuffer.append(-(p0[2] + p1[2]) / 2 + epsilon) + + # Border + grid over the surface + V = V.reshape(n, n, -1) + + # Border + for i in range(0, n - 1): + segment(V[i, 0], V[i + 1, 0]) + segment(V[i, -1], V[i + 1, -1]) + segment(V[0, i], V[0, i + 1]) + segment(V[-1, i], V[-1, i + 1]) + + for i in range(0, n - 1): + for j in range(0, n - 1): + segment(V[i, j], V[i + 1, j], 0.5) + segment(V[j, i], V[j, i + 1], 0.5) + + # Sort everything + I = np.argsort(zbuffer) + polys = [polys[i] for i in I] + facecolors = [facecolors[i] for i in I] + edgecolors = [edgecolors[i] for i in I] + linewidths = [linewidths[i] for i in I] + antialiased = [antialiased[i] for i in I] + + # Display + collection = PolyCollection( + polys, + linewidth=linewidths, + antialiased=antialiased, + facecolors=facecolors, + edgecolors=edgecolors, + ) + ax.add_collection(collection) + + +# ----------------------------------------------------------------------------- +def bar( + ax, + camera, + Y, + facecolor="white", + edgecolor="black", + facecolors=None, + edgecolors=None, + linewidth=0.5, + shading=(1.00, 1.00, 1.00, 0.75, 0.50, 1.00), +): + + # Mandatory settings for matplotlib axes + ax.set_xlim(-1, 1) + ax.set_ylim(-1, 1) + ax.set_aspect(1) + ax.axis("off") + + # Facecolor + if facecolors is None: + facecolors = np.zeros((Y.shape[0], Y.shape[1], 3)) + facecolors[...] = mpl.colors.to_rgb(facecolor) + facecolors[...] = facecolors[..., :3] + + # Facecolor + if edgecolors is None: + edgecolors = np.zeros((Y.shape[0], Y.shape[1], 3)) + edgecolors[...] = mpl.colors.to_rgb(edgecolor) + edgecolors[...] = edgecolors[..., :3] + + # Here we compute the eight 3D vertices necessary to render a bar + shape = Y.shape + dx, dz = 0.5 / shape[0], 0.5 / shape[1] + X, Z = np.meshgrid( + np.linspace(-0.5 + dx, 0.5 - dx, shape[0]), + np.linspace(-0.5 + dz, 0.5 - dz, shape[1]), + ) + P = np.c_[X.ravel(), np.zeros(X.size), Z.ravel()] + P = P.reshape(shape[0], shape[1], 3) + dx = np.array([dx, 0, 0]) + dz = np.array([0, 0, dz]) + dy = np.zeros((shape[0], shape[1], 3)) + V = np.zeros((8, shape[0], shape[1], 3)) + + dy[..., 1] = -0.5 + V[0] = P + dx - dz + dy + V[1] = P - dx - dz + dy + V[2] = P + dx + dz + dy + V[3] = P - dx + dz + dy + + dy[..., 1] = -0.5 + Y + V[4] = P + dx - dz + dy + V[5] = P - dx - dz + dy + V[6] = P + dx + dz + dy + V[7] = P - dx + dz + dy + + # Transformation of the vertices in 2D + z + T = glm.transform(V, camera) + V = T[:, :2].reshape(8, shape[0], shape[1], 2) + Z = -T[:, 2].reshape(8 * shape[0] * shape[1]) + + # Normalization of the z value such that we can maipulate zbar / zface + # Drawback is that it cannot be sorted anymore with other 3d objects + Z = glm.rescale(Z, 0, 1) + + # Building of individual bars (without bottom face) + # and a new z buffer that is a combination of the bar and face mean z + faces, colors, zbuffer = [], [], [] + indices = ( + [4, 5, 7, 6], # +Y + [0, 1, 3, 2], # -Y + [0, 1, 5, 4], # +X + [1, 3, 7, 5], # -X + [2, 3, 7, 6], # -Z + [0, 2, 6, 4], + ) # +Z + FC = np.zeros((shape[0], shape[1], len(indices), 3)) + EC = np.zeros((shape[0], shape[1], len(indices), 3)) + F = np.zeros((shape[0], shape[1], len(indices), 4, 2)) + Z_ = np.zeros((shape[0], shape[1], len(indices))) + + # This could probably be vectorized but it might be tricky + for i in range(shape[0]): + for j in range(shape[1]): + index = np.arange(8) * shape[0] * shape[1] + i * shape[1] + j + zbar = 1 * Z[index].mean() + for k, ((v0, v1, v2, v3), shade) in enumerate(zip(indices, shading)): + zface = (Z[index[v0]] + Z[index[v1]] + Z[index[v2]] + Z[index[v3]]) / 4 + F[i, j, k] = V[v0, i, j], V[v1, i, j], V[v2, i, j], V[v3, i, j] + Z_[i, j, k] = 10 * zbar + zface + FC[i, j, k] = facecolors[i, j] * shade + EC[i, j, k] = edgecolors[i, j] + + # Final reshape for sorting quads + Z = Z_.reshape(shape[0] * shape[1] * len(indices)) + F = F.reshape(shape[0] * shape[1] * len(indices), 4, 2) + FC = FC.reshape(shape[0] * shape[1] * len(indices), 3) + EC = EC.reshape(shape[0] * shape[1] * len(indices), 3) + I = np.argsort(Z) + + collection = PolyCollection( + F[I], linewidth=0.25, facecolors=FC[I], edgecolors=EC[I] + ) + ax.add_collection(collection) + + +# ----------------------------------------------------------------------------- +def contour(ax, camera, Y, n_levels=32): + + n = Y.shape[0] + T = np.linspace(-0.5, +0.5, n) + X, Z = np.meshgrid(T, T) + C = ax.contour(X, Z, Y, n_levels) + + cmap = plt.get_cmap("magma") + ymin, ymax = Y.min(), Y.max() + norm = mpl.colors.Normalize(vmin=2 * ymin, vmax=ymax) + + dy = 0.99 * (ymax - ymin) / n_levels + + segments = [] + facecolors = [] + edgecolors = [] + closed = [] + antialiased = [] + epsilon = 0.0025 + + for level, collection in zip(C.levels, C.collections): + + local_segments = [] + local_zbuffer = [] + local_antialiased = [] + local_edgecolors = [] + local_facecolors = [] + local_closed = [] + + # Collect and transform all paths + paths = [] + for path in collection.get_paths(): + V = np.array(path.vertices) + V = np.c_[V[:, 0], level * np.ones(len(path)), V[:, 1]] + V0 = V + V1 = V0 - [0, dy, 0] + T0 = glm.transform(V0, camera) + T1 = glm.transform(V1, camera) + V0, Z0 = T0[:, :2], T0[:, 2] + V1, Z1 = T1[:, :2], T1[:, 2] + paths.append([V, V0, Z0, V1, Z1]) + + for (V, V0, Z0, V1, Z1) in paths: + + for i in range(len(V0) - 1): + local_segments.append([V0[i], V0[i + 1], V1[i + 1], V1[i]]) + local_zbuffer.append(-(Z0[i] + Z0[i + 1] + Z1[i + 1] + Z1[i]) / 4) + local_antialiased.append(False) + local_edgecolors.append("none") + facecolor = np.array(cmap(norm(level))[:3]) + local_facecolors.append(0.75 * facecolor) + local_closed.append(True) + + local_segments.append([V1[i + 1], V1[i]]) + local_zbuffer.append(-(Z1[i + 1] + Z1[i]) / 2 + epsilon) + local_antialiased.append(True) + facecolor = np.array(cmap(norm(level))[:3]) + local_edgecolors.append(facecolor * 0.25) + local_facecolors.append("none") + local_closed.append(False) + + if ((V[0] - V[-1]) ** 2).sum() > 0.00001: + local_segments.append([V0[i + 1], V0[i]]) + local_zbuffer.append(-(Z0[i + 1] + Z0[i]) / 2 + epsilon) + local_antialiased.append(True) + facecolor = np.array(cmap(norm(level))[:3]) + local_edgecolors.append(facecolor * 0.25) + local_facecolors.append("none") + local_closed.append(False) + + I = np.argsort(local_zbuffer) + local_segments = [local_segments[i] for i in I] + local_facecolors = [local_facecolors[i] for i in I] + local_edgecolors = [local_edgecolors[i] for i in I] + local_closed = [local_closed[i] for i in I] + local_antialiased = [local_antialiased[i] for i in I] + + segments.extend(local_segments) + facecolors.extend(local_facecolors) + edgecolors.extend(local_edgecolors) + closed.extend(local_closed) + antialiased.extend(local_antialiased) + + for (V, V0, Z0, V1, Z1) in paths: + if ((V[0] - V[-1]) ** 2).sum() < 0.00001: + segments.append(V0) + antialiased.append(True) + facecolor = cmap(norm(level))[:3] + facecolors.append(facecolor) + edgecolors.append("black") + closed.append(True) + + collection.remove() + + collection = PolyCollection( + segments, + linewidth=0.5, + antialiased=antialiased, + closed=closed, + facecolors=facecolors, + edgecolors=edgecolors, + ) + ax.add_collection(collection) diff --git a/code/unsorted/3d/scatter.py b/code/unsorted/3d/scatter.py new file mode 100644 index 0000000..872afb0 --- /dev/null +++ b/code/unsorted/3d/scatter.py @@ -0,0 +1,76 @@ +import glm +import plot +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + from matplotlib.patches import Ellipse + from matplotlib.collections import PolyCollection + + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_axes([0, 0, 1, 1]) + camera = glm.camera(25, 45, 1, "perspective") + plot.axis(ax, camera) + + np.random.seed(1) + n = 1024 + P = 0.2 * np.random.normal(0, 1, (n, 3)) + + # Bottom shadow + V = glm.transform(P * [1, 0, 1] - [0, 0.5, 0], camera) + T = np.linspace(0, 2 * np.pi, 12) + radius = 0.015 + X, Y, Z = radius * np.cos(T), np.zeros(len(T)), radius * np.sin(T) + C = np.c_[X, Y, Z] + polys = [] + for i in range(n): + V = glm.transform(C + [P[i, 0], -0.5, P[i, 2]], camera)[:, :2] + polys.append(V) + + collection = PolyCollection( + polys, linewidths=0, alpha=0.5, zorder=+10, facecolors="0.5", edgecolor="none" + ) + ax.add_collection(collection) + + # Actual scatter + V = glm.transform(P, camera) + X, Y, Z = V[:, 0], V[:, 1], V[:, 2] + I = np.argsort(Z) + X, Y = X[I], Y[I] + facecolor = [ + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + mpl.colors.to_rgba("C4"), + (1, 1, 1, 0.25), + (1, 1, 1, 1), + ] * len(X) + edgecolor = [ + (0, 0, 0, 0.05), + (0, 0, 0, 0.10), + (0, 0, 0, 0.15), + (0, 0, 0, 1.00), + (0, 0, 0, 0.00), + (0, 0, 0, 0.00), + ] * len(X) + linewidth = [6, 4, 2, 0.5, 0.0, 0.0] * len(X) + dX = (0, 0, 0, 0, -0.0035, -0.0035) * len(X) + dY = (0, 0, 0, 0, +0.0025, +0.0025) * len(Y) + size = np.array((1, 1, 1, 1, 0.25, 0.05) * len(X)) * 50 + X, Y = np.repeat(X, 6), np.repeat(Y, 6) + ax.scatter( + X + dX, + Y + dY, + s=size, + linewidth=linewidth, + zorder=10, + facecolor=facecolor, + edgecolor=edgecolor, + ) + + plt.savefig("scatter.png", dpi=300) + plt.savefig("scatter.pdf") + plt.show() diff --git a/code/unsorted/3d/sphere.py b/code/unsorted/3d/sphere.py new file mode 100644 index 0000000..bb1db4a --- /dev/null +++ b/code/unsorted/3d/sphere.py @@ -0,0 +1,72 @@ +import glm +import plot +import numpy as np +import matplotlib.pyplot as plt + + +def sphere(radius=1.0, slices=32, stacks=32): + slices += 1 + stacks += 1 + n = slices * stacks + vertices = np.zeros((n, 3)) + theta1 = np.repeat(np.linspace(0, np.pi, stacks, endpoint=True), slices) + theta2 = np.tile(np.linspace(0, 2 * np.pi, slices, endpoint=True), stacks) + + vertices[:, 1] = np.sin(theta1) * np.cos(theta2) * radius + vertices[:, 2] = np.cos(theta1) * radius + vertices[:, 0] = np.sin(theta1) * np.sin(theta2) * radius + + indices = [] + for i in range(stacks - 1): + for j in range(slices - 1): + indices.append(i * (slices) + j) + indices.append(i * (slices) + j + 1) + indices.append(i * (slices) + j + slices + 1) + + indices.append(i * (slices) + j + slices + 1) + indices.append(i * (slices) + j + slices) + indices.append(i * (slices) + j) + + indices = np.array(indices) + indices = indices.reshape(len(indices) // 3, 3) + return vertices, indices + + +def lighting(F, direction=(1, 1, 1), color=(1, 0, 0), specular=False): + + # Faces center + C = F.mean(axis=1) + # Faces normal + N = glm.normalize(np.cross(F[:, 2] - F[:, 0], F[:, 1] - F[:, 0])) + # Relative light direction + D = glm.normalize(C - direction) + # Diffuse term + diffuse = glm.clip((N * D).sum(-1).reshape(-1, 1)) + + # Specular term + if specular: + specular = np.power(diffuse, 24) + return np.maximum(diffuse * color, specular) + + return diffuse * color + + +V, F = sphere(0.5, 2 * 32, 2 * 32) +facecolor = lighting(V[F], (-1, 1, 1), (1, 0, 0), True) + +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes([0, 0, 1, 1], xlim=[-1, +1], ylim=[-1, +1], aspect=1) + +camera = glm.camera(20, 45, 1.15, "perspective") +plot.axis(ax, camera) +plot.mesh( + ax, + camera, + V, + F, + mode="front", + linewidth=0, + facecolor=facecolor, + edgecolor=(0, 0, 0, 0), +) +plt.show() diff --git a/code/unsorted/3d/surf.py b/code/unsorted/3d/surf.py new file mode 100644 index 0000000..4ad366b --- /dev/null +++ b/code/unsorted/3d/surf.py @@ -0,0 +1,29 @@ +import glm +import plot +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + + n = 24 + X, Z = np.meshgrid( + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + np.linspace(-0.5 + 0.5 / n, +0.5 - 0.5 / n, n), + ) + Y = 0.75 * np.exp(-10 * (X ** 2 + Z ** 2)) + + cmap = plt.get_cmap("magma") + norm = mpl.colors.Normalize(vmin=-0.25, vmax=0.75) + facecolors = cmap(norm(Y))[..., :3] + + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_axes([0, 0, 1, 1]) + + camera = glm.camera(25, 45, 1, "perspective") + plot.axis(ax, camera) + plot.surf(ax, camera, Y, facecolors=facecolors) + +plt.show() diff --git a/code/unsorted/advanced-linestyles.pdf b/code/unsorted/advanced-linestyles.pdf new file mode 100644 index 0000000..34bbf35 Binary files /dev/null and b/code/unsorted/advanced-linestyles.pdf differ diff --git a/code/unsorted/advanced-linestyles.py b/code/unsorted/advanced-linestyles.py new file mode 100644 index 0000000..81315a9 --- /dev/null +++ b/code/unsorted/advanced-linestyles.py @@ -0,0 +1,204 @@ +import numpy as np +import matplotlib.pyplot as plt + + +fig = plt.figure(figsize=(4.25, 6)) +ax = plt.subplot(111, aspect=1, frameon=False, xticks=[], yticks=[]) + +X = np.linspace(0, 2 * np.pi, 100) +Y = 0.5 * np.sin(X) +yticks, ylabels = [], [] + + +# 1 +# ----------------------------------------------------------------------------- +y = 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot(X, y + Y, "black", linewidth=1) +ax.plot( + X, + y + Y, + "black", + linewidth=10, + solid_capstyle="butt", + dash_capstyle="butt", + linestyle=(0.0, (0.1, 1.0)), +) + +# 2 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot( + X, + y + Y, + ".25", + linewidth=5, + solid_capstyle="butt", + dash_capstyle="butt", + linestyle=(0.0, (0.1, 0.45)), +) +ax.plot(X, y + Y, "black", linewidth=1) +ax.plot( + X, + y + Y, + "black", + linewidth=10, + solid_capstyle="butt", + dash_capstyle="butt", + linestyle=(0.0, (0.1, 1.0)), +) + +# 3 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot(X, y + Y, "black", linewidth=8, solid_capstyle="round") +ax.plot(X, y + Y, "white", linewidth=6, solid_capstyle="round") +ax.plot(X, y + Y, "black", linewidth=1, solid_capstyle="round") + +# 4 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot( + X, + y + Y, + "black", + linewidth=8, + solid_capstyle="round", + dash_capstyle="round", + linestyle=(0.0, (0.01, 1.5)), +) + +# 5 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot( + X, y + Y, "black", linewidth=1.5, marker="o", markevery=10, mec="black", mfc="white" +) + +# 6 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot( + X, y + Y, "black", linewidth=1.5, marker="o", markevery=10, mec="white", mfc="black" +) + +# 7 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +ax.plot( + X, + y + Y, + "black", + linewidth=1.5, + markersize=10, + mew=2.5, + mec="white", + mfc="white", + marker="$↑$", + markevery=10, +) +ax.plot( + X, + y + Y, + "black", + linewidth=0.0, + markersize=10, + mew=0.5, + mec="black", + mfc="black", + marker="$↑$", + markevery=10, +) + +# 8 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +for i in range(9): + ax.plot( + X, + y + Y, + "%.2f" % (i / 10), + linewidth=4, + solid_capstyle="round", + dash_capstyle="round", + linestyle=(i + 1, (0.01, 9.0)), + ) + +# 9 +# ----------------------------------------------------------------------------- +y += 1 +yticks.append(y), ylabels.append("%d" % y) +Y = Y.mean() + 0 * Y +ax.plot( + X, + y + Y, + marker=(3, 0, -90), + mew=2.5, + mec="black", + mfc="black", + markersize=7, + linestyle="None", + markevery=2, +) +ax.plot( + X, + y + Y, + marker=(3, 0, -90), + mew=1, + mec="white", + mfc="white", + markersize=7, + linestyle="None", + markevery=2, +) + +# 10 +# ----------------------------------------------------------------------------- +y += 0.5 +yticks.append(y), ylabels.append("%d" % (y + 0.5)) +ax.plot( + X, + y + Y, + "black", + linewidth=6, + solid_capstyle="butt", + ms=10, + mew=1.5, + mec="white", + mfc="None", + marker=5, + markevery=(5, 10), +) + +# 11 +# ----------------------------------------------------------------------------- +y += 0.5 +yticks.append(y), ylabels.append("%d" % (y + 1)) +ax.plot( + X, + y + Y, + "black", + linewidth=1, + linestyle="--", + marker="$✁$", + markevery=(10, 1000), + ms=20, + mew=0.75, + mec="black", + mfc="white", +) + +ax.set_yticks(yticks) +ax.set_yticklabels(ylabels) +ax.tick_params(axis="both", which="both", length=0) + +plt.tight_layout() +plt.savefig("advanced-linestyles.pdf") +plt.show() diff --git a/code/unsorted/alpha-compositing.py b/code/unsorted/alpha-compositing.py new file mode 100644 index 0000000..bb4cd95 --- /dev/null +++ b/code/unsorted/alpha-compositing.py @@ -0,0 +1,193 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Illustrate alpha compositing (simulated) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.path as mpath +import matplotlib.patches as mpatches + + +def circle(center, radius): + """ Regular circle path """ + + T = np.arange(0, np.pi * 2.0, 0.01) + T = T.reshape(-1, 1) + X = center[0] + radius * np.cos(T) + Y = center[1] + radius * np.sin(T) + vertices = np.hstack((X, Y)) + codes = np.ones(len(vertices), dtype=mpath.Path.code_type) * mpath.Path.LINETO + codes[0] = mpath.Path.MOVETO + return vertices, codes + + +def rectangle(center, size): + """ Regular rectangle path """ + + (x, y), (w, h) = center, size + vertices = np.array([(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)]) + codes = np.array( + [ + mpath.Path.MOVETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + mpath.Path.LINETO, + ] + ) + return vertices, codes + + +def patch( + ax, + path, + facecolor="0.9", + edgecolor="black", + linewidth=0, + linestyle="-", + antialiased=True, + clip=None, +): + """ Build a patch with potential clipping path """ + patch = mpatches.PathPatch( + path, + antialiased=antialiased, + linewidth=linewidth, + linestyle=linestyle, + facecolor=facecolor, + edgecolor=edgecolor, + ) + if clip: + # If the path is not drawn, clipping doesn't work + clip_patch = mpatches.PathPatch( + clip, linewidth=0, facecolor="None", edgecolor="None" + ) + ax.add_patch(clip_patch) + patch.set_clip_path(clip_patch) + ax.add_patch(patch) + + +def subplot(cols, rows, index, title): + """ Shortcut to subplot to factorize options""" + ax = plt.subplot(cols, rows, index, aspect=1) + # ax.text(-1.25, 0.75, "A", size="large", ha="center", va="center") + # ax.text(+1.25, 0.75, "B", size="large", ha="center", va="center") + ax.set_title(title, weight="bold") + ax.set_xlim(-1.5, +1.5), ax.set_xticks([]) + ax.set_ylim(-1, +1), ax.set_yticks([]) + return ax + + +def blend(A, B, f): + xA, xB = np.array(A), np.array(B) + aA, aB = xA[3], xB[3] + aR = aA + aB * (1 - aA) + xaA, xaB = aA * xA, aB * xB + xR = 1.0 / aR * ((1 - aB) * xaA + (1 - aA) * xaB + aA * aB * f(xA, xB)) + return xR[:3] + + +def blend_multiply(A, B): + return blend(A, B, lambda x, y: x * y) + + +def blend_screen(A, B): + return blend(A, B, lambda x, y: x + y - x * y) + + +def blend_darken(A, B): + return blend(A, B, lambda x, y: np.minimum(x, y)) + + +def blend_lighten(A, B): + return blend(A, B, lambda x, y: np.minimum(x, y)) + + +def blend_color_dodge(A, B): + return blend(A, B, lambda x, y: np.where(A < 1, np.minimum(1, B / (1 - A)), 1)) + + +def blend_color_burn(A, B): + return blend(A, B, lambda x, y: np.where(A > 0, 1 - np.minimum(1, (1 - B) / A), 0)) + + +V1, C1 = rectangle((-1.5, -1), size=(3, 2)) +V2, C2 = circle((-0.5, 0), radius=0.75) +A = mpath.Path(V2, C2) +V2, C2 = circle((+0.5, 0), radius=0.75) +B = mpath.Path(V2, C2) + + +fig = plt.figure(figsize=(9, 5)) +rows, cols = 3, 4 + +cA = np.array([0.7, 0.0, 0.0, 0.8]) +cB = np.array([0.0, 0.0, 0.9, 0.4]) + +# No Blend +ax = subplot(rows, cols, 1, "No blend") +patch(ax, B, facecolor=cB) +patch(ax, B, clip=A, facecolor="white", antialiased=False) +patch(ax, A, facecolor=cA) + +# Default blend +ax = subplot(rows, cols, 2, "Default blend") +patch(ax, A, facecolor=cA) +patch(ax, B, facecolor=cB) + +# Multiply +ax = subplot(rows, cols, 3, "Multiply") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_multiply(cA, cB)) +patch(ax, B, facecolor=cB) + +# Screen +ax = subplot(rows, cols, 4, "Screen") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_screen(cA, cB)) +patch(ax, B, facecolor=cB) + +# Darken +ax = subplot(rows, cols, 5, "Darken") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_darken(cA, cB)) +patch(ax, B, facecolor=cB) + +# Lighten +ax = subplot(rows, cols, 6, "Lighten") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_lighten(cA, cB)) +patch(ax, B, facecolor=cB) + +# Color dodge +ax = subplot(rows, cols, 7, "Color dodge") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_color_dodge(cA, cB)) +patch(ax, B, facecolor=cB) + +# Color burn +ax = subplot(rows, cols, 8, "Color burn") +patch(ax, A, facecolor=cA) +patch(ax, B, clip=A, facecolor=blend_color_burn(cA, cB)) +patch(ax, B, facecolor=cB) + +# A ∩ B +ax = subplot(rows, cols, 9, "") + +# A ∩ ¬B +ax = subplot(rows, cols, 10, "") + +# ¬A ∩ B +ax = subplot(rows, cols, 11, "") + +# ¬A ∩ ¬B +ax = subplot(rows, cols, 12, "") + + +plt.tight_layout() +# plt.savefig("polygon-clipping.png", dpi=600) +# plt.savefig("polygon-clipping.pdf", dpi=600) +plt.show() diff --git a/code/unsorted/alpha-gradient.py b/code/unsorted/alpha-gradient.py new file mode 100644 index 0000000..01649a2 --- /dev/null +++ b/code/unsorted/alpha-gradient.py @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt + +# Setup +fig = plt.figure(figsize=(6, 3)) + +X = np.linspace(0, 4 * np.pi, 256) +Y1 = np.zeros((50, len(X))) +Y2 = np.zeros((len(Y1), len(X))) +for i in range(len(Y1)): + Y1[i] = np.cos(X) + np.random.uniform(-1, 1) + Y2[i] = np.sin(X) + np.random.uniform(-1, 1) + + +# Usage of transparency +ax = plt.subplot(1, 1, 1) + +Y, SD, VAR = Y1.mean(axis=0), Y1.std(axis=0), Y1.var(axis=0) + +ax.fill_between(X, Y + VAR, Y - VAR, facecolor="C0", alpha=0.25, zorder=-40) +ax.fill_between(X, Y + SD, Y - SD, facecolor="C0", alpha=0.25, zorder=-30) +ax.plot(X, Y + VAR, color="C0", linestyle="--", linewidth=1) +ax.plot(X, Y - VAR, color="C0", linestyle="--", linewidth=1) +ax.plot(X, Y, color="C0", zorder=-20) + +ax.set_xticks([]), ax.set_yticks([]) + +plt.tight_layout() +# plt.savefig("../figures/alpha-gradient.pdf") +plt.show() diff --git a/code/unsorted/dyson-hatching.py b/code/unsorted/dyson-hatching.py new file mode 100644 index 0000000..4aad409 --- /dev/null +++ b/code/unsorted/dyson-hatching.py @@ -0,0 +1,248 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Requirements: +# - noise library at https://pypi.org/project/noise/ +# - shapely library at https://pypi.org/project/Shapely/ +# - "Celtic Garamond" font at https://www.dafont.com/celtic-garamond-2nd.font +# - "Morris Roman" font at https://www.dafont.com/morris-roman.font +# - Nessy image from Irina Miroshnichenko (Sudowoodo) +# (not included in the book because of rights) +# +# Note: After having installed font, you might need to erase your font cache +# such that newly installed font can be found. Have a look at +# https://matplotlib.org/faq/troubleshooting_faq.html +# ---------------------------------------------------------------------------- + +import noise +import numpy as np +import scipy.spatial +import shapely.geometry +import matplotlib.pyplot as plt +from matplotlib.collections import LineCollection +from matplotlib.collections import PolyCollection +from math import cos, sin, floor, sqrt, pi, ceil + + +# This is important because "cities" have been manually positionned +np.random.seed(1) + + +# Blue noise sampling +# ------------------- +def blue_noise(shape, radius, k=32, seed=None): + """ + Generate blue noise over a two-dimensional rectangle of size (width,height) + + Parameters + ---------- + + shape : tuple + Two-dimensional domain (width x height) + radius : float + Minimum distance between samples + k : int, optional + Limit of samples to choose before rejection (typically k = 30) + seed : int, optional + If provided, this will set the random seed before generating noise, + for valid pseudo-random comparisons. + + References + ---------- + + .. [1] Fast Poisson Disk Sampling in Arbitrary Dimensions, Robert Bridson, + Siggraph, 2007. :DOI:`10.1145/1278780.1278807` + """ + + def sqdist(a, b): + """ Squared Euclidean distance """ + dx, dy = a[0] - b[0], a[1] - b[1] + return dx * dx + dy * dy + + def grid_coords(p): + """ Return index of cell grid corresponding to p """ + return int(floor(p[0] / cellsize)), int(floor(p[1] / cellsize)) + + def fits(p, radius): + """ Check whether p can be added to the queue """ + + radius2 = radius * radius + gx, gy = grid_coords(p) + for x in range(max(gx - 2, 0), min(gx + 3, grid_width)): + for y in range(max(gy - 2, 0), min(gy + 3, grid_height)): + g = grid[x + y * grid_width] + if g is None: + continue + if sqdist(p, g) <= radius2: + return False + return True + + # When given a seed, we use a private random generator in order to not + # disturb the default global random generator + if seed is not None: + from numpy.random.mtrand import RandomState + + rng = RandomState(seed=seed) + else: + rng = np.random + + width, height = shape + cellsize = radius / sqrt(2) + grid_width = int(ceil(width / cellsize)) + grid_height = int(ceil(height / cellsize)) + grid = [None] * (grid_width * grid_height) + + p = rng.uniform(0, shape, 2) + queue = [p] + grid_x, grid_y = grid_coords(p) + grid[grid_x + grid_y * grid_width] = p + + while queue: + qi = rng.randint(len(queue)) + qx, qy = queue[qi] + queue[qi] = queue[-1] + queue.pop() + for _ in range(k): + theta = rng.uniform(0, 2 * pi) + r = radius * np.sqrt(rng.uniform(1, 4)) + p = qx + r * cos(theta), qy + r * sin(theta) + if not (0 <= p[0] < width and 0 <= p[1] < height) or not fits(p, radius): + continue + queue.append(p) + gx, gy = grid_coords(p) + grid[gx + gy * grid_width] = p + + return np.array([p for p in grid if p is not None]) + + +# Hatch pattern with given orientation (or random if None given) +def hatch(n=4, theta=None): + theta = theta or np.random.uniform(0, np.pi) + P = np.zeros((n, 2, 2)) + X = np.linspace(-0.5, +0.5, n, endpoint=True) + P[:, 0, 1] = -0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 1] = +0.5 + np.random.normal(0, 0.05, n) + P[:, 1, 0] = X + np.random.normal(0, 0.025, n) + P[:, 0, 0] = X + np.random.normal(0, 0.025, n) + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[c, s], [-s, c]]) + return P @ Z.T + + +# Actual drawing +fig = plt.figure(figsize=(6, 6)) +fig.patch.set_facecolor("#ffffff") +ax = plt.subplot(1, 1, 1, aspect=1, frameon=False) + + +# Figure border using the hatch pattern. They are first spread according to +# a blue noise distribution, scaled according to the distance to the nearest +# neighbour and then lines composing the hatch are clipped against the +# corresponding Voronoi cell. +h = 4 # Number of line segments composing a hatch +radius = 0.2 # Minimum radius between points +# (the smaller, the longer to compute) + +P = blue_noise((11, 11), radius=radius) - (0.5, 0.5) +D = scipy.spatial.distance.cdist(P, P) +D.sort(axis=1) +S = [] +vor = scipy.spatial.Voronoi(P) +for i in range(len(vor.point_region)): + region = vor.regions[vor.point_region[i]] + if not -1 in region: + verts = np.array([vor.vertices[i] for i in region]) + poly = shapely.geometry.Polygon(verts) + H = 1.25 * D[i, 1] * hatch(h) + P[i] + for i in range(len(H)): + line = shapely.geometry.LineString(H[i]) + intersect = poly.intersection(line) + if intersect: + S.append(intersect.coords) + +# Grey background using thick lines +hatches = LineCollection(S, color="#eeeeee", linewidth=7, capstyle="round", zorder=-20) +ax.add_collection(hatches) + +# Actual hatches +hatches = LineCollection(S, color="black", linewidth=1.5, capstyle="round", zorder=-10) +ax.add_collection(hatches) + +# Plain rectangle +rectangle = plt.Rectangle((0, 0), 10, 10, fc="none", ec="white", lw=3.5) +ax.add_patch(rectangle) +rectangle = plt.Rectangle((0, 0), 10, 10, fc="white", ec="black", lw=2.5) +ax.add_patch(rectangle) + + +# A cheap map using Perlin noise and contour/contourf +shape = 256, 256 +scale = 150 +octaves = 5 +persistence = 0.5 +lacunarity = 2.5 +Z = np.zeros(shape) +for i in range(shape[0]): + for j in range(shape[1]): + Z[i][j] = noise.pnoise2( + i / scale, + j / scale, + octaves=octaves, + persistence=persistence, + lacunarity=lacunarity, + repeatx=1024, + repeaty=1024, + base=0, + ) +X = np.linspace(0, 10, 256) +Y = np.linspace(0, 10, 256) +plt.contourf(X, Y, Z, 2, colors=["#eeeeee", "#ffffff"], zorder=10) +plt.contour(X, Y, Z, 2, colors="black", linestyles="-", zorder=10) + + +# Text (could be factorized) +plt.text(1.0, 5.25, "MATPLOTLIB", family="Celtic Garamond the 2nd", size=16, zorder=20) + +plt.scatter([6.5], [6.15], s=25, color="black", zorder=20) +plt.text(6.65, 6.20, "Beautiful", family="Morris Roman", size=12, zorder=20) + +plt.scatter([1], [7.5], s=25, color="black", zorder=20) +plt.text(1, 7.75, "Versatile", ha="center", family="Morris Roman", size=12, zorder=20) + +plt.scatter([1.7], [1.7], s=25, color="black", zorder=20) +plt.text(1.7, 1.35, "Powerful", ha="center", family="Morris Roman", size=12, zorder=20) + +plt.scatter([6.2], [3.2], s=25, color="black", zorder=20) +plt.text(6.2, 2.8, "Scalable", ha="center", family="Morris Roman", size=12, zorder=20) + + +# Wind rose at the bottom right +V = np.zeros((8, 2, 3, 2)) +for i in range(4): + theta = np.pi / 4 + i * np.pi / 2 + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[c, s], [-s, c]]) + V[i, 0] = [(0, 0), (-1, 1), (0, 5)] @ Z.T + V[i, 1] = [(0, 0), (+1, 1), (0, 5)] @ Z.T + theta -= np.pi / 4 + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[c, s], [-s, c]]) + V[4 + i, 0] = [(0, 0), (-1, 1), (0, 5)] @ Z.T + V[4 + i, 1] = [(0, 0), (+1, 1), (0, 5)] @ Z.T +V = V.reshape(16, 3, 2) +V = 0.9 * V / 5 + (9, 1) +FC = np.zeros((16, 4)) +FC[0::2] = 0, 0, 0, 1 +FC[1::2] = 1, 1, 1, 1 +collection = PolyCollection(V, edgecolors="black", facecolors=FC, lw=0.75, zorder=20) +ax.add_collection(collection) + + +# Done +ax.set_xlim(-1, 11), ax.set_xticks([]) +ax.set_ylim(-1, 11), ax.set_yticks([]) +plt.tight_layout() +# plt.savefig("dyson-hatching.pdf") +plt.show() diff --git a/code/unsorted/earthquakes.py b/code/unsorted/earthquakes.py new file mode 100644 index 0000000..c56ce29 --- /dev/null +++ b/code/unsorted/earthquakes.py @@ -0,0 +1,108 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import urllib +import numpy as np +import cartopy +import matplotlib.pyplot as plt +from datetime import datetime + + +# -> http://earthquake.usgs.gov/earthquakes/feed/v1.0/csv.php +feed = "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/" + +# Significant earthquakes in the past 30 days +# url = urllib.request.urlopen(feed + "significant_month.csv") + +# Earthquakes of magnitude > 4.5 in the past 30 days +url = urllib.request.urlopen(feed + "4.5_month.csv") + +# Earthquakes of magnitude > 2.5 in the past 30 days +# url = urllib.request.urlopen(feed + "2.5_month.csv") + +# Earthquakes of magnitude > 1.0 in the past 30 days +# url = urllib.request.urlopen(feed + "1.0_month.csv") + +# Read data +data = (url.read().decode()).split("\n") +E = np.genfromtxt( + data, delimiter=",", names=True, usecols=("latitude", "longitude", "mag") +) +X, Y, M = E["longitude"], E["latitude"], E["mag"] +M = 25 * (M - 4) ** 3 + +# Plot data +fig = plt.figure(figsize=(8, 6)) +ax = plt.subplot(1, 1, 1, projection=cartopy.crs.EqualEarth()) +ax.set_global() + +ax.add_feature(cartopy.feature.OCEAN, zorder=0, facecolor="blue", alpha=0.1) +ax.add_feature( + cartopy.feature.LAND, zorder=0, facecolor="white", edgecolor="0.25", linewidth=0.5 +) + +# Visual effect for a more salient information +ax.scatter( + X, Y, M, lw=1, ec="black", alpha=1, zorder=10, transform=cartopy.crs.PlateCarree() +) +ax.scatter( + X, + Y, + M - 1, + ec="none", + fc="white", + alpha=1, + zorder=20, + transform=cartopy.crs.PlateCarree(), +) +ax.scatter( + X, + Y, + M - 1, + ec="none", + fc="red", + alpha=0.25, + zorder=20, + transform=cartopy.crs.PlateCarree(), +) +ax.scatter( + X, + Y, + 2, + ec="none", + fc="black", + alpha=1, + zorder=30, + transform=cartopy.crs.PlateCarree(), +) + +# Title & subtitles +ax.text( + 0.5, + 1.01, + "Earthquakes with magnitude > 4.5 in the past 30 days", + va="bottom", + ha="center", + transform=ax.transAxes, + family="Source Serif Pro", + size=12, + weight=600, +) +ax.text( + 0.5, + -0.01, + datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + + " - data from http://earthquake.usgs.gov", + va="top", + ha="center", + transform=ax.transAxes, + family="Source Serif Pro", + size=12, + weight=400, +) + +plt.tight_layout() +plt.savefig("earthquakes.pdf", dpi=600) +plt.show() diff --git a/code/unsorted/git-commits.py b/code/unsorted/git-commits.py new file mode 100644 index 0000000..f88b822 --- /dev/null +++ b/code/unsorted/git-commits.py @@ -0,0 +1,125 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import git +import numpy as np +import matplotlib + +matplotlib.use("module://imgcat") +import matplotlib.pyplot as plt + +from matplotlib.patches import Polygon +from datetime import date, datetime +from dateutil.relativedelta import relativedelta + + +def calmap(ax, year, data, origin="upper", weekstart="sun"): + ax.tick_params("x", length=0, labelsize="medium", which="major") + ax.tick_params("y", length=0, labelsize="x-small", which="major") + + # Month borders + xticks, labels = [], [] + + start = datetime(year, 1, 1).weekday() + + _data = np.zeros(7 * 53) * np.nan + _data[start : start + len(data)] = data + data = _data.reshape(53, 7).T + + for month in range(1, 13): + first = datetime(year, month, 1) + last = first + relativedelta(months=1, days=-1) + if origin == "lower": + y0 = first.weekday() + y1 = last.weekday() + x0 = (int(first.strftime("%j")) + start - 1) // 7 + x1 = (int(last.strftime("%j")) + start - 1) // 7 + P = [ + (x0, y0), + (x0, 7), + (x1, 7), + (x1, y1 + 1), + (x1 + 1, y1 + 1), + (x1 + 1, 0), + (x0 + 1, 0), + (x0 + 1, y0), + ] + else: + y0 = 6 - first.weekday() + y1 = 6 - last.weekday() + x0 = (int(first.strftime("%j")) + start - 1) // 7 + x1 = (int(last.strftime("%j")) + start - 1) // 7 + P = [ + (x0, y0 + 1), + (x0, 0), + (x1, 0), + (x1, y1), + (x1 + 1, y1), + (x1 + 1, 7), + (x0 + 1, 7), + (x0 + 1, y0 + 1), + ] + + xticks.append(x0 + (x1 - x0 + 1) / 2) + labels.append(first.strftime("%b")) + poly = Polygon( + P, + edgecolor="black", + facecolor="None", + linewidth=1, + zorder=20, + clip_on=False, + ) + ax.add_artist(poly) + + ax.set_xticks(xticks) + ax.set_xticklabels(labels) + ax.set_yticks(0.5 + np.arange(7)) + + labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + if origin == "upper": + labels = labels[::-1] + ax.set_yticklabels(labels) + + ax.text( + 1.01, + 0.5, + "{}".format(year), + rotation=90, + ha="left", + va="center", + transform=ax.transAxes, + size="28", + weight="bold", + alpha=0.5, + ) + + # Showing data + cmap = plt.cm.get_cmap("Purples") + im = ax.imshow( + data, extent=[0, 53, 0, 7], zorder=10, vmin=0, vmax=8, cmap=cmap, origin=origin + ) + + +def get_commits(year=2021, path="../.."): + "Collect commit dates for a given year" + + n = 1 + (date(year, 12, 31) - date(year, 1, 1)).days + C = np.zeros(n, dtype=int) + repo = git.Repo(path) + for commit in repo.iter_commits("master"): + timestamp = datetime.fromtimestamp(commit.committed_date) + if timestamp.year == year: + day = timestamp.timetuple().tm_yday + C[day - 1] += 1 + return C + + +for year in [2019, 2020, 2021]: + fig = plt.figure(figsize=(10, 2), dpi=250, frameon=False) + ax = plt.subplot(xticks=[], yticks=[], frameon=False) + calmap(ax, year, get_commits(year), origin="upper", weekstart="sun") + plt.tight_layout() + fig.show() diff --git a/code/unsorted/github-activity.py b/code/unsorted/github-activity.py new file mode 100644 index 0000000..3a420c9 --- /dev/null +++ b/code/unsorted/github-activity.py @@ -0,0 +1,139 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import os +import numpy as np +import html.parser +import urllib.request +import dateutil.parser +from datetime import date, datetime +from dateutil.relativedelta import relativedelta +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon + + +def github_contrib(user, year): + """ Get GitHub user daily contribution """ + + # Check for a cached version (file) + filename = "github-{0}-{1}.html".format(user, year) + if os.path.exists(filename): + with open(filename) as file: + contents = file.read() + # Else get file from GitHub + else: + url = "https://github.com/users/{0}/contributions?to={1}-12-31" + url = url.format(user, year) + contents = str(urllib.request.urlopen(url).read()) + with open(filename, "w") as file: + file.write(contents) + + # Parse result (html) + n = 1 + (date(year, 12, 31) - date(year, 1, 1)).days + C = -np.ones(n, dtype=int) + + class HTMLParser(html.parser.HTMLParser): + def handle_starttag(self, tag, attrs): + if tag == "rect": + data = {key: value for (key, value) in attrs} + date = dateutil.parser.parse(data["data-date"]) + count = int(data["data-count"]) + day = date.timetuple().tm_yday - 1 + if count > 0: + C[day] = count + + parser = HTMLParser() + parser.feed(contents) + return C + + +def calmap(ax, year, data, origin="upper", weekstart="sun"): + ax.tick_params("x", length=0, labelsize="medium", which="major") + ax.tick_params("y", length=0, labelsize="x-small", which="major") + + # Month borders + xticks, labels = [], [] + + start = datetime(year, 1, 1).weekday() + + _data = np.zeros(7 * 53) * np.nan + _data[start : start + len(data)] = data + data = _data.reshape(53, 7).T + + for month in range(1, 13): + first = datetime(year, month, 1) + last = first + relativedelta(months=1, days=-1) + if origin == "lower": + y0 = first.weekday() + y1 = last.weekday() + x0 = (int(first.strftime("%j")) + start - 1) // 7 + x1 = (int(last.strftime("%j")) + start - 1) // 7 + P = [ + (x0, y0), + (x0, 7), + (x1, 7), + (x1, y1 + 1), + (x1 + 1, y1 + 1), + (x1 + 1, 0), + (x0 + 1, 0), + (x0 + 1, y0), + ] + else: + y0 = 6 - first.weekday() + y1 = 6 - last.weekday() + x0 = (int(first.strftime("%j")) + start - 1) // 7 + x1 = (int(last.strftime("%j")) + start - 1) // 7 + P = [ + (x0, y0 + 1), + (x0, 0), + (x1, 0), + (x1, y1), + (x1 + 1, y1), + (x1 + 1, 7), + (x0 + 1, 7), + (x0 + 1, y0 + 1), + ] + + xticks.append(x0 + (x1 - x0 + 1) / 2) + labels.append(first.strftime("%b")) + poly = Polygon( + P, + edgecolor="black", + facecolor="None", + linewidth=1, + zorder=20, + clip_on=False, + ) + ax.add_artist(poly) + + ax.set_xticks(xticks) + ax.set_xticklabels(labels) + ax.set_yticks(0.5 + np.arange(7)) + + labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + if origin == "upper": + labels = labels[::-1] + ax.set_yticklabels(labels) + ax.set_title("{}".format(year), size="medium", weight="bold") + + # Showing data + cmap = plt.cm.get_cmap("Purples") + ax.imshow( + data, extent=[0, 53, 0, 7], zorder=10, vmin=0, vmax=10, cmap=cmap, origin=origin + ) + + +fig = plt.figure(figsize=(8, 7.5), dpi=100) +year = 2014 +n = 5 +for i in range(n): + ax = plt.subplot(n, 1, i + 1, xlim=[0, 53], ylim=[0, 7], frameon=False, aspect=1) + calmap(ax, year + i, github_contrib("rougier", year + i), origin="upper") + +plt.tight_layout() + +# plt.savefig("github-activity.png", dpi=300) +# plt.savefig("github-activity.pdf", dpi=600) +plt.show() diff --git a/code/unsorted/hatched-bars.py b/code/unsorted/hatched-bars.py new file mode 100644 index 0000000..02e2c08 --- /dev/null +++ b/code/unsorted/hatched-bars.py @@ -0,0 +1,73 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors + + +def lighten_color(color, amount=0.66): + import matplotlib.colors as mc + import colorsys + + try: + c = mc.cnames[color] + except: + c = color + c = np.array(colorsys.rgb_to_hls(*mc.to_rgb(c))) + return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) + + +cmap = plt.get_cmap("tab10") +color = cmap(0) +plt.rcParams["hatch.color"] = lighten_color(color) +plt.rcParams["hatch.linewidth"] = 8 + + +fig = plt.figure(figsize=(4.25, 2)) +ax = plt.subplot(1, 1, 1) +np.random.seed(123) + +x1, y1 = 3 * np.arange(4), np.random.randint(25, 50, 4) +x2, y2 = x1 + 1, np.random.randint(25, 75, 4) + +ax.bar(x1, y1, color=color) +for i in range(len(x1)): + plt.annotate( + "%d%%" % y1[i], + (x1[i], y1[i]), + xytext=(0, 1), + fontsize="x-small", + color=color, + textcoords="offset points", + va="bottom", + ha="center", + ) + +ax.bar(x2, y2, color=color, hatch="/") +for i in range(len(x2)): + plt.annotate( + "%d%%" % y2[i], + (x2[i], y2[i]), + xytext=(0, 1), + fontsize="x-small", + color=color, + textcoords="offset points", + va="bottom", + ha="center", + ) + +ax.set_yticks([]) +ax.set_xticks(0.5 + np.arange(0, 12, 3)) +ax.set_xticklabels(["2016", "2017", "2018", "2019"]) +ax.tick_params("x", length=0, labelsize="small", which="major") + +ax.spines["right"].set_visible(False) +ax.spines["left"].set_visible(False) +ax.spines["top"].set_visible(False) + +plt.tight_layout() +# plt.savefig("hatched-bars.pdf") +plt.show() diff --git a/code/unsorted/layout-weird.py b/code/unsorted/layout-weird.py new file mode 100644 index 0000000..e1143e3 --- /dev/null +++ b/code/unsorted/layout-weird.py @@ -0,0 +1,81 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +# Weird axes layout (just for fun) +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec + +p = plt.rcParams +p["figure.figsize"] = 7, 7 +p["font.sans-serif"] = ["Roboto Condensed"] +p["font.weight"] = "light" +p["ytick.minor.visible"] = True +p["xtick.minor.visible"] = True +p["axes.grid"] = True +p["grid.color"] = "0.5" +p["grid.linewidth"] = 0.5 + + +X = np.linspace(-np.pi, np.pi, 257, endpoint=True) +C, S = np.cos(X), np.sin(X) + +fig = plt.figure(constrained_layout=True) +nrows, ncols = 2, 2 +gspec = gridspec.GridSpec(ncols=ncols, nrows=nrows, figure=fig) + + +ax = plt.subplot(1, 1, 1) +ax.set_xlim(0, 1) +ax.set_xticks(np.linspace(0, 1, 5)) +ax.set_xlabel("X Label") +ax.set_ylim(0, 1) +ax.set_yticks(np.linspace(0, 1, 5)) +ax.set_ylabel("Y Label") +ax.set_title("Close-up", x=0.25, family="Roboto", weight=500) + +# Manual edit for the axes limits +ax2 = fig.add_axes([0.53, 0.515, 0.4485, 0.4505]) +ax2.spines["top"].set_visible(False) +ax2.spines["right"].set_visible(False) +ax2.grid(False) +ax2.set_xticks([]) +ax2.set_yticks([]) + +n = 10000 +X = np.random.normal(0.25, 1, n) +Y = np.random.normal(0.25, 1, n) + +# Manual edit for the axes limits +ax3 = fig.add_axes([0.575, 0.56, 0.4, 0.4]) +ax3.set_title("Distribution", family="Roboto", weight=500) +ax3.set_xlim(-3, 3) +ax3.set_ylim(-3, 3) +S = ax3.scatter(X, Y, s=0.5, linewidths=0, color=".5") +S = ax3.scatter(X, Y, s=0.5, linewidths=0, color="black") +from matplotlib.patches import Polygon + +p = Polygon( + [(0, 0), (0, 1), (0.5, 1), (0.5, 0.5), (1, 0.5), (1, 0), (0, 0)], + transform=ax3.transData, + closed=True, + facecolor="None", + edgecolor="black", + linewidth=0.75, + zorder=50, +) +S.set_clip_path(p) +ax3.add_artist(p) + + +S = ax.scatter(X, Y, s=100, linewidths=1, color="black") +S = ax.scatter(X, Y, s=100, linewidths=0, color="white") +for s in np.linspace(5, 100, 10): + S = ax.scatter(X, Y, s=s, linewidths=0, alpha=0.05, color="red") +S = ax.scatter(X, Y, s=3, linewidths=0, alpha=0.75, color="black") + +# plt.savefig("../figures/layout-weird.pdf") +plt.show() diff --git a/code/unsorted/make-hatch-linewidth.py b/code/unsorted/make-hatch-linewidth.py new file mode 100644 index 0000000..7bb52f5 --- /dev/null +++ b/code/unsorted/make-hatch-linewidth.py @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import imageio +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +from matplotlib.backends.backend_agg import FigureCanvasAgg + +figsize = 4.25, 5 * 0.55 +xlim = 0.0, 11.0 +ylim = 0.5, 5.0 +dx = figsize[0] / (xlim[1] - xlim[0]) +dy = figsize[1] / (ylim[1] - ylim[0]) +widths = 1, 2, 3, 4, 5, 6 +w, h = 0.75 * 10 / len(widths), 0.5 +figsize = w * dx, h * dy +dpi = 600 + +for width in widths: + plt.rcParams["hatch.linewidth"] = width + fig = plt.figure(figsize=figsize, dpi=dpi) + ax = fig.add_axes([0, 0, 1, 1], xlim=[0, 1], ylim=[0, 1]) + ax.axis("off") + canvas = FigureCanvasAgg(fig) + rect = Rectangle( + (0, 0), 1, 1, hatch="/", facecolor="0.85", edgecolor="0.00", linewidth=0.0 + ) + ax.add_patch(rect) + canvas.draw() + image = np.frombuffer(canvas.tostring_rgb(), dtype="uint8") + image = image.reshape(int(figsize[1] * dpi), int(figsize[0] * dpi), 3) + imageio.imwrite("hatch-%d.png" % width, image) diff --git a/code/unsorted/metropolis.py b/code/unsorted/metropolis.py new file mode 100644 index 0000000..bfeed00 --- /dev/null +++ b/code/unsorted/metropolis.py @@ -0,0 +1,110 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import tqdm +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.figure import Figure +from scipy.ndimage import gaussian_filter +from matplotlib.collections import LineCollection +from matplotlib.backends.backend_agg import FigureCanvas + + +# Nothing complicated here. +# Directly translated from https://github.com/marcusvolz/metropolis +# See also https://github.com/marcusvolz +# +# Don't use n > 11000 or you'll be stuck forever... +def metropolis(width=10000, height=10000, n=11000, step=75, branching=0.1, noise=2.0): + delta = noise * np.pi / 180 + points = np.zeros( + n, dtype=[("x", float), ("y", float), ("dir", float), ("level", int)] + ) + edges = np.zeros( + n, + dtype=[ + ("x0", float), + ("y0", float), + ("x1", float), + ("y1", float), + ("level", int), + ], + ) + points[0] = width / 2, height / 2, np.random.uniform(-2 * np.pi, 2 * np.pi), 1 + + for i in tqdm.trange(1, n): + while True: + point = np.random.choice(points[:i]) + branch = 1 if np.random.uniform(0, 1) <= branching else 0 + alpha = point["dir"] + delta * np.random.uniform(-1, +1) + alpha += branch * np.random.choice([-np.pi / 2, +np.pi / 2]) + v = np.array((np.cos(alpha), np.sin(alpha))) + v = v * step * (1 + 1 / (point["level"] + branch)) + x = point["x"] + v[0] + y = point["y"] + v[1] + level = point["level"] + branch + if x < 0 or x > width or y < 0 or y > height: + continue + dist = np.sqrt((points["x"] - x) ** 2 + (points["y"] - y) ** 2) + if dist.min() >= step: + points[i] = x, y, alpha, level + edges[i] = x, y, point["x"], point["y"], level + break + return edges[edges["level"] > 0] + + +np.random.seed(12345) +width, height, border = 10000, 10000, 500 +if 1: # Set to 0 after computation + edges = metropolis(width, height) + np.save("egdes.npy", edges) +else: + edges = np.load("egdes.npy") + + +segments = [] +for edge in edges: + x0, y0, x1, y1, level = edge + segments.append([(x0, y0), (x1, y1)]) + +# Drop shadow pre-processing +# We render into an array and we apply Gaussian blur +fig = Figure(figsize=(6, 6)) +canvas = FigureCanvas(fig) +ax = fig.add_axes( + [0, 0, 1, 1], + aspect=1, + frameon=False, + xticks=[], + yticks=[], + xlim=[0, width], + ylim=[0, height], +) +sigma = 2.0 +collection = LineCollection(segments, linewidths=1.5, colors="black", capstyle="round") +ax.add_collection(collection) +canvas.draw() +I = np.array(canvas.renderer.buffer_rgba())[..., :3] +I[:, :, 0] = gaussian_filter(I[:, :, 0], sigma=sigma) +I[:, :, 1] = gaussian_filter(I[:, :, 1], sigma=sigma) +I[:, :, 2] = gaussian_filter(I[:, :, 2], sigma=sigma) + + +# Actual rendering +fig = plt.figure(figsize=(6, 6)) +ax = fig.add_axes( + [0, 0, 1, 1], + aspect=1, + frameon=False, + xticks=[], + xlim=[border, width - border], + yticks=[], + ylim=[border, height - border], +) +ax.imshow(I, extent=[0, width, 0, height], alpha=0.5) +collection = LineCollection(segments, linewidths=1.0, colors="black", capstyle="round") +ax.add_collection(collection) +plt.savefig("metropolis.pdf", dpi=600) +plt.show() diff --git a/code/unsorted/poster-layout.py b/code/unsorted/poster-layout.py new file mode 100644 index 0000000..91627f1 --- /dev/null +++ b/code/unsorted/poster-layout.py @@ -0,0 +1,177 @@ +# ----------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ----------------------------------------------------------------------------- +# Complex layout and text effects +# ----------------------------------------------------------------------------- +import numpy as np +import imageio +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +from matplotlib.text import TextPath +import matplotlib.patches as mpatches +import matplotlib.patheffects as path_effects +from matplotlib.font_manager import FontProperties + + +def box(ax, index, x, y, width, height): + rectangle = mpatches.Rectangle( + (x, y), width, height, zorder=10, facecolor="none", edgecolor="black" + ) + ax.add_artist(rectangle) + + ax.text( + x + (width - 1) / 2, + y + (height - 1) / 2, + "%d" % index, + weight="bold", + zorder=50, + size="32", + va="center", + ha="center", + color="k", + alpha=0.25, + family="GlassJaw BB", + ) + + +fig = plt.figure(figsize=(6.5, 9.3)) + +ax = fig.add_axes( + [0, 0, 1, 1], + aspect=1, + frameon=False, + xlim=(0, 65), + ylim=(0, 93), + xticks=[], + yticks=[], +) + +ax.xaxis.set_major_locator(ticker.MultipleLocator(10)) +ax.xaxis.set_minor_locator(ticker.MultipleLocator(1)) +ax.yaxis.set_major_locator(ticker.MultipleLocator(10)) +ax.yaxis.set_minor_locator(ticker.MultipleLocator(1)) +# ax.grid(which="both", linewidth=0.5, color="0.85", zorder=-10) + +boxes = [ + (1, 1, 15, 21), + (17, 1, 15, 10), + (17, 12, 15, 10), + (33, 1, 15, 15), + (49, 1, 15, 15), + (1, 23, 31, 8), + (1, 32, 8, 14), + (10, 32, 22, 14), + (1, 47, 15, 13), + (17, 47, 15, 13), + (33, 45, 15, 15), + (33, 32, 15, 12), + (49, 32, 15, 18), + (49, 51, 15, 9), + (1, 61, 15, 15), + (17, 61, 36, 15), + (54, 61, 10, 15), + (49, 77, 15, 15), +] + +for index, (x, y, width, height) in enumerate(boxes): + box(ax, index, x, y, width, height) + +# fig.add_axes([33/65, 17/93, 31/65, 14/93], xticks=[], yticks=[]) + +ax.text( + 33, + 17.25, + """Matplotlib can also be used to layout\n""" + """a poster where each box can be filled\n""" + """with different subplot.\n""" + """\n""" + """To compute the boxes, just draw them\n""" + """on a piece of graph of paper and use\n""" + """the measure to get the bounds.\n""" + """\n""" + """The cartoon fonts used in this example\n""" + """are "Lint McCree" and "GlassJaw"\n""" + """""", + size=6, + color="k", + family="Lint McCree Intl BB", +) + + +# Title +# ----------------------------------------------------------------------------- +textpath = TextPath( + (8, 80), "MATPLOTLIB", size=8, prop=FontProperties(family="GlassJaw BB") +) +patch = mpatches.PathPatch( + textpath, facecolor="none", edgecolor="none", zorder=5, joinstyle="round" +) +patch.set_path_effects( + [ + path_effects.Stroke(linewidth=8, foreground="black"), + path_effects.Stroke(linewidth=6, foreground="yellow"), + path_effects.Stroke(linewidth=3, foreground="black"), + ] +) + +transform = ax.transData + mpl.transforms.Affine2D().rotate_deg(2.5) +patch.set_transform(transform) + +ax.add_artist(patch) +Z = np.linspace(1, 0, 100).reshape(100, 1) +im = ax.imshow(Z, cmap="autumn", extent=[1, 100, 79, 87], zorder=15) +im.set_transform(transform) +im.set_clip_path(patch._path, patch.get_transform()) + +ax.text( + 47, + 79, + "Scientific visualization made simple & beautiful", + color="black", + zorder=20, + family="Lint McCree Intl BB", + weight="bold", + size="xx-small", + ha="right", + va="baseline", +) + +ax.text( + 8, + 87, + " BSD Licensed ", + color="white", + zorder=30, + rotation=-1.5, + family="Lint McCree Intl BB", + ha="center", + va="center", + size=7, + bbox=dict(boxstyle="roundtooth", fc="k", ec="w", lw=1, pad=0.75), +) + + +# Box 18 +# ----------------------------------------------------------------------------- +I = imageio.imread("../data/John-Hunter-comic.png") +ax.imshow(I, extent=[49, 49 + 15, 77, 77 + 15], zorder=0, interpolation="bicubic") + +ax.text( + 49.7, + 77.5, + "John D. Hunter III", + color="black", + zorder=30, + family="Lint McCree Intl BB", + weight="bold", + ha="left", + va="bottom", + size=5, + bbox=dict(boxstyle="square", fc="w", ec="k", lw=1, pad=0.5), +) + +# plt.savefig("poster-layout.png", dpi=300) +plt.show() diff --git a/code/unsorted/scale-logit.py b/code/unsorted/scale-logit.py new file mode 100644 index 0000000..cb935c6 --- /dev/null +++ b/code/unsorted/scale-logit.py @@ -0,0 +1,43 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: BSD +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as colors +from matplotlib.ticker import NullFormatter + +X = np.random.uniform(0, 1, 1000) +Y = np.random.uniform(0, 1, 1000) +S = np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2) +C = np.ones((1000, 4)) * colors.to_rgba("C0") +C[S > 0.5] = colors.to_rgba("C1") + +figure = plt.figure(figsize=(8, 4)) + +# Linear scale +ax = plt.subplot(1, 2, 1, xlim=(0, 1), ylim=(0, 1)) +ax.scatter(X, Y, s=25, facecolor=C, edgecolor="none", alpha=0.5) +T = np.linspace(0, 2 * np.pi, 100) +X2 = 0.5 + 0.5 * np.cos(T) +Y2 = 0.5 + 0.5 * np.sin(T) +ax.plot(X2, Y2, color="black", linestyle="--", linewidth=0.75) + +# +ax = plt.subplot(1, 2, 2, xlim=(0.01, 0.99), ylim=(0.01, 0.99)) +ax.set_xscale("logit") +ax.set_yscale("logit") +ax.scatter(X, Y, s=25, facecolor=C, edgecolor="none", alpha=0.5) +ax.yaxis.set_minor_formatter(NullFormatter()) +ax.xaxis.set_minor_formatter(NullFormatter()) +T = np.linspace(0, 2 * np.pi, 256) +X2 = 0.5 + 0.5 * np.cos(T) +Y2 = 0.5 + 0.5 * np.sin(T) +ax.plot(X2, Y2, color="black", linestyle="--", linewidth=0.75) + + +# Show +plt.tight_layout() +# plt.savefig("scale-logit.pdf") +plt.show() diff --git a/code/unsorted/stacked-bars.py b/code/unsorted/stacked-bars.py new file mode 100644 index 0000000..66f207f --- /dev/null +++ b/code/unsorted/stacked-bars.py @@ -0,0 +1,71 @@ +# ---------------------------------------------------------------------------- +# Title: Scientific Visualisation - Python & Matplotlib +# Author: Nicolas P. Rougier +# License: Creative Commons BY-NC-SA International 4.0 +# ---------------------------------------------------------------------------- +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors + + +def lighten_color(color, amount=0.66): + import colorsys + + try: + c = mcolors.cnames[color] + except: + c = color + c = np.array(colorsys.rgb_to_hls(*mcolors.to_rgb(c))) + return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) + + +# cmap = plt.get_cmap("Blues") +cmap = plt.get_cmap("tab10") + + +V = np.array( + [ + [50, 23, 20, 7], + [45, 33, 12, 10], + [35, 43, 10, 12], + [54, 23, 15, 8], + [65, 23, 7, 5], + ] +) + + +fig = plt.figure(figsize=(6, 2), dpi=100) +ax = plt.subplot(111, xticks=[], ylim=[-0.5, len(V) - 0.5]) + +ratings = ["Excellent", "Good", "Average", "Awful"] +Y = np.arange(len(V)) +L = np.zeros(len(V)) + +for i in range(4): + color = lighten_color(cmap(3), 1.00 - i / 4) + ax.barh(Y, V[:, i], left=L, color=color, label=ratings[i]) + for j in range(len(V)): + ax.text( + L[j] + V[j, i] / 2, + Y[j], + "%d%%" % V[j, i], + zorder=10, + ha="center", + va="center", + color="white", + size="x-small", + ) + L += V[:, i] + + +ax.legend(frameon=False, bbox_to_anchor=(1.0, 1), loc=2, borderaxespad=0) +ax.set_yticks(Y) +ax.set_yticklabels(["Rating %d" % (i + 1) for i in Y]) + +ax.spines["top"].set_visible(False) +ax.spines["right"].set_visible(False) +ax.spines["bottom"].set_visible(False) + +plt.tight_layout() +plt.savefig("stacked-bars.pdf") +plt.show() diff --git a/cover/back-cover.aux b/cover/back-cover.aux new file mode 100644 index 0000000..b640121 --- /dev/null +++ b/cover/back-cover.aux @@ -0,0 +1,2 @@ +\relax +\gdef \@abspage@last{1} diff --git a/cover/back-cover.log b/cover/back-cover.log new file mode 100644 index 0000000..75b25e1 --- /dev/null +++ b/cover/back-cover.log @@ -0,0 +1,159 @@ +This is pdfTeX, Version 3.141592653-2.6-1.40.23 (TeX Live 2022/dev) (preloaded format=pdflatex 2021.7.18) 29 JUL 2021 12:37 +entering extended mode + restricted \write18 enabled. + %&-line parsing enabled. +**back-cover.tex +(./back-cover.tex +LaTeX2e <2021-06-01> patch level 1 +L3 programming layer <2021-07-12> +(/usr/local/texlive/2021/texmf-dist/tex/latex/standalone/standalone.cls +Document Class: standalone 2018/03/26 v1.3a Class to compile TeX sub-files stan +dalone +(/usr/local/texlive/2021/texmf-dist/tex/latex/tools/shellesc.sty +Package: shellesc 2019/11/08 v1.0c unified shell escape interface for LaTeX +Package shellesc Info: Restricted shell escape enabled on input line 77. +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/ifluatex.sty +Package: ifluatex 2019/10/25 v1.5 ifluatex legacy package. Use iftex instead. + +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/iftex.sty +Package: iftex 2020/03/06 v1.0d TeX engine tests +)) +(/usr/local/texlive/2021/texmf-dist/tex/latex/xkeyval/xkeyval.sty +Package: xkeyval 2020/11/20 v2.8 package option processing (HA) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/xkeyval.tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/xkvutils.tex +\XKV@toks=\toks16 +\XKV@tempa@toks=\toks17 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/keyval.tex)) +\XKV@depth=\count182 +File: xkeyval.tex 2014/12/03 v2.7a key=value parser (HA) +)) +\sa@internal=\count183 +\c@sapage=\count184 + +(/usr/local/texlive/2021/texmf-dist/tex/latex/standalone/standalone.cfg +File: standalone.cfg 2018/03/26 v1.3a Default configuration file for 'standalon +e' class +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/article.cls +Document Class: article 2021/02/12 v1.4n Standard LaTeX document class +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2021/02/12 v1.4n Standard LaTeX file (size option) +) +\c@part=\count185 +\c@section=\count186 +\c@subsection=\count187 +\c@subsubsection=\count188 +\c@paragraph=\count189 +\c@subparagraph=\count190 +\c@figure=\count191 +\c@table=\count192 +\abovecaptionskip=\skip47 +\belowcaptionskip=\skip48 +\bibindent=\dimen138 +) +\sa@box=\box50 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphicx.sty +Package: graphicx 2020/12/05 v1.2c Enhanced LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphics.sty +Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/trig.sty +Package: trig 2016/01/03 v1.10 sin cos tan (DPC) +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-cfg/graphics.cfg +File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration +) +Package graphics Info: Driver file: pdftex.def on input line 107. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-def/pdftex.def +File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex +)) +\Gin@req@height=\dimen139 +\Gin@req@width=\dimen140 +) +\coverheight=\skip49 +\coverwidth=\skip50 +\bleedwidth=\skip51 +\spinewidth=\skip52 +\marklength=\skip53 +\trimleft=\skip54 +\trimright=\skip55 +\trimbottom=\skip56 +\trimtop=\skip57 + +(/usr/local/texlive/2021/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def +File: l3backend-pdftex.def 2021-07-12 L3 backend support: PDF output (pdfTeX) +\l__color_backend_stack_int=\count193 +\l__pdf_internal_box=\box51 +) +(./back-cover.aux) +\openout1 = `back-cover.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. + +(/usr/local/texlive/2021/texmf-dist/tex/context/base/mkii/supp-pdf.mkii +[Loading MPS to PDF converter (version 2006.09.02).] +\scratchcounter=\count194 +\scratchdimen=\dimen141 +\scratchbox=\box52 +\nofMPsegments=\count195 +\nofMParguments=\count196 +\everyMPshowfont=\toks18 +\MPscratchCnt=\count197 +\MPscratchDim=\dimen142 +\MPnumerator=\count198 +\makeMPintoPDFobject=\count199 +\everyMPtoPDFconversion=\toks19 +) (/usr/local/texlive/2021/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty +Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf +Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 +85. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg +File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv +e +)) + +File: print-cover.pdf Graphic file (type pdf) + +Package pdftex.def Info: print-cover.pdf used on input line 34. +(pdftex.def) Requested size: 438.1817pt x 614.57951pt. + +[1 + + <./print-cover.pdf>] (./back-cover.aux) ) +Here is how much of TeX's memory you used: + 1721 strings out of 478542 + 27763 string characters out of 5850393 + 320536 words of memory out of 5000000 + 19681 multiletter control sequences out of 15000+600000 + 403430 words of font info for 27 fonts, out of 8000000 for 9000 + 1141 hyphenation exceptions out of 8191 + 57i,5n,71p,210b,217s stack positions out of 5000i,500n,10000p,200000b,80000s + +Output written on back-cover.pdf (1 page, 226122 bytes). +PDF statistics: + 44 PDF objects out of 1000 (max. 8388607) + 23 compressed objects within 1 object stream + 0 named destinations out of 1000 (max. 500000) + 12 words of extra memory for PDF output out of 10000 (max. 10000000) + diff --git a/cover/back-cover.pdf b/cover/back-cover.pdf new file mode 100644 index 0000000..1391dff Binary files /dev/null and b/cover/back-cover.pdf differ diff --git a/cover/back-cover.tex b/cover/back-cover.tex new file mode 100644 index 0000000..3799b24 --- /dev/null +++ b/cover/back-cover.tex @@ -0,0 +1,38 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\makeatletter +\newcommand{\definetrim}[2]{% + \define@key{Gin}{#1}[]{\setkeys{Gin}{trim=#2,clip}}% +} +\makeatother + +\newlength{\coverheight} +\newlength{\coverwidth} +\newlength{\bleedwidth} +\newlength{\spinewidth} +\newlength{\marklength} + +\newlength{\trimleft} +\newlength{\trimright} +\newlength{\trimbottom} +\newlength{\trimtop} + +\setlength{\coverheight}{210mm} +\setlength{\coverwidth}{148mm} +\setlength{\bleedwidth}{19mm} +\setlength{\spinewidth}{23mm} +\setlength{\marklength}{3mm} + +\setlength{\trimleft}{\dimexpr \bleedwidth \relax } +\setlength{\trimright}{\dimexpr \bleedwidth + \coverwidth + \spinewidth \relax } +\setlength{\trimbottom}{\dimexpr \bleedwidth \relax } +\setlength{\trimtop}{\dimexpr \bleedwidth \relax } +\definetrim{front}{\trimleft{} \trimbottom{} \trimright{} \trimtop{}} + +\begin{document} +\includegraphics[front,height=216mm]{print-cover.pdf} +\end{document} + +% Largeur: 371.7 - 210.7 - 22 +% Hauteur: 260 mm diff --git a/cover/cover-pattern.pdf b/cover/cover-pattern.pdf new file mode 100644 index 0000000..84197a5 Binary files /dev/null and b/cover/cover-pattern.pdf differ diff --git a/cover/cover-pattern.py b/cover/cover-pattern.py new file mode 100644 index 0000000..7b07887 --- /dev/null +++ b/cover/cover-pattern.py @@ -0,0 +1,27 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PatchCollection + +inch = 25.4 +width = 371.7 +height = 260 + +fig = plt.figure(figsize=(width/inch,height/inch)) +ax = fig.add_axes([0,0,1,1], aspect=1, frameon=False) + +radius = 25 +patches = [] +for y in range(11,-1,-1): + for x in range(8,-1,-1): + center = 2*x*radius + y%2*radius, y*radius + for i,r in enumerate(np.linspace(25, 2.5, 10)): + patches.append(plt.Circle(center, r)) + +collection = PatchCollection(patches, edgecolors="0.1", facecolors='black') +ax.add_collection(collection) + +ax.set_xlim(0, width) +ax.set_ylim(0, height) + +plt.savefig("cover-pattern.pdf") +plt.show() diff --git a/cover/front-cover.aux b/cover/front-cover.aux new file mode 100644 index 0000000..b640121 --- /dev/null +++ b/cover/front-cover.aux @@ -0,0 +1,2 @@ +\relax +\gdef \@abspage@last{1} diff --git a/cover/front-cover.log b/cover/front-cover.log new file mode 100644 index 0000000..8a71010 --- /dev/null +++ b/cover/front-cover.log @@ -0,0 +1,159 @@ +This is pdfTeX, Version 3.141592653-2.6-1.40.23 (TeX Live 2022/dev) (preloaded format=pdflatex 2021.7.18) 29 JUL 2021 12:36 +entering extended mode + restricted \write18 enabled. + %&-line parsing enabled. +**front-cover.tex +(./front-cover.tex +LaTeX2e <2021-06-01> patch level 1 +L3 programming layer <2021-07-12> +(/usr/local/texlive/2021/texmf-dist/tex/latex/standalone/standalone.cls +Document Class: standalone 2018/03/26 v1.3a Class to compile TeX sub-files stan +dalone +(/usr/local/texlive/2021/texmf-dist/tex/latex/tools/shellesc.sty +Package: shellesc 2019/11/08 v1.0c unified shell escape interface for LaTeX +Package shellesc Info: Restricted shell escape enabled on input line 77. +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/ifluatex.sty +Package: ifluatex 2019/10/25 v1.5 ifluatex legacy package. Use iftex instead. + +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/iftex.sty +Package: iftex 2020/03/06 v1.0d TeX engine tests +)) +(/usr/local/texlive/2021/texmf-dist/tex/latex/xkeyval/xkeyval.sty +Package: xkeyval 2020/11/20 v2.8 package option processing (HA) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/xkeyval.tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/xkvutils.tex +\XKV@toks=\toks16 +\XKV@tempa@toks=\toks17 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/xkeyval/keyval.tex)) +\XKV@depth=\count182 +File: xkeyval.tex 2014/12/03 v2.7a key=value parser (HA) +)) +\sa@internal=\count183 +\c@sapage=\count184 + +(/usr/local/texlive/2021/texmf-dist/tex/latex/standalone/standalone.cfg +File: standalone.cfg 2018/03/26 v1.3a Default configuration file for 'standalon +e' class +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/article.cls +Document Class: article 2021/02/12 v1.4n Standard LaTeX document class +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2021/02/12 v1.4n Standard LaTeX file (size option) +) +\c@part=\count185 +\c@section=\count186 +\c@subsection=\count187 +\c@subsubsection=\count188 +\c@paragraph=\count189 +\c@subparagraph=\count190 +\c@figure=\count191 +\c@table=\count192 +\abovecaptionskip=\skip47 +\belowcaptionskip=\skip48 +\bibindent=\dimen138 +) +\sa@box=\box50 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphicx.sty +Package: graphicx 2020/12/05 v1.2c Enhanced LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphics.sty +Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/trig.sty +Package: trig 2016/01/03 v1.10 sin cos tan (DPC) +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-cfg/graphics.cfg +File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration +) +Package graphics Info: Driver file: pdftex.def on input line 107. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-def/pdftex.def +File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex +)) +\Gin@req@height=\dimen139 +\Gin@req@width=\dimen140 +) +\coverheight=\skip49 +\coverwidth=\skip50 +\bleedwidth=\skip51 +\spinewidth=\skip52 +\marklength=\skip53 +\trimleft=\skip54 +\trimright=\skip55 +\trimbottom=\skip56 +\trimtop=\skip57 + +(/usr/local/texlive/2021/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def +File: l3backend-pdftex.def 2021-07-12 L3 backend support: PDF output (pdfTeX) +\l__color_backend_stack_int=\count193 +\l__pdf_internal_box=\box51 +) +(./front-cover.aux) +\openout1 = `front-cover.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 33. +LaTeX Font Info: ... okay on input line 33. + +(/usr/local/texlive/2021/texmf-dist/tex/context/base/mkii/supp-pdf.mkii +[Loading MPS to PDF converter (version 2006.09.02).] +\scratchcounter=\count194 +\scratchdimen=\dimen141 +\scratchbox=\box52 +\nofMPsegments=\count195 +\nofMParguments=\count196 +\everyMPshowfont=\toks18 +\MPscratchCnt=\count197 +\MPscratchDim=\dimen142 +\MPnumerator=\count198 +\makeMPintoPDFobject=\count199 +\everyMPtoPDFconversion=\toks19 +) (/usr/local/texlive/2021/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty +Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf +Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 +85. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg +File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv +e +)) + +File: print-cover.pdf Graphic file (type pdf) + +Package pdftex.def Info: print-cover.pdf used on input line 34. +(pdftex.def) Requested size: 438.18172pt x 614.57951pt. + +[1 + + <./print-cover.pdf>] (./front-cover.aux) ) +Here is how much of TeX's memory you used: + 1721 strings out of 478542 + 27774 string characters out of 5850393 + 320538 words of memory out of 5000000 + 19681 multiletter control sequences out of 15000+600000 + 403430 words of font info for 27 fonts, out of 8000000 for 9000 + 1141 hyphenation exceptions out of 8191 + 57i,5n,71p,211b,217s stack positions out of 5000i,500n,10000p,200000b,80000s + +Output written on front-cover.pdf (1 page, 226126 bytes). +PDF statistics: + 44 PDF objects out of 1000 (max. 8388607) + 23 compressed objects within 1 object stream + 0 named destinations out of 1000 (max. 500000) + 12 words of extra memory for PDF output out of 10000 (max. 10000000) + diff --git a/cover/front-cover.pdf b/cover/front-cover.pdf new file mode 100644 index 0000000..0ccfd5a Binary files /dev/null and b/cover/front-cover.pdf differ diff --git a/cover/front-cover.tex b/cover/front-cover.tex new file mode 100644 index 0000000..64e3670 --- /dev/null +++ b/cover/front-cover.tex @@ -0,0 +1,38 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\makeatletter +\newcommand{\definetrim}[2]{% + \define@key{Gin}{#1}[]{\setkeys{Gin}{trim=#2,clip}}% +} +\makeatother + +\newlength{\coverheight} +\newlength{\coverwidth} +\newlength{\bleedwidth} +\newlength{\spinewidth} +\newlength{\marklength} + +\newlength{\trimleft} +\newlength{\trimright} +\newlength{\trimbottom} +\newlength{\trimtop} + +\setlength{\coverheight}{210mm} +\setlength{\coverwidth}{148mm} +\setlength{\bleedwidth}{19mm} +\setlength{\spinewidth}{23mm} +\setlength{\marklength}{3mm} + +\setlength{\trimleft}{\dimexpr \bleedwidth + \coverwidth + \spinewidth \relax } +\setlength{\trimright}{\dimexpr \bleedwidth \relax } +\setlength{\trimbottom}{\dimexpr \bleedwidth \relax } +\setlength{\trimtop}{\dimexpr \bleedwidth \relax } +\definetrim{front}{\trimleft{} \trimbottom{} \trimright{} \trimtop{}} + +\begin{document} +\includegraphics[front,height=216mm]{print-cover.pdf} +\end{document} + +% Largeur: 371.7 - 210.7 - 22 +% Hauteur: 260 mm diff --git a/cover/gravatar.png b/cover/gravatar.png new file mode 100644 index 0000000..8547364 Binary files /dev/null and b/cover/gravatar.png differ diff --git a/cover/print-cover.aux b/cover/print-cover.aux new file mode 100644 index 0000000..b640121 --- /dev/null +++ b/cover/print-cover.aux @@ -0,0 +1,2 @@ +\relax +\gdef \@abspage@last{1} diff --git a/cover/print-cover.log b/cover/print-cover.log new file mode 100644 index 0000000..1a85549 --- /dev/null +++ b/cover/print-cover.log @@ -0,0 +1,767 @@ +This is XeTeX, Version 3.141592653-2.6-0.999993 (TeX Live 2021) (preloaded format=xelatex 2021.7.18) 30 JUL 2021 11:46 +entering extended mode + restricted \write18 enabled. + %&-line parsing enabled. +**print-cover.tex +(./print-cover.tex +LaTeX2e <2021-06-01> patch level 1 +L3 programming layer <2021-07-12> +(/usr/local/texlive/2021/texmf-dist/tex/latex/bookcover/bookcover.cls +Document Class: bookcover 2021/01/04 v3.3 class for book covers and dust jacket +s +(/usr/local/texlive/2021/texmf-dist/tex/latex/kvoptions/kvoptions.sty +Package: kvoptions 2020-10-07 v3.14 Key value format for package options (HO) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/keyval.sty +Package: keyval 2014/10/28 v1.15 key=value parser (DPC) +\KV@toks@=\toks16 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/ltxcmds/ltxcmds.sty +Package: ltxcmds 2020-05-10 v1.25 LaTeX kernel commands for general use (HO) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/kvsetkeys/kvsetkeys.sty +Package: kvsetkeys 2019/12/15 v1.18 Key value parser (HO) +)) +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/article.cls +Document Class: article 2021/02/12 v1.4n Standard LaTeX document class +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/size10.clo +File: size10.clo 2021/02/12 v1.4n Standard LaTeX file (size option) +) +\c@part=\count178 +\c@section=\count179 +\c@subsection=\count180 +\c@subsubsection=\count181 +\c@paragraph=\count182 +\c@subparagraph=\count183 +\c@figure=\count184 +\c@table=\count185 +\abovecaptionskip=\skip47 +\belowcaptionskip=\skip48 +\bibindent=\dimen138 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/geometry/geometry.sty +Package: geometry 2020/01/02 v5.9 Page Geometry + +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/ifvtex.sty +Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead. + +(/usr/local/texlive/2021/texmf-dist/tex/generic/iftex/iftex.sty +Package: iftex 2020/03/06 v1.0d TeX engine tests +)) +\Gm@cnth=\count186 +\Gm@cntv=\count187 +\c@Gm@tempcnt=\count188 +\Gm@bindingoffset=\dimen139 +\Gm@wd@mp=\dimen140 +\Gm@odd@mp=\dimen141 +\Gm@even@mp=\dimen142 +\Gm@layoutwidth=\dimen143 +\Gm@layoutheight=\dimen144 +\Gm@layouthoffset=\dimen145 +\Gm@layoutvoffset=\dimen146 +\Gm@dimlist=\toks17 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphicx.sty +Package: graphicx 2020/12/05 v1.2c Enhanced LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/graphics.sty +Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics/trig.sty +Package: trig 2016/01/03 v1.10 sin cos tan (DPC) +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-cfg/graphics.cfg +File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration +) +Package graphics Info: Driver file: xetex.def on input line 107. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-def/xetex.def +File: xetex.def 2021/03/18 v5.0k Graphics/color driver for xetex +)) +\Gin@req@height=\dimen147 +\Gin@req@width=\dimen148 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/tools/calc.sty +Package: calc 2017/05/25 v4.3 Infix arithmetic (KKT,FJ) +\calc@Acount=\count189 +\calc@Bcount=\count190 +\calc@Adimen=\dimen149 +\calc@Bdimen=\dimen150 +\calc@Askip=\skip49 +\calc@Bskip=\skip50 +LaTeX Info: Redefining \setlength on input line 80. +LaTeX Info: Redefining \addtolength on input line 81. +\calc@Ccount=\count191 +\calc@Cskip=\skip51 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.te +x +\pgfutil@everybye=\toks18 +\pgfutil@tempdima=\dimen151 +\pgfutil@tempdimb=\dimen152 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfutil-common-li +sts.tex)) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def +\pgfutil@abb=\box50 +) (/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/pgf.revision.tex) +Package: pgfrcs 2021/05/15 v3.1.9a (3.1.9a) +)) +Package: pgf 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex +Package: pgfsys 2021/05/15 v3.1.9a (3.1.9a) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex +\pgfkeys@pathtoks=\toks19 +\pgfkeys@temptoks=\toks20 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfkeysfiltered.c +ode.tex +\pgfkeys@tmptoks=\toks21 +)) +\pgf@x=\dimen153 +\pgf@y=\dimen154 +\pgf@xa=\dimen155 +\pgf@ya=\dimen156 +\pgf@xb=\dimen157 +\pgf@yb=\dimen158 +\pgf@xc=\dimen159 +\pgf@yc=\dimen160 +\pgf@xd=\dimen161 +\pgf@yd=\dimen162 +\w@pgf@writea=\write3 +\r@pgf@reada=\read2 +\c@pgf@counta=\count192 +\c@pgf@countb=\count193 +\c@pgf@countc=\count194 +\c@pgf@countd=\count195 +\t@pgf@toka=\toks22 +\t@pgf@tokb=\toks23 +\t@pgf@tokc=\toks24 +\pgf@sys@id@count=\count196 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg +File: pgf.cfg 2021/05/15 v3.1.9a (3.1.9a) +) +Driver file for pgf: pgfsys-xetex.def + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-xetex.de +f +File: pgfsys-xetex.def 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-dvipdfmx +.def +File: pgfsys-dvipdfmx.def 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-p +df.def +File: pgfsys-common-pdf.def 2021/05/15 v3.1.9a (3.1.9a) +) +\pgfsys@objnum=\count197 +))) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath. +code.tex +File: pgfsyssoftpath.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfsyssoftpath@smallbuffer@items=\count198 +\pgfsyssoftpath@bigbuffer@items=\count199 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol. +code.tex +File: pgfsysprotocol.code.tex 2021/05/15 v3.1.9a (3.1.9a) +)) (/usr/local/texlive/2021/texmf-dist/tex/latex/xcolor/xcolor.sty +Package: xcolor 2016/05/11 v2.12 LaTeX color extensions (UK) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/graphics-cfg/color.cfg +File: color.cfg 2016/01/02 v1.6 sample color configuration +) +Package xcolor Info: Driver file: xetex.def on input line 225. +Package xcolor Info: Model `cmy' substituted by `cmy0' on input line 1348. +Package xcolor Info: Model `RGB' extended on input line 1364. +Package xcolor Info: Model `HTML' substituted by `rgb' on input line 1366. +Package xcolor Info: Model `Hsb' substituted by `hsb' on input line 1367. +Package xcolor Info: Model `tHsb' substituted by `hsb' on input line 1368. +Package xcolor Info: Model `HSB' substituted by `hsb' on input line 1369. +Package xcolor Info: Model `Gray' substituted by `gray' on input line 1370. +Package xcolor Info: Model `wave' substituted by `hsb' on input line 1371. +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex +Package: pgfcore 2021/05/15 v3.1.9a (3.1.9a) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex +\pgfmath@dimen=\dimen163 +\pgfmath@count=\count266 +\pgfmath@box=\box51 +\pgfmath@toks=\toks25 +\pgfmath@stack@operand=\toks26 +\pgfmath@stack@operation=\toks27 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code. +tex +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic +.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigo +nometric.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.rando +m.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.compa +rison.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base. +code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round +.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc. +code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integ +erarithmetics.code.tex))) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex +\c@pgfmathroundto@lastzeros=\count267 +)) (/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfint.code.tex) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.co +de.tex +File: pgfcorepoints.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@picminx=\dimen164 +\pgf@picmaxx=\dimen165 +\pgf@picminy=\dimen166 +\pgf@picmaxy=\dimen167 +\pgf@pathminx=\dimen168 +\pgf@pathmaxx=\dimen169 +\pgf@pathminy=\dimen170 +\pgf@pathmaxy=\dimen171 +\pgf@xx=\dimen172 +\pgf@xy=\dimen173 +\pgf@yx=\dimen174 +\pgf@yy=\dimen175 +\pgf@zx=\dimen176 +\pgf@zy=\dimen177 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconst +ruct.code.tex +File: pgfcorepathconstruct.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@path@lastx=\dimen178 +\pgf@path@lasty=\dimen179 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage +.code.tex +File: pgfcorepathusage.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@shorten@end@additional=\dimen180 +\pgf@shorten@start@additional=\dimen181 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.co +de.tex +File: pgfcorescopes.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfpic=\box52 +\pgf@hbox=\box53 +\pgf@layerbox@main=\box54 +\pgf@picture@serial@count=\count268 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicst +ate.code.tex +File: pgfcoregraphicstate.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgflinewidth=\dimen182 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransform +ations.code.tex +File: pgfcoretransformations.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@pt@x=\dimen183 +\pgf@pt@y=\dimen184 +\pgf@pt@temp=\dimen185 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.cod +e.tex +File: pgfcorequick.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.c +ode.tex +File: pgfcoreobjects.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathproce +ssing.code.tex +File: pgfcorepathprocessing.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.co +de.tex +File: pgfcorearrows.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfarrowsep=\dimen186 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.cod +e.tex +File: pgfcoreshade.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@max=\dimen187 +\pgf@sys@shading@range@num=\count269 +\pgf@shadingcount=\count270 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.cod +e.tex +File: pgfcoreimage.code.tex 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal. +code.tex +File: pgfcoreexternal.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfexternal@startupbox=\box55 +)) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.co +de.tex +File: pgfcorelayers.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretranspare +ncy.code.tex +File: pgfcoretransparency.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns. +code.tex +File: pgfcorepatterns.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code. +tex +File: pgfcorerdf.code.tex 2021/05/15 v3.1.9a (3.1.9a) +))) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.cod +e.tex +File: pgfmoduleshapes.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfnodeparttextbox=\box56 +) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code. +tex +File: pgfmoduleplot.code.tex 2021/05/15 v3.1.9a (3.1.9a) +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version +-0-65.sty +Package: pgfcomp-version-0-65 2021/05/15 v3.1.9a (3.1.9a) +\pgf@nodesepstart=\dimen188 +\pgf@nodesepend=\dimen189 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version +-1-18.sty +Package: pgfcomp-version-1-18 2021/05/15 v3.1.9a (3.1.9a) +)) +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/utilities/pgffor.sty +(/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex) +) (/usr/local/texlive/2021/texmf-dist/tex/latex/pgf/math/pgfmath.sty +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex)) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex +Package: pgffor 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex) +\pgffor@iter=\dimen190 +\pgffor@skip=\dimen191 +\pgffor@stack=\toks28 +\pgffor@toks=\toks29 +)) +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.cod +e.tex +Package: tikz 2021/05/15 v3.1.9a (3.1.9a) + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothan +dlers.code.tex +File: pgflibraryplothandlers.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgf@plot@mark@count=\count271 +\pgfplotmarksize=\dimen192 +) +\tikz@lastx=\dimen193 +\tikz@lasty=\dimen194 +\tikz@lastxsaved=\dimen195 +\tikz@lastysaved=\dimen196 +\tikz@lastmovetox=\dimen197 +\tikz@lastmovetoy=\dimen198 +\tikzleveldistance=\dimen199 +\tikzsiblingdistance=\dimen256 +\tikz@figbox=\box57 +\tikz@figbox@bg=\box58 +\tikz@tempbox=\box59 +\tikz@tempbox@bg=\box60 +\tikztreelevel=\count272 +\tikznumberofchildren=\count273 +\tikznumberofcurrentchild=\count274 +\tikz@fig@count=\count275 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.cod +e.tex +File: pgfmodulematrix.code.tex 2021/05/15 v3.1.9a (3.1.9a) +\pgfmatrixcurrentrow=\count276 +\pgfmatrixcurrentcolumn=\count277 +\pgf@matrix@numberofcolumns=\count278 +) +\tikz@expandcount=\count279 + +(/usr/local/texlive/2021/texmf-dist/tex/generic/pgf/frontendlayer/tikz/librarie +s/tikzlibrarytopaths.code.tex +File: tikzlibrarytopaths.code.tex 2021/05/15 v3.1.9a (3.1.9a) +))) +(/usr/local/texlive/2021/texmf-dist/tex/latex/l3packages/xparse/xparse.sty +(/usr/local/texlive/2021/texmf-dist/tex/latex/l3kernel/expl3.sty +Package: expl3 2021-07-12 L3 programming layer (loader) + +(/usr/local/texlive/2021/texmf-dist/tex/latex/l3backend/l3backend-xetex.def +File: l3backend-xetex.def 2021-07-12 L3 backend support: XeTeX + +(|extractbb --version) +\c__kernel_sys_dvipdfmx_version_int=\count280 +\l__color_backend_stack_int=\count281 +\g__color_backend_stack_int=\count282 +\g__graphics_track_int=\count283 +\l__pdf_internal_box=\box61 +\g__pdf_backend_object_int=\count284 +\g__pdf_backend_annotation_int=\count285 +\g__pdf_backend_link_int=\count286 +)) +Package: xparse 2021-06-18 L3 Experimental document command parser +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/etoolbox/etoolbox.sty +Package: etoolbox 2020/10/05 v2.5k e-TeX tools for LaTeX (JAW) +\etb@tempcnta=\count287 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/fgruler/fgruler.sty +Package: fgruler 2020/11/04 v1.3 Package for drawing rulers on the foreground o +r in the text +\fgruler@marklth=\skip52 +\fgruler@sep=\skip53 +\fgruler@width=\skip54 +\fgruler@fg@width=\skip55 +\fgruler@fg@height=\skip56 +\fgruler@tempdima=\dimen257 +\fgruler@tempdimb=\dimen258 +\c@fgrulernum=\count288 +\c@fgruler@check=\count289 +) +\coverwidth=\skip57 +\coverheight=\skip58 +\spinewidth=\skip59 +\flapwidth=\skip60 +\wrapwidth=\skip61 +\marklength=\skip62 +\markthick=\skip63 +\bleedwidth=\skip64 +\bookcover@templength@a=\skip65 +\bookcover@templength@b=\skip66 +\bookcover@templength@c=\skip67 +\bookcover@templength@d=\skip68 +\bookcover@xpos@=\skip69 +\bookcover@ypos@=\skip70 +\bookcover@partwidth@=\skip71 +\bookcover@partheight@=\skip72 +\bookcover@tikz@trimmed@part@width@minus=\skip73 +\bookcover@tikz@trimmed@part@height@minus=\skip74 +\bookcover@tikz@trimmed@part@push@right=\skip75 +\bookcover@tikz@trimmed@part@push@up=\skip76 +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/setspace/setspace.sty +Package: setspace 2011/12/19 v6.7a set line spacing +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/fontspec/fontspec.sty +Package: fontspec 2020/02/21 v2.7i Font selection for XeLaTeX and LuaLaTeX + +(/usr/local/texlive/2021/texmf-dist/tex/latex/fontspec/fontspec-xetex.sty +Package: fontspec-xetex 2020/02/21 v2.7i Font selection for XeLaTeX and LuaLaTe +X +\l__fontspec_script_int=\count290 +\l__fontspec_language_int=\count291 +\l__fontspec_strnum_int=\count292 +\l__fontspec_tmp_int=\count293 +\l__fontspec_tmpa_int=\count294 +\l__fontspec_tmpb_int=\count295 +\l__fontspec_tmpc_int=\count296 +\l__fontspec_em_int=\count297 +\l__fontspec_emdef_int=\count298 +\l__fontspec_strong_int=\count299 +\l__fontspec_strongdef_int=\count300 +\l__fontspec_tmpa_dim=\dimen259 +\l__fontspec_tmpb_dim=\dimen260 +\l__fontspec_tmpc_dim=\dimen261 + +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/fontenc.sty +Package: fontenc 2021/04/29 v2.0v Standard LaTeX package +) +(/usr/local/texlive/2021/texmf-dist/tex/latex/fontspec/fontspec.cfg))) + +Package fontspec Info: Font family 'Avenir(0)' created for font 'Avenir' with +(fontspec) options [Ligatures=TeX]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;mapping=tex-text +;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;mapping=tex-te +xt;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;mapping=tex-te +xt;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;mapping=tex-t +ext;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + + +(./print-cover.aux) +\openout1 = `print-cover.aux'. + +LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 21. +LaTeX Font Info: Trying to load font information for TS1+cmr on input line 2 +1. + +(/usr/local/texlive/2021/texmf-dist/tex/latex/base/ts1cmr.fd +File: ts1cmr.fd 2019/12/16 v2.5j Standard LaTeX font definitions +) +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for TU/lmr/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. +LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 21. +LaTeX Font Info: ... okay on input line 21. + +*geometry* driver: auto-detecting +*geometry* detected driver: xetex +*geometry* verbose mode - [ preamble ] result: +* driver: xetex +* paper: custom +* layout: +* layoutoffset:(h,v)=(0.0pt,0.0pt) +* modes: +* h-part:(L,W,R)=(0.0pt, 1032.83498pt, 0.0pt) +* v-part:(T,H,B)=(0.0pt, 722.69997pt, 0.0pt) +* \paperwidth=1032.83498pt +* \paperheight=722.69997pt +* \textwidth=1032.83498pt +* \textheight=722.69997pt +* \oddsidemargin=-72.26999pt +* \evensidemargin=-72.26999pt +* \topmargin=-109.26999pt +* \headheight=12.0pt +* \headsep=25.0pt +* \topskip=10.0pt +* \footskip=30.0pt +* \marginparwidth=65.0pt +* \marginparsep=11.0pt +* \columnsep=10.0pt +* \skip\footins=9.0pt plus 4.0pt minus 2.0pt +* \hoffset=0.0pt +* \voffset=0.0pt +* \mag=1000 +* \@twocolumnfalse +* \@twosidefalse +* \@mparswitchfalse +* \@reversemarginfalse +* (1in=72.27pt=25.4mm, 1cm=28.453pt) + + +Package fontspec Info: Adjusting the maths setup (use [no-math] to avoid +(fontspec) this). + +\symlegacymaths=\mathgroup4 +LaTeX Font Info: Overwriting symbol font `legacymaths' in version `bold' +(Font) OT1/cmr/m/n --> OT1/cmr/bx/n on input line 21. +LaTeX Font Info: Redeclaring math accent \acute on input line 21. +LaTeX Font Info: Redeclaring math accent \grave on input line 21. +LaTeX Font Info: Redeclaring math accent \ddot on input line 21. +LaTeX Font Info: Redeclaring math accent \tilde on input line 21. +LaTeX Font Info: Redeclaring math accent \bar on input line 21. +LaTeX Font Info: Redeclaring math accent \breve on input line 21. +LaTeX Font Info: Redeclaring math accent \check on input line 21. +LaTeX Font Info: Redeclaring math accent \hat on input line 21. +LaTeX Font Info: Redeclaring math accent \dot on input line 21. +LaTeX Font Info: Redeclaring math accent \mathring on input line 21. +LaTeX Font Info: Redeclaring math symbol \colon on input line 21. +LaTeX Font Info: Redeclaring math symbol \Gamma on input line 21. +LaTeX Font Info: Redeclaring math symbol \Delta on input line 21. +LaTeX Font Info: Redeclaring math symbol \Theta on input line 21. +LaTeX Font Info: Redeclaring math symbol \Lambda on input line 21. +LaTeX Font Info: Redeclaring math symbol \Xi on input line 21. +LaTeX Font Info: Redeclaring math symbol \Pi on input line 21. +LaTeX Font Info: Redeclaring math symbol \Sigma on input line 21. +LaTeX Font Info: Redeclaring math symbol \Upsilon on input line 21. +LaTeX Font Info: Redeclaring math symbol \Phi on input line 21. +LaTeX Font Info: Redeclaring math symbol \Psi on input line 21. +LaTeX Font Info: Redeclaring math symbol \Omega on input line 21. +LaTeX Font Info: Redeclaring math symbol \mathdollar on input line 21. +LaTeX Font Info: Redeclaring symbol font `operators' on input line 21. +LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font +(Font) `operators' in the math version `normal' on input line 21. +LaTeX Font Info: Overwriting symbol font `operators' in version `normal' +(Font) OT1/cmr/m/n --> TU/Avenir(0)/m/n on input line 21. +LaTeX Font Info: Encoding `OT1' has changed to `TU' for symbol font +(Font) `operators' in the math version `bold' on input line 21. +LaTeX Font Info: Overwriting symbol font `operators' in version `bold' +(Font) OT1/cmr/bx/n --> TU/Avenir(0)/m/n on input line 21. +LaTeX Font Info: Overwriting symbol font `operators' in version `normal' +(Font) TU/Avenir(0)/m/n --> TU/Avenir(0)/m/n on input line 21. + +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `normal' +(Font) OT1/cmr/m/it --> TU/Avenir(0)/m/it on input line 21. +LaTeX Font Info: Overwriting math alphabet `\mathbf' in version `normal' +(Font) OT1/cmr/bx/n --> TU/Avenir(0)/b/n on input line 21. +LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `normal' +(Font) OT1/cmss/m/n --> TU/lmss/m/n on input line 21. +LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `normal' +(Font) OT1/cmtt/m/n --> TU/lmtt/m/n on input line 21. +LaTeX Font Info: Overwriting symbol font `operators' in version `bold' +(Font) TU/Avenir(0)/m/n --> TU/Avenir(0)/b/n on input line 21. + +LaTeX Font Info: Overwriting math alphabet `\mathit' in version `bold' +(Font) OT1/cmr/bx/it --> TU/Avenir(0)/b/it on input line 21. +LaTeX Font Info: Overwriting math alphabet `\mathsf' in version `bold' +(Font) OT1/cmss/bx/n --> TU/lmss/b/n on input line 21. +LaTeX Font Info: Overwriting math alphabet `\mathtt' in version `bold' +(Font) OT1/cmtt/m/n --> TU/lmtt/b/n on input line 21. +File: cover-pattern.pdf Graphic file (type pdf) + + +Package fontspec Info: Font family 'Avenir(1)' created for font 'Avenir' with +(fontspec) options [Ligatures=TeX,LetterSpace=45.0]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;letterspace=45.0 +;mapping=tex-text;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;letterspace=45 +.0;mapping=tex-text;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;letterspace=45 +.0;mapping=tex-text;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;letterspace=4 +5.0;mapping=tex-text;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + +File: ./gravatar.png Graphic file (type bmp) +<./gravatar.png> + +Package fontspec Info: Font family 'Avenir(2)' created for font 'Avenir' with +(fontspec) options [Ligatures=TeX,LetterSpace=52.0]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;letterspace=52.0 +;mapping=tex-text;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;letterspace=52 +.0;mapping=tex-text;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;letterspace=52 +.0;mapping=tex-text;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;letterspace=5 +2.0;mapping=tex-text;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + + +Package fontspec Info: Font family 'Avenir(3)' created for font 'Avenir' with +(fontspec) options +(fontspec) [Ligatures=TeX,LetterSpace=52.0,LetterSpace=22.0]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;letterspace=52.0 +;letterspace=22.0;mapping=tex-text;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;letterspace=52 +.0;letterspace=22.0;mapping=tex-text;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;letterspace=52 +.0;letterspace=22.0;mapping=tex-text;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;letterspace=5 +2.0;letterspace=22.0;mapping=tex-text;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + + +Package fontspec Info: Font family 'Avenir(4)' created for font 'Avenir' with +(fontspec) options +(fontspec) [Ligatures=TeX,LetterSpace=52.0,LetterSpace=22.0,LetterS +pace=65.0]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;letterspace=52.0 +;letterspace=22.0;letterspace=65.0;mapping=tex-text;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;letterspace=52 +.0;letterspace=22.0;letterspace=65.0;mapping=tex-text;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;letterspace=52 +.0;letterspace=22.0;letterspace=65.0;mapping=tex-text;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;letterspace=5 +2.0;letterspace=22.0;letterspace=65.0;mapping=tex-text;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + + +Package fontspec Info: Font family 'Avenir(5)' created for font 'Avenir' with +(fontspec) options [Ligatures=TeX,LetterSpace=25.0]. +(fontspec) +(fontspec) This font family consists of the following NFSS +(fontspec) series/shapes: +(fontspec) +(fontspec) - 'normal' (m/n) with NFSS spec.: +(fontspec) <->"Avenir/OT:script=latn;language=dflt;letterspace=25.0 +;mapping=tex-text;" +(fontspec) - 'small caps' (m/sc) with NFSS spec.: +(fontspec) - 'bold' (b/n) with NFSS spec.: +(fontspec) <->"Avenir/B/OT:script=latn;language=dflt;letterspace=25 +.0;mapping=tex-text;" +(fontspec) - 'bold small caps' (b/sc) with NFSS spec.: +(fontspec) - 'italic' (m/it) with NFSS spec.: +(fontspec) <->"Avenir/I/OT:script=latn;language=dflt;letterspace=25 +.0;mapping=tex-text;" +(fontspec) - 'italic small caps' (m/scit) with NFSS spec.: +(fontspec) - 'bold italic' (b/it) with NFSS spec.: +(fontspec) <->"Avenir/BI/OT:script=latn;language=dflt;letterspace=2 +5.0;mapping=tex-text;" +(fontspec) - 'bold italic small caps' (b/scit) with NFSS spec.: + +[1 + +] (./print-cover.aux) ) +Here is how much of TeX's memory you used: + 16010 strings out of 476477 + 375415 string characters out of 5814314 + 738946 words of memory out of 5000000 + 36197 multiletter control sequences out of 15000+600000 + 403622 words of font info for 51 fonts, out of 8000000 for 9000 + 1348 hyphenation exceptions out of 8191 + 99i,11n,103p,440b,648s stack positions out of 5000i,500n,10000p,200000b,80000s + +Output written on print-cover.pdf (1 page). diff --git a/cover/print-cover.pdf b/cover/print-cover.pdf new file mode 100644 index 0000000..1af1a83 Binary files /dev/null and b/cover/print-cover.pdf differ diff --git a/cover/print-cover.tex b/cover/print-cover.tex new file mode 100644 index 0000000..d170047 --- /dev/null +++ b/cover/print-cover.tex @@ -0,0 +1,102 @@ +\documentclass[ + coverheight=248mm, %% 210mm + 2x19mm + coverwidth=167mm, %% 148mm + 19mm + %% bleedwidth=19mm, % -> margin + bleedwidth=3mm, % + spinewidth=23mm, % (13mm + 10mm) + marklength=0mm, % + markcolor=white +]{bookcover} + +\letnamebookcoverpart{back typing area}{back}[19mm,19mm,0mm,19mm] +\letnamebookcoverpart{front typing area}{front}[0mm,19mm,19mm,19mm] +\letnamebookcoverpart{spine typing area}{spine}[5mm,19mm,5mm,19mm] + +\usepackage{setspace} +\usepackage{xcolor} +\usepackage{fontspec} +\setmainfont{Avenir} +\renewcommand{\baselinestretch}{1.5} + +\begin{document} + + \begin{bookcover} + + \bookcovercomponent{picture}{whole page}{cover-pattern.pdf} + + %% DEBUG + %% \bookcovercomponent{color}{bg whole}{white,opacity=0.05} + %% \bookcovercomponent{color}{back}{white,opacity=0.05} + %% \bookcovercomponent{color}{back typing area}{white,opacity=0.05} + %% \bookcovercomponent{color}{front}{white,opacity=0.05} + %% \bookcovercomponent{color}{front typing area}{white,opacity=0.05} + %% \bookcovercomponent{color}{spine}{white,opacity=0.05} + %% \bookcovercomponent{color}{spine typing area}{white,opacity=0.05} + + %% \begin{bookcoverelement}{tikz}{bg whole} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{front} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{front typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{back} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{back typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{spine} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{spine typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% END DEBUG + + + \bookcovercomponent{center}{spine}{ + \vspace{-31mm} + \addfontfeature{LetterSpace=45.0} + \rotatebox[origin=c]{-90}{\textcolor{yellow!80!white}{\Large + SCIENTIFIC~~PYTHON~~VOLUME~~II}}} + + \bookcovercomponent{normal}{spine typing area}[,,,181mm]{ + \centering + \includegraphics[width=13mm]{./gravatar.png} + } + + \bookcovercomponent{normal}{front typing area}[,,,20mm]{ + \centering + \addfontfeature{LetterSpace=52.0} + \textcolor{yellow!80!white}{\small SCIENTIFIC~~PYTHON~~VOLUME~~II} + + \addfontfeature{LetterSpace=22.0} + \textcolor{white}{\Large \bfseries SCIENTIFIC VISUALIZATION} + + \vspace{-1pt} + \addfontfeature{LetterSpace=65.0} + \textcolor{white}{\large PYTHON \& MATPLOTLIB} + } + + \bookcovercomponent{normal}{front typing area}[,,,187mm]{ + \centering + \addfontfeature{LetterSpace=25.0} + \textcolor{white}{NICOLAS~~P.~~ROUGIER} + } + + \bookcovercomponent{normal}{back typing area}[,,,180mm]{ + \setstretch{1.0} + \centering + \textcolor{white}{In memory of\\ + \textbf{John D. Hunter} + \& + \textbf{Maxim Shemanarev}\\ + Two brilliant minds that are dearly missed.} + } + + + \end{bookcover} +\end{document} diff --git a/figures/.DS_Store b/figures/.DS_Store new file mode 100644 index 0000000..0ffe925 Binary files /dev/null and b/figures/.DS_Store differ diff --git a/figures/anatomy/anatomy.pdf b/figures/anatomy/anatomy.pdf new file mode 100644 index 0000000..bcbe932 Binary files /dev/null and b/figures/anatomy/anatomy.pdf differ diff --git a/figures/anatomy/bold-ticklabel.pdf b/figures/anatomy/bold-ticklabel.pdf new file mode 100644 index 0000000..c6b0bb0 Binary files /dev/null and b/figures/anatomy/bold-ticklabel.pdf differ diff --git a/figures/anatomy/figure-dpi.pdf b/figures/anatomy/figure-dpi.pdf new file mode 100644 index 0000000..46a1c47 Binary files /dev/null and b/figures/anatomy/figure-dpi.pdf differ diff --git a/figures/anatomy/figure-dpi.png b/figures/anatomy/figure-dpi.png new file mode 100644 index 0000000..b03ed8e Binary files /dev/null and b/figures/anatomy/figure-dpi.png differ diff --git a/figures/anatomy/imgcat.png b/figures/anatomy/imgcat.png new file mode 100644 index 0000000..09e4e2d Binary files /dev/null and b/figures/anatomy/imgcat.png differ diff --git a/figures/anatomy/inch-cm.pdf b/figures/anatomy/inch-cm.pdf new file mode 100644 index 0000000..26ad211 Binary files /dev/null and b/figures/anatomy/inch-cm.pdf differ diff --git a/figures/anatomy/pixel-font.png b/figures/anatomy/pixel-font.png new file mode 100644 index 0000000..9ef1381 Binary files /dev/null and b/figures/anatomy/pixel-font.png differ diff --git a/figures/anatomy/raster-vector.pdf b/figures/anatomy/raster-vector.pdf new file mode 100644 index 0000000..72dbaed Binary files /dev/null and b/figures/anatomy/raster-vector.pdf differ diff --git a/figures/anatomy/ruler.pdf b/figures/anatomy/ruler.pdf new file mode 100644 index 0000000..3e25451 Binary files /dev/null and b/figures/anatomy/ruler.pdf differ diff --git a/figures/anatomy/zorder-plots.pdf b/figures/anatomy/zorder-plots.pdf new file mode 100644 index 0000000..bb1f31d Binary files /dev/null and b/figures/anatomy/zorder-plots.pdf differ diff --git a/figures/anatomy/zorder.pdf b/figures/anatomy/zorder.pdf new file mode 100644 index 0000000..16e619e Binary files /dev/null and b/figures/anatomy/zorder.pdf differ diff --git a/figures/animation/earthquakes-frame-50.pdf b/figures/animation/earthquakes-frame-50.pdf new file mode 100644 index 0000000..7dbe8f9 Binary files /dev/null and b/figures/animation/earthquakes-frame-50.pdf differ diff --git a/figures/animation/fluid-animation.png b/figures/animation/fluid-animation.png new file mode 100644 index 0000000..fc60ae7 Binary files /dev/null and b/figures/animation/fluid-animation.png differ diff --git a/figures/animation/less-is-more.png b/figures/animation/less-is-more.png new file mode 100644 index 0000000..68b3306 Binary files /dev/null and b/figures/animation/less-is-more.png differ diff --git a/figures/animation/lissajous.pdf b/figures/animation/lissajous.pdf new file mode 100644 index 0000000..449b115 Binary files /dev/null and b/figures/animation/lissajous.pdf differ diff --git a/figures/animation/platecarree.pdf b/figures/animation/platecarree.pdf new file mode 100644 index 0000000..b1dd036 Binary files /dev/null and b/figures/animation/platecarree.pdf differ diff --git a/figures/animation/rain.pdf b/figures/animation/rain.pdf new file mode 100644 index 0000000..e5e8355 Binary files /dev/null and b/figures/animation/rain.pdf differ diff --git a/figures/animation/sine-cosine-frame-001.pdf b/figures/animation/sine-cosine-frame-001.pdf new file mode 100644 index 0000000..b693495 Binary files /dev/null and b/figures/animation/sine-cosine-frame-001.pdf differ diff --git a/figures/animation/sine-cosine-frame-032.pdf b/figures/animation/sine-cosine-frame-032.pdf new file mode 100644 index 0000000..98d97d9 Binary files /dev/null and b/figures/animation/sine-cosine-frame-032.pdf differ diff --git a/figures/animation/sine-cosine-frame-128.pdf b/figures/animation/sine-cosine-frame-128.pdf new file mode 100644 index 0000000..0011c8f Binary files /dev/null and b/figures/animation/sine-cosine-frame-128.pdf differ diff --git a/figures/animation/sine-cosine-frame-255.pdf b/figures/animation/sine-cosine-frame-255.pdf new file mode 100644 index 0000000..5f2e688 Binary files /dev/null and b/figures/animation/sine-cosine-frame-255.pdf differ diff --git a/figures/animation/sine-cosine.mp4 b/figures/animation/sine-cosine.mp4 new file mode 100644 index 0000000..110dcee Binary files /dev/null and b/figures/animation/sine-cosine.mp4 differ diff --git a/figures/animation/sine-cosine.pdf b/figures/animation/sine-cosine.pdf new file mode 100644 index 0000000..0512b74 Binary files /dev/null and b/figures/animation/sine-cosine.pdf differ diff --git a/figures/beyond/basal-ganglia.pdf b/figures/beyond/basal-ganglia.pdf new file mode 100644 index 0000000..5f600f2 Binary files /dev/null and b/figures/beyond/basal-ganglia.pdf differ diff --git a/figures/beyond/bluenoise.pdf b/figures/beyond/bluenoise.pdf new file mode 100644 index 0000000..fb16a37 Binary files /dev/null and b/figures/beyond/bluenoise.pdf differ diff --git a/figures/beyond/dungeon.pdf b/figures/beyond/dungeon.pdf new file mode 100644 index 0000000..5c23f5f Binary files /dev/null and b/figures/beyond/dungeon.pdf differ diff --git a/figures/beyond/dungeon.png b/figures/beyond/dungeon.png new file mode 100644 index 0000000..09be29e Binary files /dev/null and b/figures/beyond/dungeon.png differ diff --git a/figures/beyond/dyson-hatching.pdf b/figures/beyond/dyson-hatching.pdf new file mode 100644 index 0000000..6d48257 Binary files /dev/null and b/figures/beyond/dyson-hatching.pdf differ diff --git a/figures/beyond/interactive-loupe.pdf b/figures/beyond/interactive-loupe.pdf new file mode 100644 index 0000000..5fe5efa Binary files /dev/null and b/figures/beyond/interactive-loupe.pdf differ diff --git a/figures/beyond/polygon-clipping.pdf b/figures/beyond/polygon-clipping.pdf new file mode 100644 index 0000000..21ca896 Binary files /dev/null and b/figures/beyond/polygon-clipping.pdf differ diff --git a/figures/beyond/radial-maze.pdf b/figures/beyond/radial-maze.pdf new file mode 100644 index 0000000..f07ed68 Binary files /dev/null and b/figures/beyond/radial-maze.pdf differ diff --git a/figures/beyond/stamp.png b/figures/beyond/stamp.png new file mode 100644 index 0000000..cbf8eb7 Binary files /dev/null and b/figures/beyond/stamp.png differ diff --git a/figures/beyond/tikz-dashes.pdf b/figures/beyond/tikz-dashes.pdf new file mode 100644 index 0000000..6693f24 Binary files /dev/null and b/figures/beyond/tikz-dashes.pdf differ diff --git a/figures/beyond/tinybot.pdf b/figures/beyond/tinybot.pdf new file mode 100644 index 0000000..b67cb2f Binary files /dev/null and b/figures/beyond/tinybot.pdf differ diff --git a/figures/cheatsheets/cheatsheets-1.pdf b/figures/cheatsheets/cheatsheets-1.pdf new file mode 100644 index 0000000..59212f0 Binary files /dev/null and b/figures/cheatsheets/cheatsheets-1.pdf differ diff --git a/figures/cheatsheets/cheatsheets-1.tex b/figures/cheatsheets/cheatsheets-1.tex new file mode 100644 index 0000000..c1d17c6 --- /dev/null +++ b/figures/cheatsheets/cheatsheets-1.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[trim=0cm 0cm 17.82cm 0cm, clip,height=210mm]{cheatsheets.pdf} +\end{document} + diff --git a/figures/cheatsheets/cheatsheets-2.pdf b/figures/cheatsheets/cheatsheets-2.pdf new file mode 100644 index 0000000..5efc361 Binary files /dev/null and b/figures/cheatsheets/cheatsheets-2.pdf differ diff --git a/figures/cheatsheets/cheatsheets-2.tex b/figures/cheatsheets/cheatsheets-2.tex new file mode 100644 index 0000000..e5f834b --- /dev/null +++ b/figures/cheatsheets/cheatsheets-2.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[trim=11.88cm 0cm 5.94cm 0cm, clip,height=210mm]{cheatsheets.pdf} +\end{document} + diff --git a/figures/cheatsheets/cheatsheets-3.pdf b/figures/cheatsheets/cheatsheets-3.pdf new file mode 100644 index 0000000..6cc5139 Binary files /dev/null and b/figures/cheatsheets/cheatsheets-3.pdf differ diff --git a/figures/cheatsheets/cheatsheets-3.tex b/figures/cheatsheets/cheatsheets-3.tex new file mode 100644 index 0000000..71bad5b --- /dev/null +++ b/figures/cheatsheets/cheatsheets-3.tex @@ -0,0 +1,8 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[trim=23.76cm 0cm 0cm 0cm, clip, page=1, height=210mm]{cheatsheets.pdf} +\includegraphics[trim=0cm 0cm 23.76cm 0cm, clip, page=2, height=210mm]{cheatsheets.pdf} +\end{document} + diff --git a/figures/cheatsheets/cheatsheets-4.pdf b/figures/cheatsheets/cheatsheets-4.pdf new file mode 100644 index 0000000..1779c08 Binary files /dev/null and b/figures/cheatsheets/cheatsheets-4.pdf differ diff --git a/figures/cheatsheets/cheatsheets-4.tex b/figures/cheatsheets/cheatsheets-4.tex new file mode 100644 index 0000000..4099911 --- /dev/null +++ b/figures/cheatsheets/cheatsheets-4.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[trim=5.94cm 0cm 11.88cm 0cm, clip, page=2, height=210mm]{cheatsheets.pdf} +\end{document} + diff --git a/figures/cheatsheets/cheatsheets-5.pdf b/figures/cheatsheets/cheatsheets-5.pdf new file mode 100644 index 0000000..1878c72 Binary files /dev/null and b/figures/cheatsheets/cheatsheets-5.pdf differ diff --git a/figures/cheatsheets/cheatsheets-5.tex b/figures/cheatsheets/cheatsheets-5.tex new file mode 100644 index 0000000..9d1e774 --- /dev/null +++ b/figures/cheatsheets/cheatsheets-5.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[trim=17.82cm 0cm 0cm 0cm, clip, page=2, height=210mm]{cheatsheets.pdf} +\end{document} + diff --git a/figures/cheatsheets/cheatsheets.pdf b/figures/cheatsheets/cheatsheets.pdf new file mode 100644 index 0000000..a4a8d0b Binary files /dev/null and b/figures/cheatsheets/cheatsheets.pdf differ diff --git a/figures/cheatsheets/handout-beginner-landscape.pdf b/figures/cheatsheets/handout-beginner-landscape.pdf new file mode 100644 index 0000000..5ff0ac3 Binary files /dev/null and b/figures/cheatsheets/handout-beginner-landscape.pdf differ diff --git a/figures/cheatsheets/handout-beginner.pdf b/figures/cheatsheets/handout-beginner.pdf new file mode 100644 index 0000000..1df4de2 Binary files /dev/null and b/figures/cheatsheets/handout-beginner.pdf differ diff --git a/figures/cheatsheets/handout-beginner.tex b/figures/cheatsheets/handout-beginner.tex new file mode 100644 index 0000000..6a96592 --- /dev/null +++ b/figures/cheatsheets/handout-beginner.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[angle=90]{handout-beginner-landscape.pdf} +\end{document} + diff --git a/figures/cheatsheets/handout-intermediate-landscape.pdf b/figures/cheatsheets/handout-intermediate-landscape.pdf new file mode 100644 index 0000000..4c88eae Binary files /dev/null and b/figures/cheatsheets/handout-intermediate-landscape.pdf differ diff --git a/figures/cheatsheets/handout-intermediate.pdf b/figures/cheatsheets/handout-intermediate.pdf new file mode 100644 index 0000000..ee2cddf Binary files /dev/null and b/figures/cheatsheets/handout-intermediate.pdf differ diff --git a/figures/cheatsheets/handout-intermediate.tex b/figures/cheatsheets/handout-intermediate.tex new file mode 100644 index 0000000..7a744ba --- /dev/null +++ b/figures/cheatsheets/handout-intermediate.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[angle=-90]{handout-intermediate-landscape.pdf} +\end{document} + diff --git a/figures/cheatsheets/handout-tips-landscape.pdf b/figures/cheatsheets/handout-tips-landscape.pdf new file mode 100644 index 0000000..704ed87 Binary files /dev/null and b/figures/cheatsheets/handout-tips-landscape.pdf differ diff --git a/figures/cheatsheets/handout-tips.pdf b/figures/cheatsheets/handout-tips.pdf new file mode 100644 index 0000000..90db2ee Binary files /dev/null and b/figures/cheatsheets/handout-tips.pdf differ diff --git a/figures/cheatsheets/handout-tips.tex b/figures/cheatsheets/handout-tips.tex new file mode 100644 index 0000000..4e3321e --- /dev/null +++ b/figures/cheatsheets/handout-tips.tex @@ -0,0 +1,7 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\begin{document} +\includegraphics[angle=90]{handout-tips-landscape.pdf} +\end{document} + diff --git a/figures/colors/alpha-scatter.pdf b/figures/colors/alpha-scatter.pdf new file mode 100644 index 0000000..80e953f Binary files /dev/null and b/figures/colors/alpha-scatter.pdf differ diff --git a/figures/colors/alpha-vs-color.pdf b/figures/colors/alpha-vs-color.pdf new file mode 100644 index 0000000..b43da8d Binary files /dev/null and b/figures/colors/alpha-vs-color.pdf differ diff --git a/figures/colors/color-gradients.pdf b/figures/colors/color-gradients.pdf new file mode 100644 index 0000000..bd48cd6 Binary files /dev/null and b/figures/colors/color-gradients.pdf differ diff --git a/figures/colors/color-wheel.pdf b/figures/colors/color-wheel.pdf new file mode 100644 index 0000000..cf284da Binary files /dev/null and b/figures/colors/color-wheel.pdf differ diff --git a/figures/colors/color-wheel.png b/figures/colors/color-wheel.png new file mode 100644 index 0000000..5fba719 Binary files /dev/null and b/figures/colors/color-wheel.png differ diff --git a/figures/colors/colored-hist.pdf b/figures/colors/colored-hist.pdf new file mode 100644 index 0000000..547a874 Binary files /dev/null and b/figures/colors/colored-hist.pdf differ diff --git a/figures/colors/colored-plot.pdf b/figures/colors/colored-plot.pdf new file mode 100644 index 0000000..c10dda5 Binary files /dev/null and b/figures/colors/colored-plot.pdf differ diff --git a/figures/colors/colormap-transform.graffle b/figures/colors/colormap-transform.graffle new file mode 100644 index 0000000..f2c92b0 Binary files /dev/null and b/figures/colors/colormap-transform.graffle differ diff --git a/figures/colors/colormap-transform.pdf b/figures/colors/colormap-transform.pdf new file mode 100644 index 0000000..3514a81 Binary files /dev/null and b/figures/colors/colormap-transform.pdf differ diff --git a/figures/colors/colormap-tree.graffle b/figures/colors/colormap-tree.graffle new file mode 100644 index 0000000..17e76af Binary files /dev/null and b/figures/colors/colormap-tree.graffle differ diff --git a/figures/colors/colormap-tree.pdf b/figures/colors/colormap-tree.pdf new file mode 100644 index 0000000..2913357 Binary files /dev/null and b/figures/colors/colormap-tree.pdf differ diff --git a/figures/colors/flower-polar.pdf b/figures/colors/flower-polar.pdf new file mode 100644 index 0000000..741ef91 Binary files /dev/null and b/figures/colors/flower-polar.pdf differ diff --git a/figures/colors/flower-polar.png b/figures/colors/flower-polar.png new file mode 100644 index 0000000..7529897 Binary files /dev/null and b/figures/colors/flower-polar.png differ diff --git a/figures/colors/material-colors.pdf b/figures/colors/material-colors.pdf new file mode 100644 index 0000000..138821b Binary files /dev/null and b/figures/colors/material-colors.pdf differ diff --git a/figures/colors/mona-lisa.pdf b/figures/colors/mona-lisa.pdf new file mode 100644 index 0000000..078f0d7 Binary files /dev/null and b/figures/colors/mona-lisa.pdf differ diff --git a/figures/colors/open-colors.pdf b/figures/colors/open-colors.pdf new file mode 100644 index 0000000..83d6802 Binary files /dev/null and b/figures/colors/open-colors.pdf differ diff --git a/figures/colors/stacked-plots.pdf b/figures/colors/stacked-plots.pdf new file mode 100644 index 0000000..977f194 Binary files /dev/null and b/figures/colors/stacked-plots.pdf differ diff --git a/figures/coordinates/collage.png b/figures/coordinates/collage.png new file mode 100644 index 0000000..00950f4 Binary files /dev/null and b/figures/coordinates/collage.png differ diff --git a/figures/coordinates/coordinates-cartesian.graffle b/figures/coordinates/coordinates-cartesian.graffle new file mode 100644 index 0000000..554910e Binary files /dev/null and b/figures/coordinates/coordinates-cartesian.graffle differ diff --git a/figures/coordinates/coordinates-cartesian.pdf b/figures/coordinates/coordinates-cartesian.pdf new file mode 100644 index 0000000..837503f Binary files /dev/null and b/figures/coordinates/coordinates-cartesian.pdf differ diff --git a/figures/coordinates/coordinates-polar.graffle b/figures/coordinates/coordinates-polar.graffle new file mode 100644 index 0000000..1d34775 Binary files /dev/null and b/figures/coordinates/coordinates-polar.graffle differ diff --git a/figures/coordinates/coordinates-polar.pdf b/figures/coordinates/coordinates-polar.pdf new file mode 100644 index 0000000..cb1e1d0 Binary files /dev/null and b/figures/coordinates/coordinates-polar.pdf differ diff --git a/figures/coordinates/transforms-blend.pdf b/figures/coordinates/transforms-blend.pdf new file mode 100644 index 0000000..99ec282 Binary files /dev/null and b/figures/coordinates/transforms-blend.pdf differ diff --git a/figures/coordinates/transforms-exercise-1.pdf b/figures/coordinates/transforms-exercise-1.pdf new file mode 100644 index 0000000..c55adeb Binary files /dev/null and b/figures/coordinates/transforms-exercise-1.pdf differ diff --git a/figures/coordinates/transforms-floating-axis.pdf b/figures/coordinates/transforms-floating-axis.pdf new file mode 100644 index 0000000..f821158 Binary files /dev/null and b/figures/coordinates/transforms-floating-axis.pdf differ diff --git a/figures/coordinates/transforms-hist.pdf b/figures/coordinates/transforms-hist.pdf new file mode 100644 index 0000000..a54315b Binary files /dev/null and b/figures/coordinates/transforms-hist.pdf differ diff --git a/figures/coordinates/transforms-letter.pdf b/figures/coordinates/transforms-letter.pdf new file mode 100644 index 0000000..2ea57b5 Binary files /dev/null and b/figures/coordinates/transforms-letter.pdf differ diff --git a/figures/coordinates/transforms-polar.pdf b/figures/coordinates/transforms-polar.pdf new file mode 100644 index 0000000..3210b46 Binary files /dev/null and b/figures/coordinates/transforms-polar.pdf differ diff --git a/figures/defaults/defaults-exercice-1.pdf b/figures/defaults/defaults-exercice-1.pdf new file mode 100644 index 0000000..ff15a4f Binary files /dev/null and b/figures/defaults/defaults-exercice-1.pdf differ diff --git a/figures/defaults/defaults-step-1.pdf b/figures/defaults/defaults-step-1.pdf new file mode 100644 index 0000000..1ff9ca7 Binary files /dev/null and b/figures/defaults/defaults-step-1.pdf differ diff --git a/figures/defaults/defaults-step-2.pdf b/figures/defaults/defaults-step-2.pdf new file mode 100644 index 0000000..62da404 Binary files /dev/null and b/figures/defaults/defaults-step-2.pdf differ diff --git a/figures/defaults/defaults-step-3.pdf b/figures/defaults/defaults-step-3.pdf new file mode 100644 index 0000000..e95eb2e Binary files /dev/null and b/figures/defaults/defaults-step-3.pdf differ diff --git a/figures/defaults/defaults-step-4.pdf b/figures/defaults/defaults-step-4.pdf new file mode 100644 index 0000000..9ef79b4 Binary files /dev/null and b/figures/defaults/defaults-step-4.pdf differ diff --git a/figures/defaults/defaults-step-5.pdf b/figures/defaults/defaults-step-5.pdf new file mode 100644 index 0000000..e7b9083 Binary files /dev/null and b/figures/defaults/defaults-step-5.pdf differ diff --git a/figures/defaults/sine-cosine-variants.graffle b/figures/defaults/sine-cosine-variants.graffle new file mode 100644 index 0000000..2f5589f Binary files /dev/null and b/figures/defaults/sine-cosine-variants.graffle differ diff --git a/figures/defaults/sine-cosine-variants.png b/figures/defaults/sine-cosine-variants.png new file mode 100644 index 0000000..062e571 Binary files /dev/null and b/figures/defaults/sine-cosine-variants.png differ diff --git a/figures/introduction/black-hole.jpg b/figures/introduction/black-hole.jpg new file mode 100644 index 0000000..45ed9be Binary files /dev/null and b/figures/introduction/black-hole.jpg differ diff --git a/figures/introduction/matplotlib-timeline.pdf b/figures/introduction/matplotlib-timeline.pdf new file mode 100644 index 0000000..34afce4 Binary files /dev/null and b/figures/introduction/matplotlib-timeline.pdf differ diff --git a/figures/introduction/matplotlib-timeline.png b/figures/introduction/matplotlib-timeline.png new file mode 100644 index 0000000..11c7d65 Binary files /dev/null and b/figures/introduction/matplotlib-timeline.png differ diff --git a/figures/introduction/visualization-landscape.pdf b/figures/introduction/visualization-landscape.pdf new file mode 100644 index 0000000..ffdb60c Binary files /dev/null and b/figures/introduction/visualization-landscape.pdf differ diff --git a/figures/introduction/visualization-landscape.png b/figures/introduction/visualization-landscape.png new file mode 100644 index 0000000..c0e12ec Binary files /dev/null and b/figures/introduction/visualization-landscape.png differ diff --git a/figures/layout/aspects.graffle b/figures/layout/aspects.graffle new file mode 100644 index 0000000..69755d0 Binary files /dev/null and b/figures/layout/aspects.graffle differ diff --git a/figures/layout/aspects.pdf b/figures/layout/aspects.pdf new file mode 100644 index 0000000..7f4ec51 Binary files /dev/null and b/figures/layout/aspects.pdf differ diff --git a/figures/layout/complex-layout-bare.pdf b/figures/layout/complex-layout-bare.pdf new file mode 100644 index 0000000..6b44f9b Binary files /dev/null and b/figures/layout/complex-layout-bare.pdf differ diff --git a/figures/layout/complex-layout.pdf b/figures/layout/complex-layout.pdf new file mode 100644 index 0000000..618ab35 Binary files /dev/null and b/figures/layout/complex-layout.pdf differ diff --git a/figures/layout/layout-aspect-1.pdf b/figures/layout/layout-aspect-1.pdf new file mode 100644 index 0000000..3c7b8fd Binary files /dev/null and b/figures/layout/layout-aspect-1.pdf differ diff --git a/figures/layout/layout-aspect-2.pdf b/figures/layout/layout-aspect-2.pdf new file mode 100644 index 0000000..6340d26 Binary files /dev/null and b/figures/layout/layout-aspect-2.pdf differ diff --git a/figures/layout/layout-aspect-3.pdf b/figures/layout/layout-aspect-3.pdf new file mode 100644 index 0000000..600429f Binary files /dev/null and b/figures/layout/layout-aspect-3.pdf differ diff --git a/figures/layout/layout-classical.pdf b/figures/layout/layout-classical.pdf new file mode 100644 index 0000000..ed188d3 Binary files /dev/null and b/figures/layout/layout-classical.pdf differ diff --git a/figures/layout/layout-gridspec.pdf b/figures/layout/layout-gridspec.pdf new file mode 100644 index 0000000..f03e0d2 Binary files /dev/null and b/figures/layout/layout-gridspec.pdf differ diff --git a/figures/layout/standard-layout-1.pdf b/figures/layout/standard-layout-1.pdf new file mode 100644 index 0000000..f59b0b7 Binary files /dev/null and b/figures/layout/standard-layout-1.pdf differ diff --git a/figures/layout/standard-layout-2.pdf b/figures/layout/standard-layout-2.pdf new file mode 100644 index 0000000..e6a20a1 Binary files /dev/null and b/figures/layout/standard-layout-2.pdf differ diff --git a/figures/optimization/line-benchmark.png b/figures/optimization/line-benchmark.png new file mode 100644 index 0000000..00f7174 Binary files /dev/null and b/figures/optimization/line-benchmark.png differ diff --git a/figures/optimization/multisample.png b/figures/optimization/multisample.png new file mode 100644 index 0000000..5ea9ce3 Binary files /dev/null and b/figures/optimization/multisample.png differ diff --git a/figures/optimization/multithread.png b/figures/optimization/multithread.png new file mode 100644 index 0000000..7203ac2 Binary files /dev/null and b/figures/optimization/multithread.png differ diff --git a/figures/optimization/scatter-benchmark.png b/figures/optimization/scatter-benchmark.png new file mode 100644 index 0000000..7c2eeee Binary files /dev/null and b/figures/optimization/scatter-benchmark.png differ diff --git a/figures/optimization/scatters.png b/figures/optimization/scatters.png new file mode 100644 index 0000000..d16c1ad Binary files /dev/null and b/figures/optimization/scatters.png differ diff --git a/figures/optimization/self-cover.pdf b/figures/optimization/self-cover.pdf new file mode 100644 index 0000000..035ef27 Binary files /dev/null and b/figures/optimization/self-cover.pdf differ diff --git a/figures/optimization/transparency.pdf b/figures/optimization/transparency.pdf new file mode 100644 index 0000000..7361fc8 Binary files /dev/null and b/figures/optimization/transparency.pdf differ diff --git a/figures/ornaments/annotation-direct.pdf b/figures/ornaments/annotation-direct.pdf new file mode 100644 index 0000000..eaf3cab Binary files /dev/null and b/figures/ornaments/annotation-direct.pdf differ diff --git a/figures/ornaments/annotation-side.pdf b/figures/ornaments/annotation-side.pdf new file mode 100644 index 0000000..20aa9bf Binary files /dev/null and b/figures/ornaments/annotation-side.pdf differ diff --git a/figures/ornaments/annotation-zoom.pdf b/figures/ornaments/annotation-zoom.pdf new file mode 100644 index 0000000..09c2cd4 Binary files /dev/null and b/figures/ornaments/annotation-zoom.pdf differ diff --git a/figures/ornaments/bessel-functions.pdf b/figures/ornaments/bessel-functions.pdf new file mode 100644 index 0000000..f1d26a6 Binary files /dev/null and b/figures/ornaments/bessel-functions.pdf differ diff --git a/figures/ornaments/elegant-scatter.pdf b/figures/ornaments/elegant-scatter.pdf new file mode 100644 index 0000000..a6c5483 Binary files /dev/null and b/figures/ornaments/elegant-scatter.pdf differ diff --git a/figures/ornaments/latex-text-box.png b/figures/ornaments/latex-text-box.png new file mode 100644 index 0000000..5b6c22b Binary files /dev/null and b/figures/ornaments/latex-text-box.png differ diff --git a/figures/ornaments/legend-alternatives.pdf b/figures/ornaments/legend-alternatives.pdf new file mode 100644 index 0000000..846c108 Binary files /dev/null and b/figures/ornaments/legend-alternatives.pdf differ diff --git a/figures/ornaments/legend-regular.pdf b/figures/ornaments/legend-regular.pdf new file mode 100644 index 0000000..4c3dd57 Binary files /dev/null and b/figures/ornaments/legend-regular.pdf differ diff --git a/figures/ornaments/title-regular.pdf b/figures/ornaments/title-regular.pdf new file mode 100644 index 0000000..dcc0731 Binary files /dev/null and b/figures/ornaments/title-regular.pdf differ diff --git a/figures/reference/axes-adjustment.pdf b/figures/reference/axes-adjustment.pdf new file mode 100644 index 0000000..aebacd3 Binary files /dev/null and b/figures/reference/axes-adjustment.pdf differ diff --git a/figures/reference/collection.pdf b/figures/reference/collection.pdf new file mode 100644 index 0000000..97f5512 Binary files /dev/null and b/figures/reference/collection.pdf differ diff --git a/figures/reference/colormap-diverging.pdf b/figures/reference/colormap-diverging.pdf new file mode 100644 index 0000000..daa044c Binary files /dev/null and b/figures/reference/colormap-diverging.pdf differ diff --git a/figures/reference/colormap-qualitative.pdf b/figures/reference/colormap-qualitative.pdf new file mode 100644 index 0000000..cd18326 Binary files /dev/null and b/figures/reference/colormap-qualitative.pdf differ diff --git a/figures/reference/colormap-sequential-1.pdf b/figures/reference/colormap-sequential-1.pdf new file mode 100644 index 0000000..521ebeb Binary files /dev/null and b/figures/reference/colormap-sequential-1.pdf differ diff --git a/figures/reference/colormap-sequential-2.pdf b/figures/reference/colormap-sequential-2.pdf new file mode 100644 index 0000000..a2bd4e5 Binary files /dev/null and b/figures/reference/colormap-sequential-2.pdf differ diff --git a/figures/reference/colormap-uniform.pdf b/figures/reference/colormap-uniform.pdf new file mode 100644 index 0000000..e8cf338 Binary files /dev/null and b/figures/reference/colormap-uniform.pdf differ diff --git a/figures/reference/colorspec.pdf b/figures/reference/colorspec.pdf new file mode 100644 index 0000000..2a71463 Binary files /dev/null and b/figures/reference/colorspec.pdf differ diff --git a/figures/reference/font.pdf b/figures/reference/font.pdf new file mode 100644 index 0000000..842b741 Binary files /dev/null and b/figures/reference/font.pdf differ diff --git a/figures/reference/hatch.pdf b/figures/reference/hatch.pdf new file mode 100644 index 0000000..c245a72 Binary files /dev/null and b/figures/reference/hatch.pdf differ diff --git a/figures/reference/line.pdf b/figures/reference/line.pdf new file mode 100644 index 0000000..c528e99 Binary files /dev/null and b/figures/reference/line.pdf differ diff --git a/figures/reference/marker.pdf b/figures/reference/marker.pdf new file mode 100644 index 0000000..0427cfe Binary files /dev/null and b/figures/reference/marker.pdf differ diff --git a/figures/reference/scale.pdf b/figures/reference/scale.pdf new file mode 100644 index 0000000..1f2d684 Binary files /dev/null and b/figures/reference/scale.pdf differ diff --git a/figures/reference/text-alignment.pdf b/figures/reference/text-alignment.pdf new file mode 100644 index 0000000..92aa42a Binary files /dev/null and b/figures/reference/text-alignment.pdf differ diff --git a/figures/reference/tick-formatter.pdf b/figures/reference/tick-formatter.pdf new file mode 100644 index 0000000..f21ce28 Binary files /dev/null and b/figures/reference/tick-formatter.pdf differ diff --git a/figures/reference/tick-locator.pdf b/figures/reference/tick-locator.pdf new file mode 100644 index 0000000..ccbbf3f Binary files /dev/null and b/figures/reference/tick-locator.pdf differ diff --git a/figures/rules/rule-1.pdf b/figures/rules/rule-1.pdf new file mode 100644 index 0000000..1200ff5 Binary files /dev/null and b/figures/rules/rule-1.pdf differ diff --git a/figures/rules/rule-2.pdf b/figures/rules/rule-2.pdf new file mode 100644 index 0000000..00c2512 Binary files /dev/null and b/figures/rules/rule-2.pdf differ diff --git a/figures/rules/rule-3.pdf b/figures/rules/rule-3.pdf new file mode 100644 index 0000000..f6f25e9 Binary files /dev/null and b/figures/rules/rule-3.pdf differ diff --git a/figures/rules/rule-5.pdf b/figures/rules/rule-5.pdf new file mode 100644 index 0000000..12a5b59 Binary files /dev/null and b/figures/rules/rule-5.pdf differ diff --git a/figures/rules/rule-6.pdf b/figures/rules/rule-6.pdf new file mode 100644 index 0000000..1ec2ad5 Binary files /dev/null and b/figures/rules/rule-6.pdf differ diff --git a/figures/rules/rule-7.pdf b/figures/rules/rule-7.pdf new file mode 100644 index 0000000..d081600 Binary files /dev/null and b/figures/rules/rule-7.pdf differ diff --git a/figures/rules/rule-8.pdf b/figures/rules/rule-8.pdf new file mode 100644 index 0000000..d741517 Binary files /dev/null and b/figures/rules/rule-8.pdf differ diff --git a/figures/rules/rule-9.pdf b/figures/rules/rule-9.pdf new file mode 100644 index 0000000..97cf227 Binary files /dev/null and b/figures/rules/rule-9.pdf differ diff --git a/figures/scales-projections/geo-projections.pdf b/figures/scales-projections/geo-projections.pdf new file mode 100644 index 0000000..3916c67 Binary files /dev/null and b/figures/scales-projections/geo-projections.pdf differ diff --git a/figures/scales-projections/geo-projections.png b/figures/scales-projections/geo-projections.png new file mode 100644 index 0000000..5d55dd8 Binary files /dev/null and b/figures/scales-projections/geo-projections.png differ diff --git a/figures/scales-projections/polar-patterns.pdf b/figures/scales-projections/polar-patterns.pdf new file mode 100644 index 0000000..941c739 Binary files /dev/null and b/figures/scales-projections/polar-patterns.pdf differ diff --git a/figures/scales-projections/projection-3d-frame.pdf b/figures/scales-projections/projection-3d-frame.pdf new file mode 100644 index 0000000..0d79310 Binary files /dev/null and b/figures/scales-projections/projection-3d-frame.pdf differ diff --git a/figures/scales-projections/projection-polar-config.pdf b/figures/scales-projections/projection-polar-config.pdf new file mode 100644 index 0000000..8660283 Binary files /dev/null and b/figures/scales-projections/projection-polar-config.pdf differ diff --git a/figures/scales-projections/projection-polar-histogram.pdf b/figures/scales-projections/projection-polar-histogram.pdf new file mode 100644 index 0000000..ae4ac10 Binary files /dev/null and b/figures/scales-projections/projection-polar-histogram.pdf differ diff --git a/figures/scales-projections/scales-comparison.pdf b/figures/scales-projections/scales-comparison.pdf new file mode 100644 index 0000000..e7d37e0 Binary files /dev/null and b/figures/scales-projections/scales-comparison.pdf differ diff --git a/figures/scales-projections/scales-custom.pdf b/figures/scales-projections/scales-custom.pdf new file mode 100644 index 0000000..55dd628 Binary files /dev/null and b/figures/scales-projections/scales-custom.pdf differ diff --git a/figures/scales-projections/scales-log-log.pdf b/figures/scales-projections/scales-log-log.pdf new file mode 100644 index 0000000..8ab894a Binary files /dev/null and b/figures/scales-projections/scales-log-log.pdf differ diff --git a/figures/scales-projections/text-polar.pdf b/figures/scales-projections/text-polar.pdf new file mode 100644 index 0000000..f58cd45 Binary files /dev/null and b/figures/scales-projections/text-polar.pdf differ diff --git a/figures/showcases/VSOM.png b/figures/showcases/VSOM.png new file mode 100644 index 0000000..5445357 Binary files /dev/null and b/figures/showcases/VSOM.png differ diff --git a/figures/showcases/boots-stipple.png b/figures/showcases/boots-stipple.png new file mode 100644 index 0000000..dafc638 Binary files /dev/null and b/figures/showcases/boots-stipple.png differ diff --git a/figures/showcases/contour-dropshadow.png b/figures/showcases/contour-dropshadow.png new file mode 100644 index 0000000..97ba832 Binary files /dev/null and b/figures/showcases/contour-dropshadow.png differ diff --git a/figures/showcases/domain-coloring.pdf b/figures/showcases/domain-coloring.pdf new file mode 100644 index 0000000..a9245a7 Binary files /dev/null and b/figures/showcases/domain-coloring.pdf differ diff --git a/figures/showcases/domain-coloring.png b/figures/showcases/domain-coloring.png new file mode 100644 index 0000000..07782a9 Binary files /dev/null and b/figures/showcases/domain-coloring.png differ diff --git a/figures/showcases/elevation.png b/figures/showcases/elevation.png new file mode 100644 index 0000000..318587e Binary files /dev/null and b/figures/showcases/elevation.png differ diff --git a/figures/showcases/escher.pdf b/figures/showcases/escher.pdf new file mode 100644 index 0000000..12226aa Binary files /dev/null and b/figures/showcases/escher.pdf differ diff --git a/figures/showcases/heightmap.png b/figures/showcases/heightmap.png new file mode 100644 index 0000000..1304603 Binary files /dev/null and b/figures/showcases/heightmap.png differ diff --git a/figures/showcases/mandelbrot.png b/figures/showcases/mandelbrot.png new file mode 100644 index 0000000..687ab47 Binary files /dev/null and b/figures/showcases/mandelbrot.png differ diff --git a/figures/showcases/mosaic.pdf b/figures/showcases/mosaic.pdf new file mode 100644 index 0000000..f26c627 Binary files /dev/null and b/figures/showcases/mosaic.pdf differ diff --git a/figures/showcases/recursive-voronoi.pdf b/figures/showcases/recursive-voronoi.pdf new file mode 100644 index 0000000..a7c495b Binary files /dev/null and b/figures/showcases/recursive-voronoi.pdf differ diff --git a/figures/showcases/recursive-voronoi.png b/figures/showcases/recursive-voronoi.png new file mode 100644 index 0000000..b2a23f1 Binary files /dev/null and b/figures/showcases/recursive-voronoi.png differ diff --git a/figures/showcases/text-shadow.pdf b/figures/showcases/text-shadow.pdf new file mode 100644 index 0000000..9ff5a92 Binary files /dev/null and b/figures/showcases/text-shadow.pdf differ diff --git a/figures/showcases/text-shadow.png b/figures/showcases/text-shadow.png new file mode 100644 index 0000000..e143f18 Binary files /dev/null and b/figures/showcases/text-shadow.png differ diff --git a/figures/showcases/text-spiral.pdf b/figures/showcases/text-spiral.pdf new file mode 100644 index 0000000..46af7d9 Binary files /dev/null and b/figures/showcases/text-spiral.pdf differ diff --git a/figures/showcases/waterfall-3d.pdf b/figures/showcases/waterfall-3d.pdf new file mode 100644 index 0000000..5d548c3 Binary files /dev/null and b/figures/showcases/waterfall-3d.pdf differ diff --git a/figures/showcases/windmap.pdf b/figures/showcases/windmap.pdf new file mode 100644 index 0000000..c5414d3 Binary files /dev/null and b/figures/showcases/windmap.pdf differ diff --git a/figures/showcases/windmap.png b/figures/showcases/windmap.png new file mode 100644 index 0000000..4e5fe98 Binary files /dev/null and b/figures/showcases/windmap.png differ diff --git a/figures/threed/bunnies.pdf b/figures/threed/bunnies.pdf new file mode 100644 index 0000000..1d0c126 Binary files /dev/null and b/figures/threed/bunnies.pdf differ diff --git a/figures/threed/bunny-1.pdf b/figures/threed/bunny-1.pdf new file mode 100644 index 0000000..c7adaa3 Binary files /dev/null and b/figures/threed/bunny-1.pdf differ diff --git a/figures/threed/bunny-2.pdf b/figures/threed/bunny-2.pdf new file mode 100644 index 0000000..879f174 Binary files /dev/null and b/figures/threed/bunny-2.pdf differ diff --git a/figures/threed/bunny-3.pdf b/figures/threed/bunny-3.pdf new file mode 100644 index 0000000..ca47edb Binary files /dev/null and b/figures/threed/bunny-3.pdf differ diff --git a/figures/threed/bunny-4.pdf b/figures/threed/bunny-4.pdf new file mode 100644 index 0000000..7cb30f4 Binary files /dev/null and b/figures/threed/bunny-4.pdf differ diff --git a/figures/threed/bunny-5.pdf b/figures/threed/bunny-5.pdf new file mode 100644 index 0000000..e2c7b09 Binary files /dev/null and b/figures/threed/bunny-5.pdf differ diff --git a/figures/threed/bunny-6.pdf b/figures/threed/bunny-6.pdf new file mode 100644 index 0000000..402d7db Binary files /dev/null and b/figures/threed/bunny-6.pdf differ diff --git a/figures/threed/bunny-7.pdf b/figures/threed/bunny-7.pdf new file mode 100644 index 0000000..a659469 Binary files /dev/null and b/figures/threed/bunny-7.pdf differ diff --git a/figures/threed/bunny-8.pdf b/figures/threed/bunny-8.pdf new file mode 100644 index 0000000..8aeb14e Binary files /dev/null and b/figures/threed/bunny-8.pdf differ diff --git a/figures/threed/bunny.pdf b/figures/threed/bunny.pdf new file mode 100644 index 0000000..ab2c7db Binary files /dev/null and b/figures/threed/bunny.pdf differ diff --git a/figures/threed/projection.pdf b/figures/threed/projection.pdf new file mode 100644 index 0000000..18f2bca Binary files /dev/null and b/figures/threed/projection.pdf differ diff --git a/figures/threed/projection.png b/figures/threed/projection.png new file mode 100755 index 0000000..e5ea5e3 Binary files /dev/null and b/figures/threed/projection.png differ diff --git a/figures/threed/projection.svg b/figures/threed/projection.svg new file mode 100755 index 0000000..c1d2e59 --- /dev/null +++ b/figures/threed/projection.svg @@ -0,0 +1,2048 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Far clip plane + Near clip plane + Camera + Perspective projection (P) + + + + + + + + + + + + + + + + + + + + + + + Far clip plane + Near clip plane + Orthographic projection (O) + Camera + + + + + + + + + + + + + + + + + Top + Bottom + Left + Right + + + Top + Bottom + Left + Right + + + + diff --git a/figures/typography/projection-3d-gaussian.pdf b/figures/typography/projection-3d-gaussian.pdf new file mode 100644 index 0000000..89630b1 Binary files /dev/null and b/figures/typography/projection-3d-gaussian.pdf differ diff --git a/figures/typography/text-outline.pdf b/figures/typography/text-outline.pdf new file mode 100644 index 0000000..3de66c5 Binary files /dev/null and b/figures/typography/text-outline.pdf differ diff --git a/figures/typography/text-starwars.pdf b/figures/typography/text-starwars.pdf new file mode 100644 index 0000000..b45c56d Binary files /dev/null and b/figures/typography/text-starwars.pdf differ diff --git a/figures/typography/tick-labels-variation.pdf b/figures/typography/tick-labels-variation.pdf new file mode 100644 index 0000000..790a685 Binary files /dev/null and b/figures/typography/tick-labels-variation.pdf differ diff --git a/figures/typography/typography-font-stacks.pdf b/figures/typography/typography-font-stacks.pdf new file mode 100644 index 0000000..08e0643 Binary files /dev/null and b/figures/typography/typography-font-stacks.pdf differ diff --git a/figures/typography/typography-legibility.pdf b/figures/typography/typography-legibility.pdf new file mode 100644 index 0000000..977492a Binary files /dev/null and b/figures/typography/typography-legibility.pdf differ diff --git a/figures/typography/typography-math-cm.pdf b/figures/typography/typography-math-cm.pdf new file mode 100644 index 0000000..444d082 Binary files /dev/null and b/figures/typography/typography-math-cm.pdf differ diff --git a/figures/typography/typography-math-custom.pdf b/figures/typography/typography-math-custom.pdf new file mode 100644 index 0000000..cb30801 Binary files /dev/null and b/figures/typography/typography-math-custom.pdf differ diff --git a/figures/typography/typography-math-dejavusans.pdf b/figures/typography/typography-math-dejavusans.pdf new file mode 100644 index 0000000..166ff88 Binary files /dev/null and b/figures/typography/typography-math-dejavusans.pdf differ diff --git a/figures/typography/typography-math-dejavuserif.pdf b/figures/typography/typography-math-dejavuserif.pdf new file mode 100644 index 0000000..f7a7341 Binary files /dev/null and b/figures/typography/typography-math-dejavuserif.pdf differ diff --git a/figures/typography/typography-math-stacks.pdf b/figures/typography/typography-math-stacks.pdf new file mode 100644 index 0000000..5623b5a Binary files /dev/null and b/figures/typography/typography-math-stacks.pdf differ diff --git a/figures/typography/typography-math-stix.pdf b/figures/typography/typography-math-stix.pdf new file mode 100644 index 0000000..e703cb2 Binary files /dev/null and b/figures/typography/typography-math-stix.pdf differ diff --git a/figures/typography/typography-math-stixsans.pdf b/figures/typography/typography-math-stixsans.pdf new file mode 100644 index 0000000..d9f449b Binary files /dev/null and b/figures/typography/typography-math-stixsans.pdf differ diff --git a/figures/typography/typography-matters.pdf b/figures/typography/typography-matters.pdf new file mode 100644 index 0000000..75fd35f Binary files /dev/null and b/figures/typography/typography-matters.pdf differ diff --git a/figures/typography/typography-matters.png b/figures/typography/typography-matters.png new file mode 100644 index 0000000..ecbf9b6 Binary files /dev/null and b/figures/typography/typography-matters.png differ diff --git a/figures/typography/typography-text-path.pdf b/figures/typography/typography-text-path.pdf new file mode 100644 index 0000000..0b176ce Binary files /dev/null and b/figures/typography/typography-text-path.pdf differ diff --git a/figures/unsorted/coordinates.pdf b/figures/unsorted/coordinates.pdf new file mode 100644 index 0000000..1969a81 Binary files /dev/null and b/figures/unsorted/coordinates.pdf differ diff --git a/figures/unsorted/dyson-hatching.pdf b/figures/unsorted/dyson-hatching.pdf new file mode 100644 index 0000000..1c7c235 Binary files /dev/null and b/figures/unsorted/dyson-hatching.pdf differ diff --git a/figures/unsorted/earthquakes.pdf b/figures/unsorted/earthquakes.pdf new file mode 100644 index 0000000..1cb3754 Binary files /dev/null and b/figures/unsorted/earthquakes.pdf differ diff --git a/figures/unsorted/layout-weird.pdf b/figures/unsorted/layout-weird.pdf new file mode 100644 index 0000000..b328dc7 Binary files /dev/null and b/figures/unsorted/layout-weird.pdf differ diff --git a/figures/unsorted/metropolis.pdf b/figures/unsorted/metropolis.pdf new file mode 100644 index 0000000..cccf053 Binary files /dev/null and b/figures/unsorted/metropolis.pdf differ diff --git a/figures/unsorted/multisample.pdf b/figures/unsorted/multisample.pdf new file mode 100644 index 0000000..aedc3a5 Binary files /dev/null and b/figures/unsorted/multisample.pdf differ diff --git a/figures/unsorted/multisample.png b/figures/unsorted/multisample.png new file mode 100644 index 0000000..d6adfe6 Binary files /dev/null and b/figures/unsorted/multisample.png differ diff --git a/figures/unsorted/polar-better-frame.pdf b/figures/unsorted/polar-better-frame.pdf new file mode 100644 index 0000000..67daab0 Binary files /dev/null and b/figures/unsorted/polar-better-frame.pdf differ diff --git a/figures/unsorted/polar-patterns.pdf b/figures/unsorted/polar-patterns.pdf new file mode 100644 index 0000000..db104de Binary files /dev/null and b/figures/unsorted/polar-patterns.pdf differ diff --git a/figures/unsorted/polygon-clipping.pdf b/figures/unsorted/polygon-clipping.pdf new file mode 100644 index 0000000..464d964 Binary files /dev/null and b/figures/unsorted/polygon-clipping.pdf differ diff --git a/figures/unsorted/polygon-clipping.png b/figures/unsorted/polygon-clipping.png new file mode 100644 index 0000000..629fc2b Binary files /dev/null and b/figures/unsorted/polygon-clipping.png differ diff --git a/figures/unsorted/poster-layout.png b/figures/unsorted/poster-layout.png new file mode 100644 index 0000000..b1d7bfe Binary files /dev/null and b/figures/unsorted/poster-layout.png differ diff --git a/fonts/roboto/Apache License.txt b/fonts/roboto/Apache License.txt new file mode 100644 index 0000000..63d5302 --- /dev/null +++ b/fonts/roboto/Apache License.txt @@ -0,0 +1,203 @@ +Font data copyright Google 2012 + + 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. \ No newline at end of file diff --git a/fonts/roboto/Roboto-Black.ttf b/fonts/roboto/Roboto-Black.ttf new file mode 100644 index 0000000..689fe5c Binary files /dev/null and b/fonts/roboto/Roboto-Black.ttf differ diff --git a/fonts/roboto/Roboto-BlackItalic.ttf b/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..0b4e0ee Binary files /dev/null and b/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/fonts/roboto/Roboto-Bold.ttf b/fonts/roboto/Roboto-Bold.ttf new file mode 100644 index 0000000..d3f01ad Binary files /dev/null and b/fonts/roboto/Roboto-Bold.ttf differ diff --git a/fonts/roboto/Roboto-BoldItalic.ttf b/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..41cc1e7 Binary files /dev/null and b/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/fonts/roboto/Roboto-Italic.ttf b/fonts/roboto/Roboto-Italic.ttf new file mode 100644 index 0000000..6a1cee5 Binary files /dev/null and b/fonts/roboto/Roboto-Italic.ttf differ diff --git a/fonts/roboto/Roboto-Light.ttf b/fonts/roboto/Roboto-Light.ttf new file mode 100644 index 0000000..219063a Binary files /dev/null and b/fonts/roboto/Roboto-Light.ttf differ diff --git a/fonts/roboto/Roboto-LightItalic.ttf b/fonts/roboto/Roboto-LightItalic.ttf new file mode 100644 index 0000000..0e81e87 Binary files /dev/null and b/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/fonts/roboto/Roboto-Medium.ttf b/fonts/roboto/Roboto-Medium.ttf new file mode 100644 index 0000000..1a7f3b0 Binary files /dev/null and b/fonts/roboto/Roboto-Medium.ttf differ diff --git a/fonts/roboto/Roboto-MediumItalic.ttf b/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..0030295 Binary files /dev/null and b/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/fonts/roboto/Roboto-Regular.ttf b/fonts/roboto/Roboto-Regular.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/fonts/roboto/Roboto-Regular.ttf differ diff --git a/fonts/roboto/Roboto-Thin.ttf b/fonts/roboto/Roboto-Thin.ttf new file mode 100644 index 0000000..b74a4fd Binary files /dev/null and b/fonts/roboto/Roboto-Thin.ttf differ diff --git a/fonts/roboto/Roboto-ThinItalic.ttf b/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..dd0ddb8 Binary files /dev/null and b/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/fonts/roboto/RobotoCondensed-Bold.ttf b/fonts/roboto/RobotoCondensed-Bold.ttf new file mode 100644 index 0000000..48dd635 Binary files /dev/null and b/fonts/roboto/RobotoCondensed-Bold.ttf differ diff --git a/fonts/roboto/RobotoCondensed-BoldItalic.ttf b/fonts/roboto/RobotoCondensed-BoldItalic.ttf new file mode 100644 index 0000000..ad72864 Binary files /dev/null and b/fonts/roboto/RobotoCondensed-BoldItalic.ttf differ diff --git a/fonts/roboto/RobotoCondensed-Italic.ttf b/fonts/roboto/RobotoCondensed-Italic.ttf new file mode 100644 index 0000000..a232513 Binary files /dev/null and b/fonts/roboto/RobotoCondensed-Italic.ttf differ diff --git a/fonts/roboto/RobotoCondensed-Light.ttf b/fonts/roboto/RobotoCondensed-Light.ttf new file mode 100644 index 0000000..a6e368d Binary files /dev/null and b/fonts/roboto/RobotoCondensed-Light.ttf differ diff --git a/fonts/roboto/RobotoCondensed-LightItalic.ttf b/fonts/roboto/RobotoCondensed-LightItalic.ttf new file mode 100644 index 0000000..5b2b6ae Binary files /dev/null and b/fonts/roboto/RobotoCondensed-LightItalic.ttf differ diff --git a/fonts/roboto/RobotoCondensed-Regular.ttf b/fonts/roboto/RobotoCondensed-Regular.ttf new file mode 100644 index 0000000..65bf32a Binary files /dev/null and b/fonts/roboto/RobotoCondensed-Regular.ttf differ diff --git a/fonts/source-code-pro/SIL Open Font License.txt b/fonts/source-code-pro/SIL Open Font License.txt new file mode 100644 index 0000000..295975a --- /dev/null +++ b/fonts/source-code-pro/SIL Open Font License.txt @@ -0,0 +1,43 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/fonts/source-code-pro/SourceCodePro-Black.otf b/fonts/source-code-pro/SourceCodePro-Black.otf new file mode 100644 index 0000000..bbbbf64 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Black.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-BlackIt.otf b/fonts/source-code-pro/SourceCodePro-BlackIt.otf new file mode 100644 index 0000000..9835c17 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-BlackIt.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-Bold.otf b/fonts/source-code-pro/SourceCodePro-Bold.otf new file mode 100644 index 0000000..f4e576c Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Bold.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-BoldIt.otf b/fonts/source-code-pro/SourceCodePro-BoldIt.otf new file mode 100644 index 0000000..681a937 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-BoldIt.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-ExtraLight.otf b/fonts/source-code-pro/SourceCodePro-ExtraLight.otf new file mode 100644 index 0000000..de41f8b Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-ExtraLight.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-ExtraLightIt.otf b/fonts/source-code-pro/SourceCodePro-ExtraLightIt.otf new file mode 100644 index 0000000..49813d0 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-ExtraLightIt.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-It.otf b/fonts/source-code-pro/SourceCodePro-It.otf new file mode 100644 index 0000000..0ab9eea Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-It.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-Light.otf b/fonts/source-code-pro/SourceCodePro-Light.otf new file mode 100644 index 0000000..834a29b Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Light.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-LightIt.otf b/fonts/source-code-pro/SourceCodePro-LightIt.otf new file mode 100644 index 0000000..cd833d2 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-LightIt.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-Medium.otf b/fonts/source-code-pro/SourceCodePro-Medium.otf new file mode 100644 index 0000000..c5dddda Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Medium.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-MediumIt.otf b/fonts/source-code-pro/SourceCodePro-MediumIt.otf new file mode 100644 index 0000000..77cd5da Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-MediumIt.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-Regular.otf b/fonts/source-code-pro/SourceCodePro-Regular.otf new file mode 100644 index 0000000..4e3b9d0 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Regular.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-Semibold.otf b/fonts/source-code-pro/SourceCodePro-Semibold.otf new file mode 100644 index 0000000..2c4b5b4 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-Semibold.otf differ diff --git a/fonts/source-code-pro/SourceCodePro-SemiboldIt.otf b/fonts/source-code-pro/SourceCodePro-SemiboldIt.otf new file mode 100644 index 0000000..cb064e7 Binary files /dev/null and b/fonts/source-code-pro/SourceCodePro-SemiboldIt.otf differ diff --git a/fonts/source-sans-pro/.DS_Store b/fonts/source-sans-pro/.DS_Store new file mode 100644 index 0000000..ec2ca3b Binary files /dev/null and b/fonts/source-sans-pro/.DS_Store differ diff --git a/fonts/source-sans-pro/SIL Open Font License.txt b/fonts/source-sans-pro/SIL Open Font License.txt new file mode 100644 index 0000000..295975a --- /dev/null +++ b/fonts/source-sans-pro/SIL Open Font License.txt @@ -0,0 +1,43 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/fonts/source-sans-pro/SourceSansPro-Black.otf b/fonts/source-sans-pro/SourceSansPro-Black.otf new file mode 100644 index 0000000..0c25f3d Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-Black.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-BlackIt.otf b/fonts/source-sans-pro/SourceSansPro-BlackIt.otf new file mode 100644 index 0000000..da3504c Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-BlackIt.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-Bold.otf b/fonts/source-sans-pro/SourceSansPro-Bold.otf new file mode 100644 index 0000000..98dbee7 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-Bold.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-BoldIt.otf b/fonts/source-sans-pro/SourceSansPro-BoldIt.otf new file mode 100644 index 0000000..6600c86 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-BoldIt.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-ExtraLight.otf b/fonts/source-sans-pro/SourceSansPro-ExtraLight.otf new file mode 100644 index 0000000..f885ce7 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-ExtraLight.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-ExtraLightIt.otf b/fonts/source-sans-pro/SourceSansPro-ExtraLightIt.otf new file mode 100644 index 0000000..f932024 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-ExtraLightIt.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-It.otf b/fonts/source-sans-pro/SourceSansPro-It.otf new file mode 100644 index 0000000..2d627d9 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-It.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-Light.otf b/fonts/source-sans-pro/SourceSansPro-Light.otf new file mode 100644 index 0000000..159979f Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-Light.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-LightIt.otf b/fonts/source-sans-pro/SourceSansPro-LightIt.otf new file mode 100644 index 0000000..e3d49b5 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-LightIt.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-Regular.otf b/fonts/source-sans-pro/SourceSansPro-Regular.otf new file mode 100644 index 0000000..bdcfb27 Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-Regular.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-Semibold.otf b/fonts/source-sans-pro/SourceSansPro-Semibold.otf new file mode 100644 index 0000000..fffdbaf Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-Semibold.otf differ diff --git a/fonts/source-sans-pro/SourceSansPro-SemiboldIt.otf b/fonts/source-sans-pro/SourceSansPro-SemiboldIt.otf new file mode 100644 index 0000000..e90515b Binary files /dev/null and b/fonts/source-sans-pro/SourceSansPro-SemiboldIt.otf differ diff --git a/fonts/source-serif-pro/SIL Open Font License.txt b/fonts/source-serif-pro/SIL Open Font License.txt new file mode 100644 index 0000000..a53e4fb --- /dev/null +++ b/fonts/source-serif-pro/SIL Open Font License.txt @@ -0,0 +1,43 @@ +Copyright 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/fonts/source-serif-pro/SourceSerifPro-Black.otf b/fonts/source-serif-pro/SourceSerifPro-Black.otf new file mode 100644 index 0000000..11bb8ea Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-Black.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-BlackIt.otf b/fonts/source-serif-pro/SourceSerifPro-BlackIt.otf new file mode 100644 index 0000000..a5cee34 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-BlackIt.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-Bold.otf b/fonts/source-serif-pro/SourceSerifPro-Bold.otf new file mode 100644 index 0000000..034d9d4 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-Bold.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-BoldIt.otf b/fonts/source-serif-pro/SourceSerifPro-BoldIt.otf new file mode 100644 index 0000000..8127399 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-BoldIt.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-ExtraLight.otf b/fonts/source-serif-pro/SourceSerifPro-ExtraLight.otf new file mode 100644 index 0000000..213a3cd Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-ExtraLight.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-ExtraLightIt.otf b/fonts/source-serif-pro/SourceSerifPro-ExtraLightIt.otf new file mode 100644 index 0000000..4da4cbb Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-ExtraLightIt.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-It.otf b/fonts/source-serif-pro/SourceSerifPro-It.otf new file mode 100644 index 0000000..a0dec7e Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-It.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-Light.otf b/fonts/source-serif-pro/SourceSerifPro-Light.otf new file mode 100644 index 0000000..9ac9950 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-Light.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-LightIt.otf b/fonts/source-serif-pro/SourceSerifPro-LightIt.otf new file mode 100644 index 0000000..d7aaa1f Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-LightIt.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-Regular.otf b/fonts/source-serif-pro/SourceSerifPro-Regular.otf new file mode 100644 index 0000000..dde2fd4 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-Regular.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-Semibold.otf b/fonts/source-serif-pro/SourceSerifPro-Semibold.otf new file mode 100644 index 0000000..8bb0390 Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-Semibold.otf differ diff --git a/fonts/source-serif-pro/SourceSerifPro-SemiboldIt.otf b/fonts/source-serif-pro/SourceSerifPro-SemiboldIt.otf new file mode 100644 index 0000000..0586ddd Binary files /dev/null and b/fonts/source-serif-pro/SourceSerifPro-SemiboldIt.otf differ diff --git a/images/book.png b/images/book.png new file mode 100644 index 0000000..74983e2 Binary files /dev/null and b/images/book.png differ diff --git a/rst/.DS_Store b/rst/.DS_Store new file mode 100644 index 0000000..eba399d Binary files /dev/null and b/rst/.DS_Store differ diff --git a/rst/00-acknowledgments.rst b/rst/00-acknowledgments.rst new file mode 100644 index 0000000..286054a --- /dev/null +++ b/rst/00-acknowledgments.rst @@ -0,0 +1,108 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. include:: common.rst +.. _chap-acknowledgments: + + +Acknowledgments +=============== + +I would like to thank all my sponsors & supporters for their great +contributions, comments and questions that help to improve the book +and the associated code. It's really great to have these people on +your side you when you're writing a book. + +Sponsors (in no specific order) +------------------------------- + +* Scott Lasley (`@selasley `_) +* Steven Armour (`@GProtoZeroW `_) +* Daniel Gomez (`@dangom `_) +* Nathan Howell (`@neh `_) +* Aymen-lng (`@Aymen-lng `_) +* Paniterka (`@paniterka `_) +* t. m. k. (`@teehemkay `_) +* Alberto Mario Ceballos-Arroyo (`@alceballosa `_) +* Panagiotis Simakis (`@sp1thas `_) +* Anton Emelyanov (`@emertonyc `_) +* Florent Langenfeld (`@FloLangenfeld `_) +* Andrei Berceanu (`@berceanu `_) +* Robin Mattheussen (`@romatthe `_) +* Robert Crim (`@ottbot `_) +* Jonas Bernoulli (`@tarsius `_) + + +Supporters (in no specific order) +--------------------------------- + +* Andrew (`@ahuang11 `_) +* Alister Burt (`@alisterburt `_) +* `@argapost `_ +* `@artinus `_ +* Avihay Bar (`@avihaybar `_) +* Georgios Bakirtzis (`@bakirtzisg `_) +* `@BCMarquez `_ +* Behrooz Bashokooh (`@BehroozBashokooh `_) +* Bill Little (`@bjlittle `_) +* Benjamin Morgan (`@bjmorgan `_) +* Sébastien Boisgérault (`@boisgera `_) +* Brandon Rohrer (`@brohrer `_) +* Brian Hamilton (`@bsxfun `_) +* Christopher Anderson (`@chrisLanderson `_) +* Chris Short (`@ChristopherShort `_) +* `@ciscostud `_ +* Chris Morgan (`@cmorgan `_) +* Onuralp (`@cx0 `_) +* Jochen Schröder (`@cycomanic `_) +* David Ignacio Cortes (`@davidcortesortuno `_) +* Danylo Malyuta (`@dmalyuta `_) +* `@earlev4 `_ +* Eitan Lees (`@eitanlees `_) +* Javier González Monge (`@Enterprixe `_) +* Folgert Karsdorp (`@fbkarsdorp `_) +* Federico Vaggi (`@FedericoV `_) +* Francisco (`@fpozon `_) +* Giovanni d'Ario (`@gdario `_) +* Georg Wille (`@georgwille `_) +* Luciano Gerber (`@gerberl `_) +* Jeff Borisch (`@horshacktest `_) +* `@imsalte `_ +* `@jafvert `_ +* Jonathan Whitmore (`@jbwhit `_) +* Julien Hillairet (`@jhillairet `_) +* Joseph Szymborski (`@jszym `_) +* `@ltosti `_ +* Matthieu Leroy (`@m-leroy `_) +* Markus Degen (`@MarkusDegen `_) +* Michael Dick (`@midick `_) +* Niru Maheswaranathan (`@nirum `_) +* `@ofrighil `_ +* `@oszaar `_ +* `@paulgoulain `_ +* Paul Nakroshis (`@paulnakroshis `_) +* `@qsandi `_ +* Rik Huygen (`@rhuygen `_) +* Rich Teague (`@richteague `_) +* Rocco Meli (`@RMeli `_) +* `@s7oneghos7 `_ +* Sébastien Le Maguer (`@seblemaguer `_) +* Andrew Slabko (`@slabko `_) +* wonjun (`@sleepyeye `_) +* Serge Toropov (`@sombr `_) +* `@samdani-1729 `_ +* Shen Zhou (`@szsdk `_) +* Thomas Lentali (`@tlentali `_) +* VO-PY (`@VO-PY `_) +* Xavier Olive (`@xoolive `_) +* Izaak "Zaak" Beekman (`@zbeekman `_) +* zguo (`@zguoch `_) +* Zhang Zhou (`@zznature `_) + + +A special thanks to **Eitan Lees** who reviewed several chapters to +correct my poor English. All remaining typos are my own. I also would +like to thank **Sébastien Boisgérault** who helped me a lot with the +automated build process (well, actually, he setup everything). diff --git a/rst/00-dedication.rst b/rst/00-dedication.rst new file mode 100644 index 0000000..eee531f --- /dev/null +++ b/rst/00-dedication.rst @@ -0,0 +1,11 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- + +.. epigraph:: + + In memory of **John D. Hunter** (1968 — 2012), creator of the Matplotlib + library & **Maxim Shemanarev** (1966 — 2013), creator of the antigrain + geometry library. Two brilliant minds that are dearly missed. diff --git a/rst/00-introduction.rst b/rst/00-introduction.rst new file mode 100644 index 0000000..b63ad6d --- /dev/null +++ b/rst/00-introduction.rst @@ -0,0 +1,171 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. include:: common.rst +.. _chap-introduction: + +Introduction +============ + +The Python scientific visualisation landscape is huge (see figure +:ref:`fig-landscape`). It is composed of a myriad of tools, ranging from the +most versatile and widely used down to the more specialised and +confidential. Some of these tools are community based while others are developed +by companies. Some are made specifically for the web, others are for the desktop only, +some deal with 3D and large data, while others target flawless 2D rendering. + +.. figure:: introduction/visualization-landscape.pdf + :width: 100% + + Python scientific visualisation landscape in 2018 (not exhaustive). Adapted + from the original idea of `Jake Vanderplas `_. + **Sources:** `github.com/rougier/python-visualization-landscape + `__ + :label:`fig-landscape` + +Facing such a large choice, it may be thus difficult to find the package that +best suit your needs, simply because you may not even be aware that this or that +package exists. To help you in your choice, you can start by asking yourself a +few questions: + +* Do you target desktop or web rendering? +* Do you need complex 3D rendering? +* Do you need publication quality? +* Do you have very large data? +* Is there an active community? +* Are there documentation and tutorials? + +.. figure:: introduction/matplotlib-timeline.pdf + :width: 100% + + Matplotlib has been originally written by John D. Hunter and the first + public version was released in 2003. Michael Droettboom was nominated as + matplotlib's lead developer shortly before John Hunter's death in August + 2012, joined in 2014 by Thomas Caswell who is now (2021) the + lead-developer. The latest version is 3.4 (at the time of writing), and is + Python 3 only while the 2.2 version is a long term support version + compatible with Python 2 and Python 3. **Sources:** + :source:`introduction/matplotlib-timeline.py` :label:`fig-matplotlib-timeline`. + +Depending on your answers, you may be able to decide which package to use and +to invest some time learning it. For example, if you need interactive +visualization in the browser with seamless integration with jupyter, bokeh_ +might be an answer. If you have very large data and needs 3D on the desktop, +vispy_ or mayavi_ might be an option. If you're interested in a very intuitive +tool to rapidly build beautiful figures, then seaborn_ and altair_ are your friends. +However, if you're working in geosciences, then you cannot overlook cartopy_, etc. I cannot list +them all and I'm sure that between the writing of this chapter and the actual +publication of the book, new visualization libraries will have been created. A +good source of information is the pyviz_ website (Python tools for data +visualization) that offers a lot of pointers and has an up-to-date list of +active packages (as opposed to dormant). + +.. figure:: introduction/black-hole.jpg + :width: 100% + + The supermassive black hole at the core of supergiant elliptical galaxy + Messier 87, with a mass ~7 billion times the Sun's, as depicted in the + first image released by the Event Horizon Telescope (10 April 2019). + **Source:** `Wikipedia `__ + :label:`fig-black-hole` + +In this landscape, Matplotlib has a very special place. It was originally +created by `John D. Hunter `_ in +2003 in order to visualize electrocorticography data. Here is the `official +announcement `__ posted on the Python mailing list on May 23, 2003 [#]_. + +.. [#] Many thanks to Anthony Lee for pointing me to this archive. + +:: + + + Matplotlib + + Matplotlib is a pure python plotting package for python and + pygtk. My goal is to make high quality, publication quality + plotting easy in python, with a syntax familiar to matlab + users. matplotlib is young, and several things need to be + done for this goal is achieved. But it works well enough to + make nice, simple plots. + + Requirements + + python 2.2, GTK2, pygtk-1.99.x, and Numeric. + + Download + + See the homepage - nitace.bsd.uchicago.edu:8080/matplotlib + + Here are some of the things that matplotlib tries to do well + + * Allow easy navigation of large data sets. Right click on + figure window to bring up navigation tool bar for pan and + zoom of x and y axes. This requires a wheel mouse. Place + the wheel mouse over the navigation buttons and scroll away. + * Handle very large data sets efficiently by making use of + Numeric clipping. I have used matplotlib in an EEG + plotting application with 128 channels and several + minutes of data sampled at 400Hz, eg, plotting matrices + with dimensions 120,000 x 128. + * Choose tick marks and labels intelligently + * make easy things easy (subplots, linestyles, colors) + * make hard things possible (OO interface for full control) + + Matplotlib is a class library that can be used to make plots + in pygtk applications. But there is a matlab functional + compatibility interface that you can get with, eg:: + + from matplotlib.matlab import plot, subplot, show, gca + + Example scripts and screenshots available at + http://nitace.bsd.uchicago.edu:8080/matplotlib + + John Hunter + + +The initial goal was to replace the popular Matlab graphics engine and to +support different platforms, to have high quality raster and vector output, to +provide support for mathematical expressions and to work interactively from the +shell. The first official release was made in 2003 (see figure +:ref:`fig-matplotlib-timeline`) and more than 15 years later, the initial goals +remains the same even though they have been further developed and +polished. Today, the Matplotlib library is a *de facto* standard for Python +scientific visualization. It has, for example, been used to display the first +ever photography of a black hole (see figure :ref:`fig-black-hole`) and to +illustrate the existence of `gravitational waves +`__. Matplotlib is +both a versatile and powerful library that allows you to design very high +quality figures, suitable for scientific publishing. It offers both a simple and +intuitive interface (`pyplot`) as well as an object oriented architecture that +allows you to tweak anything within a figure. Note that, it can also be used as +a regular graphic library in order to design non-scientific figures, as we'll +see throughout this book. For example, the Matplotlib timeline figure (see +figure :ref:`fig-matplotlib-timeline`) is simply made of a line with markers and +some styled annotations. + +This book is organized into 4 parts. The first part considers the fundamental +principles of the Matplotlib library. This includes reviewing the different +parts that constitute a figure, the different coordinate systems, the available +scales and projections, and we'll also introduce a few concepts related to +typography and colors. The second part is dedicated to the actual design of a +figure. After introducing some simple rules for generating better figures, we'll +then go on to explain the Matplotlib defaults and styling system before diving +on into figure layout organization. We'll then explore the different types of +plot available and see how a figure can be ornamented with different +elements. The third part is dedicated to more advanced concepts, namely 3D +figures, optimization, animation and toolkits. Lastly, the fourth and final +part is a collection of showcases and their analysis. + + +.. --- Links ------------------------------------------------------------------ +.. _bokeh: https://bokeh.pydata.org +.. _altair: https://altair-viz.github.io/ +.. _seaborn: http://seaborn.pydata.org/ +.. _vispy: http://vispy.org/ +.. _mayavi: https://docs.enthought.com/mayavi/mayavi/ +.. _cartopy: https://scitools.org.uk/cartopy/docs/latest/ +.. _pyviz: https://pyviz.org/ +.. ---------------------------------------------------------------------------- + diff --git a/rst/00-preface.rst b/rst/00-preface.rst new file mode 100644 index 0000000..7e86f59 --- /dev/null +++ b/rst/00-preface.rst @@ -0,0 +1,112 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. include:: common.rst +.. _chap-preface: + +Preface +======== + +About the author +---------------- + +Nicolas P. Rougier is a full-time researcher in computational cognitive +neuroscience, located in Bordeaux, France. He's doing his research at Inria +(the French institute for computer science) and the Institute of +Neurodegenerative Diseases where he investigates decision making, learning and +cognition using computational models of the brain and distributed, numerical +and adaptive computing, a.k.a. artificial neural networks and machine +learning. His research aims to irrigate the fields of philosophy with regard to +the mind-body problem, medicine to account for the normal and pathological +functioning of the brain and the digital sciences to offer alternative +computing paradigms. Beside neuroscience and philosophy, he's also interested +in open and reproducible science (he has co-founded ReScience C with Konrad +Hinsen and ReScience X with Etienne Roesch), scientific visualization (he +created glumpy, co-created VisPy), Science outreach (e.g. The Conversation) and +computer graphics (especially digital typography). + +Nicolas P. Rougier has been using Python for more than 20 years and Matplotlib +for more than 15 years for modeling in neuroscience, machine learning and for +advanced visualization. Nicolas P. Rougier is the author of several online +resources and tutorials and he's teaching Python, NumPy and scientific +visualisation at the University of Bordeaux as well as at various conferences +and schools worldwide. + + +About this book +--------------- + +This open access book has been written in reStructuredText_ converted to LaTeX +using docutils and exported to Portable Document Format using XeLaTeX. Sources +are available at `github.com/rougier/python-scientific-visualisation +`_ + + +How to contribute +----------------- + +If you want to contribute to this book, you can: + +* Review chapters & suggest improvements +* Report issues & correct my English +* Star the project on GitHub & buy the printed book + + +Prerequisites +------------- + +This book is not a Python beginner guide and you should have an intermediate +level in Python and ideally a beginner level in NumPy. If this is not the case, +have a look at the bibliography for a curated list of resources. + +Conventions +----------- + +We will use usual naming conventions. If not stated explicitly, each script +should import NumPy, SciPy and Matplotlib as: + +.. code:: Python + + import scipy + import numpy as np + import matplotlib.pyplot as plt + + +We'll use up-to-date versions (at the date of writing, June 2019) of the +different packages: + +.. code:: Python + + >>> import sys; print(sys.version) + 3.7.4 (default, Jul 9 2019, 18:13:23) + [Clang 10.0.1 (clang-1001.0.46.4)] + >>> import numpy; print(numpy.__version__) + 1.16.4 + >>> import scipy; print(scipy.__version__) + 1.3.0 + >>> import matplotlib; print(matplotlib.__version__) + 3.1.0 + + +License +------- + +This volume is is licensed under a Creative Commons Attribution Non Commercial +Share Alike 4.0 International License, which permits use, sharing, adaptation, +distribution and reproduction in any medium or format, as long as you give +appropriate credit to the original author(s) and the source, provide a link to +the Creative Commons license, and indicate if changes were made. You may not +use the material for commercial purposes. If you remix, transform, or build +upon the material, you must distribute your contributions under the same +license as the original. To learn more, visit `creativecommons.org`_. + +Unless stated otherwise, all the figures are licensed under a Creative Commons +Attribution 4.0 International License and all the code is licensed under a BSD +2-clause license. + +.. --- Links ------------------------------------------------------------------ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _creativecommons.org: https://creativecommons.org/ +.. ---------------------------------------------------------------------------- diff --git a/rst/anatomy.rst b/rst/anatomy.rst new file mode 100644 index 0000000..2fe63d2 --- /dev/null +++ b/rst/anatomy.rst @@ -0,0 +1,483 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-anatomy: + +Anatomy of a figure +=================== + +A matplotlib figure is composed of a hierarchy of elements that, when put +together, forms the actual figure as shown on figure :ref:`fig-anatomy`. Most +of the time, those elements are not created explicitly by the user but derived +from the processing of the various plot commands. Let us consider for example +the most simple matplotlib script we can write: + +.. code:: python + + plt.plot(range(10)) + plt.show() + +In order to display the result, matplotlib needs to create most of the elements +shown on figure :ref:`fig-anatomy`. The exact list depends on your default +settings (see chapter chap-defaults_), but the bare minimum is the creation of +a Figure_ that is the top level container for all the plot elements, an Axes_ +that contains most of the figure elements and of course your actual plot, a +line in this case. The possibility to not specify everything might be +convenient but in the meantime, it limits your choices because missing elements +are created automatically, using default values. For example, in the previous +example, you have no control of the initial figure size since is has been +chosen implicitly during creation. If you want to change the figure size or +the axes aspect, you need to be more explicit: + +.. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(aspect=1) + ax.plot(range(10)) + plt.show() + +.. figure:: anatomy/anatomy.pdf + :width: 100% + + A matplotlib figure is composed of a hierarchy of several elements that, + when put together, forms the actual figure (sources: :source:`anatomy/anatomy.py`). + :label:`fig-anatomy` + +In many cases, this can be further compacted using the subplots_ method. + +.. code:: python + + fig, ax = plt.subplots(figsize=(6,6), + subplot_kw={"aspect"=1}) + ax.plot(range(10)) + plt.show() + + +Elements +-------- + +You may have noticed in the previous example that the plot_ command is attached +to `ax` instead of `plt`. The use of `plt.plot` is actually a way to tell +matplotlib that we want to plot on the current axes, that is, the last axes +that has been created, implicitly or explicitly. No need to remind that +*explicit is better than implicit* as explained in the The Zen of Python, by +Tim Peters (`import this`). When you have choice, it is thus preferable to +specify exactly what you want to do. Consequently, it is important to know what +are the different elements of a figure. + +Figure_: + The most important element of a figure is the figure itself. It is created + when you call the `figure` method and we've already seen you can specify its + size but you can also specify a background color (`facecolor`) as well as a + title (`suptitle`). It is important to know that the background color won't + be used when you save the figure because the savefig_ function has also a + `facecolor` argument (that is white by default) that will override your + figure background color. If you don't want any background you can specify + `transparent=True` when you save the figure. + +Axes_: + This is the second most important element that corresponds to the actual area + where your data will be rendered. It is also called a subplot. You can have + have one to many axes per figure and each is usually surrounded by four edges + (left, top, right and bottom) that are called **spines**. Each of these + spines can be decorated with major and minor **ticks** (that can point inward + or outward), **tick labels** and a **label**. By default, matplotlib + decorates only the left and bottom spines. + +Axis_: + The decorated spines are called axis. The horizontal one is the **xaxis** and + the vertical one is the **yaxis**. Each of them are made of a spine, major and + minor ticks, major and minor ticks labels and an axis label. + +Spines_: + Spines are the lines connecting the **axis** tick marks and noting the + boundaries of the data area. They can be placed at arbitrary positions and + may be visible or invisible. + +Artist_: + Everything on the figure, including Figure, Axes, and Axis objects, is an + artist. This includes Text objects, Line2D objects, collection objects, Patch + objects. When the figure is rendered, all of the artists are drawn to the + canvas. A given artist can only be in one Axes. + + +Graphic primitives +------------------ + +A plot, independently of its nature, is made of patches, lines and +texts. Patches can be very small (e.g. markers) or very large (e.g. bars) and +have a range of shapes (circles, rectangles, polygons, etc.). Lines can be very +small and thin (e.g. ticks) or very thick (e.g. hatches). Text can use any font +available on your system and can also use a latex engine to render maths. + +.. figure:: anatomy/bold-ticklabel.pdf + :width: 100% + + All the graphic primitives (i.e. artists) can be accessed and modified. In + the figure above, we modified the boldness of the X axis tick labels + (sources: :source:`anatomy/bold-ticklabel.py`). + +Each of these graphic primitives have also a lot of other properties such as +color (facecolor and edgecolor), transparency (from 0 to 1), patterns +(e.g. dashes), styles (e.g. cap styles), special effects (e.g. shadows or +outline), antialiased (True or False), etc. Most of the time, you do not +manipulate these primitives directly. Instead, you call methods that build a +rendering using a collection of such primitives. For example, when you add a +new `Axes` to a figure, matplotlib will build a collection of line segments for +the spines and the ticks and will also add a collection of labels for the tick +labels and the axis labels. Even though this is totally transparent for you, +you can still access those elements individually if necessary. For example, to +make the X axis tick to be bold, we would write: + +.. code:: Python + + fig, ax = plt.subplots(figsize=(5,2)) + for label in ax.get_xaxis().get_ticklabels(): + label.set_fontweight("bold") + plt.show() + +One important property of any primitive is the `zorder` property that indicates +the virtual depth of the primitives as shown on figure :ref:`fig-zorder`. This +zorder value is used to sort the primitives from the lowest to highest before +rendering them. This allows to control what is behind what. Most artists +possess a default zorder value such that things are rendered properly. For +example, the spines, the ticks and the tick label are generally *behind* +your actual plot. + +.. figure:: anatomy/zorder.pdf + :width: 50% + + Default rendering order of different elements and graphic primitives. The + rendering order is from bottom to top. Note that some methods will override + these default to position themselves properly (sources: :source:`anatomy/zorder.py`). + :label:`fig-zorder` + + +Backends +-------- + +A backend is the combination of a renderer that is responsible for the actual +drawing and an optional user interface that allows to interact with a +figure. Until now, we've been using the default renderer and interface +resulting in a window being shown when the `plt.show()` method was called. To +know what is your default backend, you can type: + +.. code:: Python + + import matplotlib + print(matplotlib.get_backend()) + +In my case, the default backend is `MacOSX` but yours may be different. If you +want to test for an alternative backend, you can type: + +.. code:: Python + + import matplotlib + matplotlib.use("xxx") + +If you replace `xxx` with a renderer from table :ref:`table-renderers` below, +you'll end up with a non-interactive figure, i.e. a figure that cannot be shown +on screen but only saved on disk. + +.. table:: Available matplotlib renderers. + :label:`table-renderers` + :align: left + + ========= ================== ============================================= + Renderer Type Filetype + ========= ================== ============================================= + Agg raster Portable Network Graphic (PNG) + PS vector Postscript (PS) + PDF vector Portable Document Format (PDF) + SVG vector Scalable Vector Graphics (SVG) + Cairo raster / vector PNG / PDF / SVG + ========= ================== ============================================= + +.. table:: Available matplotlib interfaces. + :label:`table-interfaces` + :align: left + + ========== =============== =============================================== + Interface Renderer Dependencies + ========== =============== =============================================== + GTK3 Agg or Cairo PyGObject_ & Pycairo_ + Qt4 Agg PyQt4_ + Qt5 Agg PyQt5_ + Tk Agg TkInter_ + Wx Agg wxPython_ + MacOSX — OSX (obviously) + Web Agg Browser + ========== =============== =============================================== + +The canonical renderer is Agg which uses the `Anti-Grain Geometry C++ library +`__ to make a raster image of the figure (see figure +:ref:`fig-raster-vector` to see the different between raster and vector). Note +that even if you choose a raster renderer, you can still save the figure in a +vector format and vice-versa. + +.. figure:: anatomy/raster-vector.pdf + :width: 75% + + Zooming effect for raster graphics and vector graphics (sources: + :source:`anatomy/raster-vector.py`). :label:`fig-raster-vector` + +If you want to have some interaction with your figure, you have to combine one +of the available interface (see table :ref:`table-interfaces`) with a +renderer. For example `GTK3Cairo` or `WebAgg`. + + + +For example, to have a rendering in a browser, you can write: + +.. code:: Python + + import matplotlib + matplotlib.use('webagg') + import matplotlib.pyplot as plt + plt.show() + +.. warning:: + **Warning.** The `use` function must be called before importing `pyplot`. + +Once you've chosen an interactive backend, you can decide to produce a figure +in interactive mode (figure is updated after each matplotlib command): + +.. code:: Python + + plt.ion() # Interactive mode on + plt.plot([1,2,3]) # Plot is shown + plt.xlabel("X Axis") # Label is updated + plt.ioff() # Interactive mode off + +If you want to know more on backends, you can have a look at the `introductory +tutorial `__ +on the matplotlib website. + + +An interesting backend under OSX and `iterm2 `__ +terminal is the `imgcat `__ +backend that allows to render a figure directly inside the terminal, +emulating a kind of jupyter notebook as shown on figure +:ref:`figure-imgcat` + +.. figure:: anatomy/imgcat.png + :width: 100% + + Matplotlib imgcat backend + :label:`figure-imgcat` + (sources: :source:`anatomy/imgcat.py`). + +.. code:: python + + import numpy as np + import matplotlib + matplotlib.use("module://imgcat") + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=(8,4), frameon=False) + ax = plt.subplot(2,1,1) + X = np.linspace(0, 4*2*np.pi, 500) + line, = ax.plot(X, np.cos(X)) + ax = plt.subplot(2,1,2) + X = np.linspace(0, 4*2*np.pi, 500) + line, = ax.plot(X, np.sin(X)) + plt.tight_layout() + plt.show() + +For other terminals, you might need to use the `sixel `__ backend that may work with xterm (not tested). + + +Dimensions & resolution +----------------------- + +In the first example of this chapter, we specified a figure size of `(6,6)` +that corresponds to a size of 6 inches (width) by 6 inches (height) using a +default dpi (dots per inch) of 100. When displayed on a screen, dots +corresponds to pixels and we can immediately deduce that the figure size +(i.e. window size without the toolbar) will be exactly 600×600 pixels. Same is +true if you save the figure in a bitmap format such as png (Portable Network +Graphics): + +.. code:: python + + fig = plt.figure(figsize=(6,6)) + plt.savefig("output.png") + +If we use the `identify` command from the ImageMagick_ graphical suite to +enquiry about the produced image, we get: + +.. code:: bash + + $ identify output.png + Image: output.png + Format: PNG (Portable Network Graphics) + Mime type: image/png + Class: DirectClass + Geometry: 600x600+0+0 + Resolution: 39.37x39.37 + Print size: 15.24x15.24 + Units: PixelsPerCentimeter + Colorspace: sRGB + ... + +This confirms that the image geometry is 600×600 while the resolution is 39.37 +ppc (pixels per centimeter) which corresponds to 39.37*2.54 ≈ 100 dpi (dots per +inch). If you were to include this image inside a document while keeping the +same dpi, you would need to set the size of the image to 15.24cm by 15.24cm. If +you reduce the size of the image in your document, let's say by a factor of 3, +this will mechanically increase the figure dpi to 300 in this specific +case. For a scientific article, publishers will generally request figures dpi +to be between 300 and 600. To get things right, it is thus good to know what +will be the physical dimension of your figure once inserted into your document. + +.. figure:: anatomy/figure-dpi.png + :width: 100% + + A text rendered in matplotlib and saved using different dpi (50,100,300 & + 600) (sources: :source:`anatomy/figure-dpi.py`). :label:`figure-dpi` + +For a more concrete example, let us consider this book whose format is A5 +(148×210 millimeters). Right and left margins are 20 millimeters each and +images are usually displayed using the full text width. This means the physical +width of an image is exactly 108 millimeters, or approximately 4.25 inches. If +we were to use the recommended 600 dpi, we would end up with a width of 2550 +pixels which might be beyond screen resolution and thus not very convenient. +Instead, we can use the default matplotlib dpi (100) when we display the figure +on the screen and only when we save it, we use a different and higher dpi: + +.. code:: Python + + def figure(dpi): + fig = plt.figure(figsize=(4.25,.2)) + ax = plt.subplot(1,1,1) + text = "Text rendered at 10pt using %d dpi" % dpi + ax.text(0.5, 0.5, text, ha="center", va="center", + fontname="Source Serif Pro", + fontsize=10, fontweight="light") + plt.savefig("figure-dpi-%03d.png" % dpi, dpi=dpi) + + figure(50), figure(100), figure(300), figure(600) + +Figure :ref:`figure-dpi` shows the output for the different dpi. Only the 600 +dpi output is acceptable. Note that when it is possible, it is preferable to +save the result in PDF (Portable Document Format) because it is a vector format +that will adapt flawlessly to any resolution. However, even if you save your +figure in a vector format, you still need to indicate a dpi for figure elements +that cannot be vectorized (e.g .images). + +Finally, you may have noticed that the font size on figure :ref:`figure-dpi` +appears to be the same as the font size of the text you're currently +reading. This is not by accident since this Latex document uses a font size of +10 points and the matplotlib figure also uses a font size of 10 points. But +what is a point exactly? In Latex, a point (pt) corresponds to 1/72.27 inches +while in matplotlib it corresponds to 1/72 inches. + +To help you visualize the exact dimension of your figure, it is +possible to add a ruler to a figure such that it displays current +figure dimension as shown on figure :ref:`figure-ruler`. If you +manually resize the figure, you'll see that the actual dimension of +the figure changes while if you only change the dpi, the size will +remain the same. Usage is really simple: + +.. code:: python + + import ruler + import numpy as np + import matplotlb.pyplot as plt + + fig,ax = plt.subplots() + ruler = ruler.Ruler(fig) + plt.show() + + + +.. figure:: anatomy/ruler.pdf + :width: 100% + + Interactive ruler :label:`figure-ruler` + (:source:`anatomy/ruler.py`). + + +Exercise +-------- + +It's now time to try to make some simple exercises gathering all the concepts +we've seen so far (including finding the relevant documentation). + +**Exercise 1** Try to produce a figure with a given (and exact) pixel size +(e.g. 512x512 pixels). How would you specify the size and save the figure? + +.. figure:: anatomy/pixel-font.png + :width: 100% + + Pixel font text using exact image size :label:`figure-pixel-font` + (:source:`anatomy/pixel-font.py`). + + +**Exercise 2** +The goal is to make the figure :ref:`figure-inch-cm` that shows a dual axis, one +in inches and one in centimeters. The difficulty is that we want the +centimeters and inched to be physically correct when printed. This requires +some simple computations for finding the right size and some trials and errors +to make the actual figure. Don't pay too much attention to all the details, the +essential part is to get the size right. + +.. figure:: anatomy/inch-cm.pdf + :width: 100% + + Inches/centimeter conversion :label:`figure-inch-cm` + (**solution**: :source:`anatomy/inch-cm.py`). + + +**Exercise 3** + +Here we'll try to reproduce the figure :ref:`figure-zorder-plots`. If you look +at the figure, you'll realize that each curve is partially covering other +curves and it is thus important to set a proper zorder for each curve such that +the rendering will be independent of drawing order. For the actual curves, you can start from the following code: + +.. code:: Python + + def curve(): + n = np.random.randint(1,5) + centers = np.random.normal(0.0,1.0,n) + widths = np.random.uniform(5.0,50.0,n) + widths = 10*widths/widths.sum() + scales = np.random.uniform(0.1,1.0,n) + scales /= scales.sum() + X = np.zeros(500) + x = np.linspace(-3,3,len(X)) + for center, width, scale in zip(centers, widths, scales): + X = X + scale*np.exp(- (x-center)*(x-center)*width) + return X + +.. figure:: anatomy/zorder-plots.pdf + :width: 100% + + Multiple plots partially covering each other :label:`figure-zorder-plots` + (**solution**: :source:`anatomy/zorder-plots.py`). + + +.. --- Links ------------------------------------------------------------------ +.. _Figure: https://matplotlib.org/api/figure_api.html +.. _Axes: https://matplotlib.org/api/axes_api.html +.. _plot: https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html +.. _ImageMagick: https://imagemagick.org +.. _savefig: https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html +.. _subplots: https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html +.. _Axis: https://matplotlib.org/api/axis_api.html +.. _Axes: https://matplotlib.org/api/axes_api.html +.. _Ticks: https://matplotlib.org/api/ticker_api.html +.. _Spines: https://matplotlib.org/api/spines_api.html +.. _Artist: https://matplotlib.org/tutorials/intermediate/artists.html +.. _pygobject: https://pygobject.readthedocs.io/en/latest/ +.. _pycairo: https://pycairo.readthedocs.io/en/latest/ +.. _pyqt4: https://www.riverbankcomputing.com/software/pyqt/intro +.. _pyqt5: https://www.riverbankcomputing.com/software/pyqt/intro +.. _tkinter: https://wiki.python.org/moin/TkInter +.. _wxpython: https://www.wxpython.org/ +.. ---------------------------------------------------------------------------- + + diff --git a/rst/animation.rst b/rst/animation.rst new file mode 100644 index 0000000..ceeb79f --- /dev/null +++ b/rst/animation.rst @@ -0,0 +1,403 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-animation: + +Animation +========= + +Animation with matplotlib can be created very easily using the +`animation framework +`_. Let's start +with a very simple animation. We want to make an animation where the +sine and cosine functions are plotted progressively on the screen. To +do that, we need first to tell matplotlib we want to make an animation +and then, we have to specify what we want to draw at each frame. One +common mistake is to re-draw everything at each frame that makes the +whole process very slow. Instead, we can only update what is necessary +because we know that (in our case) a lot of things won't change from +one frame to the other. For a line plot, we'll use the `set_data` +method to update the drawing and matplotlib will take care of of the +rest. + +.. code:: python + + import numpy as np + import matplotlib.pyplot as plt + import matplotlib.animation as animation + + fig = plt.figure(figsize=(7,2), dpi=100) + ax = plt.subplot() + + X = np.linspace(-np.pi, np.pi, 256, endpoint=True) + C, S = np.cos(X), np.sin(X) + line1, = ax.plot(X, C, marker="o", markevery=[-1], + markeredgecolor="white") + line2, = ax.plot(X, S, marker="o", markevery=[-1], + markeredgecolor="white") + + def update(frame): + line1.set_data(X[:frame], C[:frame]) + line2.set_data(X[:frame], S[:frame]) + + ani = animation.FuncAnimation(fig, update, interval=10) + plt.show() + +Notice the end point marker that move with the animation. The reason +is that we specify a single marker at the end (`markevery=[-1]`) such +that each time we set new data, marker is automatically updated and +moves with the animation. See figure +:ref:`figure-animation-sine-cosine`. + +.. figure:: animation/sine-cosine.pdf + :width: 100% + + Snapshots of the sine cosine animation + :label:`figure-animation-sine-cosine` + (sources: :source:`chapter-12/sine-cosine.py`). + + +If we now want to save this animation, matplotlib can create a movie +file but options are rather scarce. A better solution is to use an +external library such as `FFMpeg `__ which is +available on most systems. Once installed, we can use the dedicated +`FFMpegWriter `__ as shown below: + +.. code:: python + + writer = animation.FFMpegWriter(fps=30) + anim = animation.FuncAnimation(fig, update, interval=10, frames=len(X)) + anim.save("sine-cosine.mp4", writer=writer, dpi=100) + +You may have noticed that when we save the movie, the animation does +not start immediately because there is actually a delay that +corresponds to the movie creation. For sine and cosine, the delay is +rather short and it is not really a problem. However, for long and +complex animations, this delay can become quite significant and it +becomes necessary to track its progress. So let's add some information +using the `tqdm `__ library. + +.. code:: python + + from tqdm.autonotebook import tqdm + bar = tqdm(total=len(X)) + anim.save("../data/sine-cosine.mp4", writer=writer, dpi=300, + progress_callback = lambda i, n: bar.update(1)) + bar.close() + +Creation time remains the same, but at least now, we can check how +slow or fast it is. Here is some output of the animation: + +.. image:: animation/sine-cosine-frame-032.pdf + :width: 100% +.. image:: animation/sine-cosine-frame-128.pdf + :width: 100% +.. figure:: animation/sine-cosine-frame-255.pdf + :width: 100% + + Still from the sine/cosine animation + (sources :source:`animation/sine-cosine.py`). + +Make it rain +------------ + +A very simple rain effect can be obtained by having small growing +rings randomly positioned over a figure. Of course, they won't grow +forever since ripples are supposed to damp with time. To simulate this +phenomenon, we can use an increasingly transparent color as the ring +is growing, up to the point where it is no more visible. At this +point, we remove the ring and create a new one. First step is to +create a blank figure. + +.. code:: python + + fig = plt.figure(figsize=(6,6), facecolor='white', dpi=50) + ax = fig.add_axes([0,0,1,1], frameon=False, aspect=1) + ax.set_xlim(0,1), ax.set_xticks([]) + ax.set_ylim(0,1), ax.set_yticks([]) + +Then we create an empty scatter plot but we take care of settings +linewidth (0.5) and facecolors ("None") that will apply to any new +data. + +.. code:: python + + scatter = ax.scatter([], [], s=[], lw=0.5, + edgecolors=[], facecolors="None") + +Next, we need to create several rings. For this, we can use the +scatter plot object that is generally used to visualize points cloud, +but we can also use it to draw rings by specifying we don't have a +facecolor. We also have to take care of initial size and color for +each ring such that we have all sizes between a minimum and a maximum +size. In addition, we need to make sure the largest ring is almost +transparent. + +.. code:: python + + n = 50 + R = np.zeros(n, dtype=[ ("position", float, (2,)), + ("size", float, (1,)), + ("color", float, (4,)) ]) + R["position"] = np.random.uniform(0, 1, (n,2)) + R["size"] = np.linspace(0, 1, n).reshape(n,1) + R["color"][:,3] = np.linspace(0, 1, n) + +Now, we need to write the update function for our animation. We know +that at each time step each ring should grow and become more +transparent while the largest ring should be totally transparent and +thus removed. Of course, we won't actually remove the largest ring but +re-use it to set a new ring at a new random position, with nominal +size and color. Hence, we keep the number of rings constant. + +.. code:: python + + def rain_update(frame): + global R, scatter + + # Transparency of each ring is increased + R["color"][:,3] = np.maximum(0, R["color"][:,3] - 1/len(R)) + + # Size of each rings is increased + R["size"] += 1/len(R) + + # Reset last ring + i = frame % len(R) + R["position"][i] = np.random.uniform(0, 1, 2) + R["size"][i] = 0 + R["color"][i,3] = 1 + + # Update scatter object accordingly + scatter.set_edgecolors(R["color"]) + scatter.set_sizes(1000*R["size"].ravel()) + scatter.set_offsets(R["position"]) + +Last step is to tell matplotlib to use this function as an update +function for the animation and display the result (or save it as a +movie): + +.. code:: python + + animation = animation.FuncAnimation(fig, rain_update, + interval=10, frames=200) + + +.. figure:: animation/rain.pdf + :width: 100% + + Still from the rain animation (sources :source:`animation/rain.py`). + + +Visualizing earthquakes on Earth +-------------------------------- + +We'll now use the rain animation to visualize earthquakes on the +planet from the last 30 days. The USGS Earthquake Hazards Program is +part of the National Earthquake Hazards Reduction Program (NEHRP) and +provides several data on their website. Those data are sorted +according to earthquakes magnitude, ranging from significant only down +to all earthquakes, major or minor. You would be surprised by the +number of minor earthquakes happening every hour on the planet. Since +this would represent too much data for us, we'll stick to earthquakes +with magnitude > 4.5. At the time of writing, this already represent +more than 300 earthquakes in the last 30 days. + +First step is to read and convert data. We'll use the urllib library +that allows us to open and read remote data. Data on the website use +the CSV format whose content is given by the first line:: + + time,latitude,longitude,depth,mag,magType,nst,gap,dmin,rms,... + 2015-08-17T13:49:17.320Z,37.8365,-122.2321667,4.82,4.01,mw,... + 2015-08-15T07:47:06.640Z,-10.9045,163.8766,6.35,6.6,mwp,... + + +We are only interested in latitude, longitude and magnitude and +consequently, we won't parse the time of event. + +.. code:: python + + import urllib + import numpy as np + + # -> http://earthquake.usgs.gov/earthquakes/feed/v1.0/csv.php + feed = "http://earthquake.usgs.gov/" \ + + "earthquakes/feed/v1.0/summary/" + + # Magnitude > 4.5 + url = urllib.request.urlopen(feed + "4.5_month.csv") + + # Magnitude > 2.5 + # url = urllib.request.urlopen(feed + "2.5_month.csv") + + # Magnitude > 1.0 + # url = urllib.request.urlopen(feed + "1.0_month.csv") + + # Reading and storage of data + data = url.read().split(b'\n')[+1:-1] + E = np.zeros(len(data), dtype=[('position', float, (2,)), + ('magnitude', float, (1,))]) + + for i in range(len(data)): + row = data[i].split(b',') + E['position'][i] = float(row[2]),float(row[1]) + E['magnitude'][i] = float(row[4]) + + +We need to draw the earth to show precisely where the earthquake +center is and to translate latitude/longitude in some coordinates +matplotlib can handle. Fortunately, there is the `cartopy +`_ library that is not +so simple to install but really easy to use. + +First step is to define a projection to draw the earth onto a screen. +There exists many different projections but we'll use the `Equirectangular +projection `_ which +is rather standard for non-specialists like me. + +.. code:: python + + import cartopy.crs as ccrs + import matplotlib.pyplot as plt + + fig = plt.figure(figsize=(10,5)) + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.coastlines() + + plt.show() + +.. figure:: animation/platecarree.pdf + :width: 100% + + Equirectangular projection + :label:`figure-animation-equirectangular` + (sources: :source:`animation/platecarree.py`). + +We can now adapt the rain animation to display eartquakes. To do that, +we just need to add a `transform` to the scatter plot such that +coordinates will be automatically transformed (by cartopy). + +.. code:: python + + scatter = ax.scatter([], [], transform=ccrs.PlateCarree()) + + + +.. figure:: animation/earthquakes-frame-50.pdf + :width: 100% + + Earthquakes still (July 23, 2021 at 11am CET) + :label:`figure-animation-earthquakes + (sources: :source:`animation/earthquakes.py`). + + +Scenarized animation +-------------------- + +We've seen the basic principles of animation. It is now time to define +a more elaborated scenario for our animation. To do that, we'll play +with fluid simulation because it's fun. In +:source:`animation/fluid.py`, you'll find an implementation of stable +fluid simulation written by `Gregory Johnson +`__ based on the paper of +`Joe Stam +`__. + +I've modified the original script and written an `inflow` method that +define a source at a given position (angle). At each frame, we want to +define active sources such that the overall animation displays a +sequence of emitting sources. + +In the scenario below, I define arbitrarily a rotating sequence of +sources to maximize blending in the center but you could also imagine +synchronizing this animation with some music for example. + +.. code:: python + + import numpy as np + from fluid import Fluid, inflow + from scipy.special import erf + import matplotlib.pyplot as plt + import matplotlib.animation as animation + + shape = 256, 256 + duration = 500 + fluid = Fluid(shape, 'dye') + inflows = [inflow(fluid, x) + for x in np.linspace(-np.pi, np.pi, 8, endpoint=False)] + + # Animation setup + fig = plt.figure(figsize=(5, 5), dpi=100) + ax = fig.add_axes([0, 0, 1, 1], frameon=False) + ax.set_xlim(0, 1); ax.set_xticks([]); + ax.set_ylim(0, 1); ax.set_yticks([]); + im = ax.imshow( np.zeros(shape), extent=[0, 1, 0, 1], + vmin=0, vmax=1, origin="lower", + interpolation='bicubic', cmap=plt.cm.RdYlBu) + + # Animation scenario + scenario = [] + for i in range(8): + scenario.extend( [[i]]*20 ) + scenario.extend([[0,2,4,6]]*30) + scenario.extend([[1,3,5,7]]*30) + + # Animation update + def update(frame): + frame = frame % len(scenario) + for i in scenario[frame]: + inflow_velocity, inflow_dye = inflows[i] + fluid.velocity += inflow_velocity + fluid.dye += inflow_dye + divergence, curl, pressure = fluid.step() + Z = curl + Z = (erf(Z * 2) + 1) / 4 + + im.set_data(Z) + im.set_clim(vmin=Z.min(), vmax=Z.max()) + + anim = animation.FuncAnimation(fig, update, interval=10, frames=duration) + plt.show() + + +.. figure:: animation/fluid-animation.png + :width: 100% + + Fluid simulation + :label:`figure-fluid-animation` + (sources: :source:`animation/fluid-animation.py`). + +Note that in the update function, I took care of updating the limits +of the colormap. This is necessary because the displayed image is +dynamic and the minimum and maximum values may vary from one frame ot +the other. If you don't do that, you might have some flickering. + +You can also have much more elaborated scenario such as in the +following example which is a `remake +`__ of an animation +originally designed by dark horse analytics. + + +.. figure:: animation/less-is-more.png + :width: 100% + + Less is more :label:`figure-less-is-more` + (sources: :source:`animation/less-is-more.py`). + + +Exercise +-------- + +The goal of this exercise is to create an animation showing how +`Lissajous curves `__ +are generated. Figure :ref:`figure-lissajous` shows a still from the +animation. Make sure to try to copy the exact style. + +.. figure:: animation/lissajous.pdf + :width: 100% + + Lissajous curves :label:`figure-lissajous` + (sources: :source:`animation/lissajous.py`). + + diff --git a/rst/beyond.rst b/rst/beyond.rst new file mode 100644 index 0000000..eb447ff --- /dev/null +++ b/rst/beyond.rst @@ -0,0 +1,325 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-beyond: + + +Graphic library +=============== + +Beyond its usage for scientific visualization, matplotlib is also *a +comprehensive library for creating static, animated, and interactive +visualizations in Python* as written on the `website +`__. Said differently, matplotlib is a +graphic library that can be used for virtually any purpose, even +though the performance may vary greatly from one application to the +other, depending on the complexity of the rendering. Such versatility +can be explained by the presence of a number of low-level objects, +that allow to produce virtually any rendering, and supported by a +number of standard operations as shown on figure +:ref:`figure-polygon-clipping` + +.. figure:: beyond/polygon-clipping.pdf + :width: 100% + + Polygon clipping + :label:`figure-polygon-clipping` + (sources: :source:`beyond/polygon-clipping.py`). + +Here is a first example showing the capability of matplotlib in terms +of polygon and clipping. As you can see on figure +:ref:`figure-polygon-clipping`, clipping allows to render any +combination of two polygons. + +Such clipping can be used as well in a regular figure to make some +interesting effect as shown on figure :ref:`figure-interactive-loupe`. + +.. figure:: beyond/interactive-loupe.pdf + :width: 100% + + Polygon clipping + :label:`figure-interactive-loupe` + (sources: :source:`beyond/interactive-loupe.py`). + + +Matplotlib dungeon +------------------ + +If you ever played role playing game (especially Dungeons & Dragons), +you may have encountered "Dyson hatching" as shown on figure +:ref:`figure-matplotlib-dungeon` (look at the outside border of +the walls). This kind of hatching is quite unique and immediately +identifies the plan as some kind of dungeon. This hatching has been +originally designed by `Dyson Logos `__ who +was kind enough to explain `how he draws it (by hand) +`__. Question is then, how to reproduce it using matplotlib? + +It's actually not too difficult but it's not totally straightforward +either because we have to take care of several details to get a nice +result. The starting point is a random two-dimensional distribution +where points needs to be not too close to each other. To achieve such +result, can use Bridson’s Algorithm which is a very popular method to +produce such blue noise sample point distributions that guarantees +that no two points are closer than a given distance. If you observe +figure :ref:`figure-bluenoise`, you can see the algorithm makes a real +difference when compared to either a pure uniform distribution or a +regular grid with some normal jitters. + +.. figure:: beyond/bluenoise.pdf + :width: 100% + + Uniform distribution, jittered grid and blue noise distribution + :label:`figure-bluenoise` + (sources: :source:`beyond/bluenoise.py`). + +From this blue noise distribution, we can insert hatch pattern at each +location with a random orientation. A hatch pattern is a set of +n parallel lines with some noise: + +.. code:: python + + def hatch(n=4, theta=None): + theta = theta or np.random.uniform(0,np.pi) + P = np.zeros((n,2,2)) + X = np.linspace(-0.5,+0.5,n,endpoint=True) + P[:,0,1] = -0.5 + np.random.normal(0, 0.05, n) + P[:,1,1] = +0.5 + np.random.normal(0, 0.05, n) + P[:,1,0] = X + np.random.normal(0, 0.025, n) + P[:,0,0] = X + np.random.normal(0, 0.025, n) + c, s = np.cos(theta), np.sin(theta) + Z = np.array([[ c, s],[-s, c]]) + return P @ Z.T + +You can see the result in the center of figure +:ref:`figure-dyson-hatching`. This starts to looks like Dyson hatching +but it is not yet satisfactory because hatches cover each others. To +avoid that, we need to clip hatches using the corresponding voronoi +cells. The easiest way to do that is to use the `shapely library +`__ that provides methods to +compute intersection with generic polygons. You can see the result on +the right part of figure :ref:`figure-dyson-hatching` and it looks +much nicer (in my honest opinion). + +.. figure:: beyond/dyson-hatching.pdf + :width: 100% + + Dyson hatching + :label:`figure-dyson-hatching` + (sources: :source:`beyond/dyson-hatching.py`). + +We are not done yet. Next part is to generate a dungeon. If you search +for dungeon generator on the internet, you'll find many generators, +from the most basic ones to the much more complex. In my case, I +simply designed the dungeon using inkscape and I extracted the +coordinates of the walls from the svg file: + +.. code:: python + + Walls = np.array([ + [1,1],[5,1],[5,3],[8,3],[8,2],[11,2],[11,5],[10,5], + [10,6],[12,6],[12,8],[13,8],[13,10],[11,10],[11,12], + [2,12],[2,10],[1,10],[1,7],[4,7],[4,10],[3,10], + [3,11],[10,11],[10,10],[9,10],[9,8],[11,8],[11,7], + [9,7],[9,5],[8,5],[8,4],[5,4],[5,6],[1,6], [1,1]]) + walls = Polygon(Walls, closed=True, zorder=10, + facecolor="white", edgecolor="None", + lw=3, joinstyle="round") + ax.add_patch(walls) + +The next step is to restrict the hatching to the vicinity of the +walls. Since hatches corresponds to our initial point distribution, it +is only a matter of filtering hatches whose centers are sufficiently +close to any wall. It thus only requires to compute the distance of a +point to a line segment. At this point, we do not care it the the +hatch is inside or outside the dungeon since the internal hatches are +hidden by the interior of the dungeon (see zorder above). I proceeded +by adding dotted squares inside corridors using a collection of +vertical and horizontal lines as well as some random "rocks" which are +actually collection of small ellipses. Last, I added a nice title +using an old looking font. I used `Morris Roman +`__ font by Dieter +Steffmann. + +The result looks nice but it can be further improved. For example, we +could introduce some noise in walls to suggest manual drawing, we +could improve rocks by adding noise, etc. Matplotlib provides +everything that is needed and the only limit is your imagination. If +you're curious on chat could be achieved, make sure to have a look at +`one page dungeon `__ by +Oleg Dolya or the `Fantasy map generator +`__ by Martin O'Leary. + +.. figure:: beyond/dungeon.pdf + :width: 100% + + Matplotlib dungeon + :label:`figure-matplotlib-dungeon` + (sources: :source:`beyond/dungeon.py`). + + +Tiny bot simulator +------------------ + +Using the same approach, it is possible to design a tiny bot simulator +as shown on figure :ref:`figure-tinybot` which is a snapshot of the +simulation. To design this simulator, I started by splitting the figure +using gridspec as follows: + +.. code:: python + + fig = plt.figure(figsize=(10,5), frameon=False) + G = GridSpec(8, 2, width_ratios=(1,2)) + ax = plt.subplot( G[:,0], aspect=1, frameon=False) + ... + + for i in range(8): # 8 sensors + sax = plt.subplot( G[i,1]) + ... + +`ax` is the axes on the left showing the maze and the bot while sax +are axes to display sensors value on the right. Maze walls are +rendered using a line collection while the robot is rendered using a +circle (for the body), a line (for the "head", i.e. a line indicating +direction) and a line collection for the sensors. The overall +simulation is a matplotlib animation where the update function is +responsible for updating the bot position and sensors values. + +.. figure:: beyond/tinybot.pdf + :width: 100% + + Tiny bot simulator + :label:`figure-tinybot` + (sources: :source:`beyond/tinybot.py`). + +There is no real difficulty but the computation of sensors & wall +intersection which can be vectorized using Numpy to make it fast: + +.. code:: python + + def line_intersect(p1, p2, P3, P4): + + p1 = np.atleast_2d(p1) + p2 = np.atleast_2d(p2) + P3 = np.atleast_2d(P3) + P4 = np.atleast_2d(P4) + + x1, y1 = p1[:,0], p1[:,1] + x2, y2 = p2[:,0], p2[:,1] + X3, Y3 = P3[:,0], P3[:,1] + X4, Y4 = P4[:,0], P4[:,1] + + D = (Y4-Y3)*(x2-x1) - (X4-X3)*(y2-y1) + + # Colinearity test + C = (D != 0) + UA = ((X4-X3)*(y1-Y3) - (Y4-Y3)*(x1-X3)) + UA = np.divide(UA, D, where=C) + UB = ((x2-x1)*(y1-Y3) - (y2-y1)*(x1-X3)) + UB = np.divide(UB, D, where=C) + + # Test if intersections are inside each segment + C = C * (UA > 0) * (UA < 1) * (UB > 0) * (UB < 1) + + X = np.where(C, x1 + UA*(x2-x1), np.inf) + Y = np.where(C, y1 + UA*(y2-y1), np.inf) + return np.stack([X,Y],axis=1) + +This simulator could be easily extended with a camera showing the +environment in 3D using the renderer I introduced in chapter +`chap-3D`_. In the end, it is possible to write a complete simulator +in a few lines of Python. The goal is of course not to replace a real +simulator, but it comes handy to rapidly prototype an idea which is +exactly what I did to study decision making using the reservoir +computing paradigm. + + +Real example +------------ + +When put together, these graphical primitives allow to draw quite +elaborated figures as shown on figure +:ref:`figure-basal-ganglia`. This figure comes the article `A +graphical, scalable and intuitive method for the placement and the +connection of biological cells +`_ that introduces a graphical +method originating from the computer graphics domain that is used for +the arbitrary placement of cells over a two-dimensional manifold. The +figure represents a schematic slice of the basal ganglia (striatum and +globus pallidus) that has been split in four different subfigures: + +* **Subfigure A** is made of a bitmap image showing an arbitrary + density of neurons. I used a bitmap image because it is not yet + possible to render such arbitrary gradient using + matplotlib. However, I also read the corresponding SVG image to + extract the paths delimiting each structure and plot them on the + figure. + +* **Subfigure B** is represents the actual method for positioning an + arbitrary number of neurons enforcing the density represented by the + color gradient. To represent them, I used a simple scatter plot and + colored some neurons according to their input/output status. + +* **Subfigure C** represents an interpolation of the activity of the + neurons and has been made using a 2D histogram made. To do that, I + simply built a big array representing the whole image and I set the + activity around the neuron using a disc of constant radius. This is + only a matter of translating the 2d coordinates of the neuron to a + 2D index inside the image array. I then used an `imshow` to show the + result and I drew over the frontiers of each structure. This kind of + rendering helps to see the overall activity inside the structure. + +* **Subfigure D** is probably the most complex because it involved the + computation of Voronoi cells and their intersection with the border + of the structure. Once again, the shapely library is incredibly + useful to achieve such result. Once the cell have been computed, it + is only a matter of painting them with a colormap according to their + activity. For efficiency, this is made using a poly collection. + +This is actually quite a complex example, but once you've written the +code, it can be adapted to any input (the SVG file in this case) such +that your final result is fully automated. Of course, the amount of +work this represents should be balanced with your actual needs. If you +need the figure only once, it is probably not worth the effort if you +can do it manually. + + +.. figure:: beyond/basal-ganglia.pdf + :width: 100% + + A schematic view of a slice of the basal + ganglia. Sources availables from the `spatial-computation + `_ repository on + GitHub. :label:`figure-basal-ganglia` + + + +Exercises +--------- + +**Stamp like effect** `Fancy boxes `_ offer several style that can be used to achieve different effect as shown on figure :ref:`figure-mona-lisa-stamp`. The goal is to achieve the same effect. + +.. figure:: beyond/stamp.png + :width: 75% + + Mona Lisa stamp + :label:`figure-mona-lisa-stamp` + (sources: :source:`beyond/stamp.py`). + + +**Radial Maze** Try to redo the figure :ref:`figure-radial-maze` which +displays a radial maze (that is used quite often in neuroscience to +study mouse or rat behavior) and a simulated path representing a rat +exploring the maze (this has been generated by recording the +(computer) mouse movements). The color of each block represents the +occupancy rate, that is, the number of recorded point inside the +block. + +.. figure:: beyond/radial-maze.pdf + :width: 100% + + Radial maze + :label:`figure-radial-maze` + (sources: :source:`beyond/radial-maze.py`). + diff --git a/rst/chapter.tex b/rst/chapter.tex new file mode 100644 index 0000000..f03c6f3 --- /dev/null +++ b/rst/chapter.tex @@ -0,0 +1 @@ +$body diff --git a/rst/cheatsheets.rst b/rst/cheatsheets.rst new file mode 100644 index 0000000..5356783 --- /dev/null +++ b/rst/cheatsheets.rst @@ -0,0 +1,23 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-cheatsheets: + +Quick References +================ + +Here are cheatsheets and handouts I've designed for Matplotlib. They +are also available in A4 format at `github.com/matplotlib/cheatsheets +`__. + + +:raw-latex:`\pdfinclude{190mm}{../figures/cheatsheets/cheatsheets-1.pdf}` +:raw-latex:`\pdfinclude{190mm}{../figures/cheatsheets/cheatsheets-2.pdf}` +:raw-latex:`\pdfinclude{190mm}{../figures/cheatsheets/cheatsheets-3.pdf}` +:raw-latex:`\pdfinclude{190mm}{../figures/cheatsheets/cheatsheets-4.pdf}` +:raw-latex:`\pdfinclude{190mm}{../figures/cheatsheets/cheatsheets-5.pdf}` +:raw-latex:`\pdfinclude{200mm}{../figures/cheatsheets/handout-beginner.pdf}` +:raw-latex:`\pdfinclude{200mm}{../figures/cheatsheets/handout-intermediate.pdf}` +:raw-latex:`\pdfinclude{200mm}{../figures/cheatsheets/handout-tips.pdf}` diff --git a/rst/colors.rst b/rst/colors.rst new file mode 100644 index 0000000..69838ba --- /dev/null +++ b/rst/colors.rst @@ -0,0 +1,245 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-colors: + +A primer on colors +================== + +Color is a `highly complex topic +`_ and a whole book would +probably not be enough to explain each and every aspect. This is the +reason why I won't try to explain everything here, the other reason +being that I'm simply not knowledgeable enough on the topic. There are +nonetheless a few things that are good to know, for example +how are colors represented on a computer. To represent a color on a +computer, we use (most of the time) the notion of a `color model +`_ (how do we represent a +color) and a `color space +`_ (what colors can be +represented). There exists several color models (RGB, HSV, HLS, CMYK, +CIEXYZ, CIELAB, etc.) and several color spaces (Adobe RGB, sRGB, +Colormatch RGB, etc.) such that you can access the same color space +using different color models. The standard for computers (since 1996) +is the `sRGB color space `_ where +the `s` stands for standard. This color space uses an additive color +model based on the RGB model. This means that to obtain a given color, +you need to mix different amounts of red, green, and blue light. When +these amounts are all zero, you obtain black and when these amounts +are all at full intensity, you obtain white (D65 white point, see `CIE +1931 xy chromaticity space +`_). + +Consequently, when you specify a color in matplotlib +(e.g. `"#123456"`), you need to realize that this color is implicitly +encoded in the sRGB color model and space. This draws immediate +consequences. For example, if you try to produce a gradient between +two colors using a naive approach, you'll get wrong perceptual results +because the sRGB model is not linear. This is illustrated on figure +:ref:`figure-color-gradients` where I plotted gradients using the sRGB +naive approach (first line on each gradient). You can observe that the +result is far from being satisfactory. A better way to build a +gradient is to first convert colors to the linear RGB space, apply +the gradient, and then convert it back to the sRGB color model. This is +illustrated on the second line of each gradient that are now +perceptually smoother. A third (and better) solution is to use the +`CIE Lab `_ model +that has been tailored to the human perception and provides a +perceptually uniform space. It is a bit more complicated to manipulate +and you'll need external packages such as `scikit-image +`_ or `colour +`_ to make the +conversion between the different models and spaces, but results are +worth the effort. + +.. figure:: colors/color-gradients.pdf + :width: 100% + + Linear color gradients using different color models + :label:`figure-color-gradients` (sources: :source:`colors/color-gradients.py`). + +Another popular model is the `HSV +`_ model that stands for +Hue, Saturation and Value (see figure :ref:`figure-color-wheel`). It +provides an alternate color model to access the same color space as +the sRGB system. Matplotlib provides methods to convert to and +from the HSV model (see the `colors `_ module). + +.. figure:: colors/color-wheel.pdf + :width: 100% + + Color wheel (HSV) + :label:`figure-color-wheel` (sources: :source:`colors/color-wheel.py`). + + +Choosing colors +--------------- + +Maybe at this point the only question you have in mind is "Ok, +interesting, but how do I choose a color then? Do I even have to +choose anyway?" For this second question, you can actually let +Matplotlib choose for you. When you draw several plots at once, you +may have noticed that the plots use several different colors. These +colors are picked from what is called a color cycle: + +.. code:: python + + >>> import matplotlib.pyplot as plt + >>> print(plt.rcParams['axes.prop_cycle'].by_key()['color']) + ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', + '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] + +These colors come from the `tab10` colormap which itself comes from +the `Tableau `_ software: + +.. code:: python + + >>> import matplotlib.colors as colors + >>> cmap = plt.get_cmap("tab10") + >>> [colors.to_hex(cmap(i)) for i in range(10)] + ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', + '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] + +These colors have been designed to be sufficiently different such as +to ease the visual perception of difference while being not too +aggressive on the eye (compared to saturated pure blue, green or red +colors for example). If you need more colors, you need first to ask +yourself whether you really need more colors. Then, and only then, you +might consider using palettes that have been designed with care. This +is the case of the open color palette (see figure +:ref:`figure-open-colors`) and the material color palette (see +:ref:`figure-material-colors`). For example, on figure +:ref:`stacked-plots`, I use two color stacks (`blue grey` and `yellow` +from the material palettes) to highlight an area of interest. + + +.. figure:: colors/stacked-plots.pdf + :width: 100% + + Stacked plots using two different color stacks to better highlight + an area of interest :label:`stacked-plots` (sources: + :source:`colors/stacked-plots.py`). + + +.. figure:: colors/open-colors.pdf + :width: 100% + + Open colors + :label:`figure-open-colors` (sources: :source:`colors/open-colors.py`). + + +.. figure:: colors/material-colors.pdf + :width: 100% + + Material colors + :label:`figure-material-colors` (sources: :source:`colors/material-colors.py`). + + +Another usage is to use color stacks to identify different groups +while allowing variation inside each group. When doing this, you need +to conserve the same color semantics throughout all your subsequent +figures. + +.. figure:: colors/colored-hist.pdf + :width: 100% + + Identification of groups with internal variations using color stacks. + :label:`figure-colored-hist` (sources: :source:`colors/colored-hist.py`). + +Another popular usage of color is to show some plots associated with their standard deviation (SD) or standard error (SE). To do that, there are two different ways to do it. Either with use palettes as the o,e defined previously or we use transparency using the `alpha` keyword. Let's compare the results. + +.. figure:: colors/alpha-vs-color.pdf + :width: 100% + + Showing standard deviation, with or without transparency + :label:`figure-alpha-vs-color` (sources: :source:`colors/alpha-vs-color.py`). + +As you can see on the left part of figure :ref:`figure-alpha-vs-color`, using transparency results in the two plots to be somehow mixed together. This might be a useful effect since it allows you to show what is happening in shared areas. This is not the case when using opaque colors and you thus have to decide which plot is covering the other (using `zorder`). Note that the choice of one or the other solution is up to you since it very much depends on your date. + +However, it is important to note that the use of transparency is quite specific in the sense that the visual result is not specified explicitly in the script. It depends actually from the actual rendering of the figure and the way matplotlib composes the different elements. Let's consider for example a scatter plot (normal distribution) where each point is transparent (10%): + +.. figure:: colors/alpha-scatter.pdf + :width: 100% + + Semi-transparent scatter plots + :label:`figure-alpha-scatter` (sources: :source:`colors/alpha-scatter.py`). + +On the left part of figure :ref:`figure-alpha-scatter`, we can see the result with a perceptually darker area in the center. This is a direct result of rendering several small discs on top of each other in the central area. If we want to quantify this perceptual result, we need to use a trick. The trick is to render the scatter plot in an array such that we can consider the result as an image. Such image is displayed in the central part and from this, we can play with the perceptual density as shown on the right part. + + +Choosing colormaps +------------------ + +Colormapping corresponds to the mapping of values to colors, using a colormap that defines, for each value, the corresponding color. There are different types of colormaps (sequential, diverging, cyclic, qualitative or none of these) that correspond to different use cases. It is is important to use the right type or colormap that corresponds to your data. To pick a colormap, you can start by answering questions illustrated on figure :ref:`colormap-tree` and then choose the corresponding `colormap `_ from the matplotlib website. + +.. figure:: colors/colormap-tree.pdf + :width: 100% + + How to choose a colormap? + :label:`colormap-tree` + +Problem is, for each type, there exist several colormaps. But if you pick the right type, the choice is yours and depends mostly on you aesthetic taste. As long as you choose the right type, you cannot be wrong. Figure :ref:`figure-mona-lisa` a few choices associated with sequential colormaps and they all look good. In this case, one selection criterion could be the fact that the image represents a human being and we may prefer a colormap close to skin tones. + +.. figure:: colors/mona-lisa.pdf + :width: 100% + + Variations on Mona Lisa (Leonardo da Vinci, 1503). + :label:`figure-mona-lisa` (sources: :source:`colors/mona-lisa.py`). + +Diverging colormaps needs special care because they are really composed of two gradients with a special central value. By default, this central value is mapped to 0.5 in the normalized linear mapping and this works pretty well as long as the absolute minimum and maximum value of your data are the same. Now, consider the situation illustrated on figure :ref:`figure-colormap-transform`. Here we have a small domain with negative values and a larger domain with positive values. Ideally, we would like the negative values to be mapped with blueish colors and positive values with yellowish colors. If we use a diverging colormap without any precaution, there's no guarantee that we'll obtain the result we want. To fix the problem, we thus need to tell matplotlib what is the central value and to do this, we need to use a `Two Slope norm `_ instead of a `Linear norm `_. + +.. figure:: colors/colormap-transform.pdf + :width: 100% + + Colormap with linear norm vs two slopes norm. + :label:`figure-colormap-transform` + + +.. code:: python + + >>> import matplotlib.pyplot as plt + >>> import matplotlib.colors as colors + >>> cmap = plt.get_cmap("Spectral") + + >>> norm = mpl.colors.Normalize(vmin=-3, vmax=10) + >>> Print(norm(0)) + 0.23076923076923078 + >>> print(cmap(norm(0))) + (0.968, 0.507, 0.300, 1.0) + + >>> norm = mpl.colors.TwoSlopeNorm(vmin=-3, vcenter=0, vmax=10) + >>> print(norm(0)) + 0.5 + >>> cmap = plt.get_cmap("Spectral") + >>> print(cmap(norm(0))) + (0.998, 0.999, 0.746, 1.0) + + +Exercises +--------- + +**Exercise 1** The goal is to reproduce the figure :ref:`figure-colored-plot`. The trick is to split each line is small segments such that they can each have their own colors since it is not possible to do that with a regular plot. However, for performance reasons, you'll need to use `LineCollection `_. You can start from the following code: + +.. code:: python + + X = np.linspace(-5*np.pi, +5*np.pi, 2500) + for d in np.linspace(0,1,15): + dx, dy = 1 + d/3, d/2 + (1-np.abs(X)/X.max())**2 + Y = dy * np.sin(dx*X) + 0.1*np.cos(3+5*X) + +.. figure:: colors/colored-plot.pdf + :width: 100% + + (Too much) colored line plots (sources :source:`colors/colored-plot.py`) + :label:`figure-colored-plot` + + +**Exercise 2** This exercise is a bit tricky and requires the usage of `PolyCollection `_. The tricky part is to define, in a generic way, each polygon depending on the number of branches and sections. It is mostly trigonometry. I advise to start by drawing only the main lines and then create the small patches. The color part should then be easy because it depends only on the angle and you can thus use HSV encoding. + +.. figure:: colors/flower-polar.pdf + :width: 100% + + Flower polar (sources :source:`colors/flower-polar.py`) + :label:`figure-flower-polar` diff --git a/rst/common.rst b/rst/common.rst new file mode 100644 index 0000000..28f7f5f --- /dev/null +++ b/rst/common.rst @@ -0,0 +1,31 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. Date: June 2019 +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- + +.. role:: ref +.. role:: nameref +.. role:: label +.. role:: cite +.. role:: code +.. role:: source +.. default-role:: code + +.. role:: raw-latex(raw) + :format: latex + + +.. |newline| raw:: latex + + \newline + +.. |newpage| raw:: latex + + \clearpage + + +.. |br| raw:: html + +
diff --git a/rst/conclusion.rst b/rst/conclusion.rst new file mode 100644 index 0000000..f37bd0c --- /dev/null +++ b/rst/conclusion.rst @@ -0,0 +1,33 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-conclusion: + + +:raw-latex:`\chapter*{Conclusion}` + + +You've now reached the end of this book that took me nearly two years +to write, mostly due to the pandemic that slowed down things quite a +bit. I'm not totally satisfied with the result because there is a +lot of features I wanted to introduce but I've never found the time to +do so and the book had to be released at some point. This will be +included in the second edition, depending how the first edition is +perceived by the community. + +In any case, I hope you'll find the book useful for your own work and +spread the word if you like it. If you want to support my work, you +can buy a printed edition on Amazon. + + +.. raw:: latex + + \vspace{10mm} + \begin{flushright} + Nicolas P. Rougier,\\ + Bordeaux, 12 November 2021.\\ + \vspace{5mm} + \includegraphics[width=3cm]{gravatar-2.png} + \end{flushright} diff --git a/rst/coordinates.rst b/rst/coordinates.rst new file mode 100644 index 0000000..2628e9d --- /dev/null +++ b/rst/coordinates.rst @@ -0,0 +1,352 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-coordinates: + +Coordinate systems +================== + +In any matplotlib figure, there is at least two different coordinate systems +that co-exist anytime. One is related to the figure (FC) while the others are +related each of the individual plots (DC). Each of these coordinate systems +exists in normalized (NxC) or native version (xC) as illustrated in figures +:ref:`fig-coordinates-cartesian` and :ref:`fig-coordinates-polar`. To convert a +coordinate from one system to the other, matplotlib provides a set of +`transform` functions: + +.. code:: Python + + fig = plt.figure(figsize=(6, 5), dpi=100) + ax = fig.add_subplot(1, 1, 1) + ax.set_xlim(0,360), ax.set_ylim(-1,1) + + # FC : Figure coordinates (pixels) + # NFC : Normalized figure coordinates (0 → 1) + # DC : Data coordinates (data units) + # NDC : Normalized data coordinates (0 → 1) + + DC_to_FC = ax.transData.transform + FC_to_DC = ax.transData.inverted().transform + + NDC_to_FC = ax.transAxes.transform + FC_to_NDC = ax.transAxes.inverted().transform + + NFC_to_FC = fig.transFigure.transform + FC_to_NFC = fig.transFigure.inverted().transform + + +.. figure:: coordinates/coordinates-cartesian.pdf + :width: 100% + + The co-existing coordinate systems within a figure using Cartesian + projection. **FC**: Figure Coordinates, **NFC** Normalized Figure Coordinates, + **DC**: Data Coordinatess, **NDC**: Normalized Data Coordinates. + :label:`fig-coordinates-cartesian` + + +.. figure:: coordinates/coordinates-polar.pdf + :width: 100% + + The co-existing coordinate systems within a figure using Polar projection. + **FC**: Figure Coordinates, **NFC** Normalized Figure Coordinates, **DC**: + Data Coordinatess, **NDC**: Normalized Data Coordinates. + :label:`fig-coordinates-polar` + + +Let's test theses functions on some specific points (corners): + +.. code:: python + + # Top right corner in normalized figure coordinates + print(NFC_to_FC([1,1])) # (600,500) + + # Top right corner in normalized data coordinates + print(NDC_to_FC([1,1])) # (540,440) + + # Top right corner in data coordinates + print(DC_to_FC([360,1])) # (540,440) + +Since we also have the inverse functions, we can create our own transforms. For +example, from native data coordinates (DC) to normalized data coordinates (NDC): + +.. code:: Python + + # Native data to normalized data coordinates + DC_to_NDC = lambda x: FC_to_NDC(DC_to_FC(x)) + + # Bottom left corner in data coordinates + print(DC_to_NDC([0, -1])) # (0.0, 0.0) + + # Center in data coordinates + print(DC_to_NDC([180,0])) # (0.5, 0.5) + + # Top right corner in data coordinates + print(DC_to_NDC([360,1])) # (1.0, 1.0) + +When using Cartesian projection, the correspondence is quite clear between the +normalized and native data coordinates. With other kind of projection, things +work just the same even though it might appear less obvious. For example, let +us consider a polar projection where we want to draw the outer axes border. In +normalized data coordinates, we know the coordinates of the four corners, +namely `(0,0)`, `(1,0)`, `(1,1)` and `(0,1)`. We can then transforms these +normalized data coordinates back to native data coordinates and draw the +border. There is however a supplementary difficulty because those coordinates +are beyond the axes limit and we'll need to tell matplotlib to not care about +the limit using the `clip_on` arguments. + +.. code:: Python + + fig = plt.figure(figsize=(5, 5), dpi=100) + ax = fig.add_subplot(1, 1, 1, projection='polar') + + FC_to_DC = ax.transData.inverted().transform + NDC_to_FC = ax.transAxes.transform + NDC_to_DC = lambda x: FC_to_DC(NDC_to_FC(x)) + P = NDC_to_DC([[0,0], [1,0], [1,1], [0,1], [0,0]]) + + plt.plot(P[:,0], P[:,1], clip_on=False, zorder=-10 + color="k", linewidth=1.0, linestyle="--", ) + plt.scatter(P[:-1,0], P[:-1,1], + clip_on=False, facecolor="w", edgecolor="k") + plt.show() + +The result is shown on figure :ref:`fig-transforms-polar`. + +.. figure:: coordinates/transforms-polar.pdf + :width: 75% + + Axes boundaries in polar projection using a transform from normalized data + coordinates to data coordinates (:source:`coordinates/transform-polar.py`). + :label:`fig-transforms-polar` + +However, most of the time, you won't need to use these transform functions +explicitly but rather implicitly. For example, consider the case where you +want to add some text over a specific plot. For this, you need to use the text_ +function and to specify what is to be written (of course) and the coordinates +where you want to display the text. The question (for matplotlib) is how to +consider these coordinates? Are they expressed in data coordinates? normalized +data coordinates? normalized figure coordinates? The default is to consider +they are expressed in data coordinates. Consequently, if you want to us a +different system, you'll need to explicitly specify a `transform` when calling +the function. Let's say for example we want to add a letter on the bottom right +corner. We can write: + +.. code:: Python + + fig = plt.figure(figsize=(6, 5), dpi=100) + ax = fig.add_subplot(1, 1, 1) + + ax.text(0.1, 0.1, "A", transform=ax.transAxes) + plt.show() + +The letter will be placed at 10% from the left spine and 10% from the bottom +spine. If the two spines have the same physical size (in pixels), the letter +will be equidistant from the right and bottom spines. But, if they have +different size, this won't be true anymore and the results will not be very +satisfying (see panel A of figure :ref:`fig-transforms-letter`). What we want +to do instead is to specify a transform that is a combination of the normalized +data coordinates (0,0) plus an offset expressed in figure native units +(pixels). To do that, we need to build our own transform function to compute +the offset: + +.. code:: Python + + from matplotlib.transforms import ScaleTranslation + + fig = plt.figure(figsize=(6, 4)) + + ax = fig.add_subplot(2, 1, 1) + plt.text(0.1, 0.1, "A", transform=ax.transAxes) + + ax = fig.add_subplot(2, 1, 2) + dx, dy = 10/fig.dpi, 10/fig.dpi + offset = ScaledTranslation(dx, dy, fig.dpi_scale_trans) + plt.text(0, 0, "B", transform=ax.transAxes + offset) + + plt.show() + +The result is illustrated on panel B of figure :ref:`fig-transforms-letter`. +The text is now properly positioned and will stay at the right position +independently of figure aspect ratio or data limits. + +.. figure:: coordinates/transforms-letter.pdf + :width: 100% + + Using transforms to position precisely a text over a plot. Top panel uses + normalized data coordinates (0.1,0.1), bottom panel uses normalized data + coordinates (0.0,0.0) plus an offset (10,10) expressed in figure + coordinates (:source:`coordinates/transform-letter.py`). :label:`fig-transforms-letter` + + +Things can become even more complicated when you need a different transform on +the X and Y axis. Let us consider for example the case where you want to add +some text below the X tick labels. The X position of the tick labels is +expressed in data coordinates, but how do we put something under as illustrated +on figure :ref:`fig-transforms-blend`? + +.. figure:: coordinates/transforms-blend.pdf + :width: 100% + + Axes boundaries in polar projection using a transform from normalized data + coordinates to data coordinates (:source:`coordinates/transform-blend.py`). + :label:`fig-transforms-blend` + +The natural unit for text is point and we thus want to position our arrow using +a Y offset expressed in points. To do that, we need to use a blend transform: + +.. code:: Python + + point = 1/72 + fontsize = 12 + dx, dy = 0, -1.5*fontsize*point + offset = ScaledTranslation(dx, dy, fig.dpi_scale_trans) + transform = blended_transform_factory( + ax.transData, ax.transAxes+offset) + + +We can also use transformations to a totally different usage as shown +on figure :ref:`figure-collage`. To obtain such figure, I rewrote the +`imshow +`__ +function to apply translation, scaling and rotation and I call the +function 200 times with random values. + +.. code:: python + + def imshow(ax, I, position=(0,0), scale=1, angle=0): + height, width = I.shape + extent = scale * np.array([-width/2, width/2, + -height/2, height/2]) + im = ax.imshow(I, extent=extent, zorder=zorder) + t = transforms.Affine2D().rotate_deg(angle).translate(*position) + im.set_transform(t + ax.transData) + + +.. figure:: coordinates/collage.png + :width: 100% + + Collage + :label:`figure-collage` + (sources: :source:`coordinates/collage.py`). + + +Transformations are quite powerful tools even though you won't manipulate them +too often in your daily life. But there are some few cases where you'll be +happy to know about them. You can read further on transforms and coordinates +with the `Transformation tutorial`_ on the matplotlib website. + + +Real case usage +--------------- + +Let's now study a real case of transforms as shown on figure +:ref:`fig-transforms-hist`. This is a simple scatter plot showing some Gaussian +data, with two principal axis. I added a histogram that is orthogonal to the +first principal component axis to show the distribution on the main axis. +This figure might appear simple (a scatter plot and an oriented histogram) but +the reality is quite different and rendering such a figure is far from +obvious. The main difficulty is to have the histogram at the right position, +size and orientation knowing that position must be set in data coordinates, +size must be given in figure normalized coordinates and orientation in +degrees. To complicate things, we want to express the elevation of the text +above the histogram bars in term of data points. |newline| + +.. figure:: coordinates/transforms-hist.pdf + :width: 100% + + Rotated histogram aligned with second main PCA axis + (:source:`coordinates/transforms-hist.py`). :label:`fig-transforms-hist` + +You can have a look at the sources for the complete story but let's concentrate +on the main difficulty, that is adding a rotated floating axis. Let us start with +a simple figure: + +.. code:: Python + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.transforms import Affine2D + import mpl_toolkits.axisartist.floating_axes as floating + + fig = plt.figure(figsize=(8,8)) + ax1 = plt.subplot(1,1,1, aspect=1, + xlim=[0,10], ylim=[0,10]) + +Let's imagine we want to have a floating axis whose center is (5,5) in data +coordinates, size is (5,3) in data coordinates and orientation is -30 degrees: + +.. code:: Python + + center = np.array([5,5]) + size = np.array([5,3]) + orientation = -30 + T = size/2*[(-1,-1), (+1,-1), (+1,+1), (-1,+1)] + rotation = Affine2D().rotate_deg(orientation) + P = center + rotation.transform(T) + +In the code above, we defined the four points delimiting the extent of our new +axis and we took advantage of matplotlib affine transforms to do the actual +rotation. At this point, we have thus four points describing the border of the +axis in data coordinates and we need to transform them in figure normalized +coordinates because the floating axis requires normalized figure coordinates. + +.. code:: Python + + DC_to_FC = ax1.transData.transform + FC_to_NFC = fig.transFigure.inverted().transform + DC_to_NFC = lambda x: FC_to_NFC(DC_to_FC(x)) + +We have one supplementary difficulty because the position of a floating axis +needs to be defined in term of non-rotated bounding box: + +.. code:: Python + + xmin, ymin = DC_to_NFC((P[:,0].min(), P[:,1].min())) + xmax, ymax = DC_to_NFC((P[:,0].max(), P[:,1].max())) + +We now have all the information to add our new axis: + +.. code:: Python + + transform = Affine2D().rotate_deg(orientation) + helper = floating.GridHelperCurveLinear( + transform, (0, size[0], 0, size[1])) + ax2 = floating.FloatingSubplot( + fig, 111, grid_helper=helper, zorder=0) + ax2.set_position((xmin, ymin, xmax-xmin, ymax-xmin)) + fig.add_subplot(ax2) + +The result is shown on figure :ref:`fig-transforms-floating-axis`. + + +Exercise +-------- + +**Exercise 1** When you specify the size of markers in a scatter plot, this +size is expressed in points. Try to make a scatter plot whose size is expressed +in data points such as to obtain figure :ref:`fig-transforms-exercise-1`. + +.. figure:: coordinates/transforms-exercise-1.pdf + :width: 100% + + A scatter plot whose marker size is expressed in data coordinates instead of points + (:source:`coordinates/transforms-exercise-1.py`). + :label:`fig-transforms-exercise-1` + + +.. figure:: coordinates/transforms-floating-axis.pdf + :width: 100% + + A floating and rotated floating axis with controlled position size and + rotation (:source:`coordinates/transforms-floating-axis.py`). + :label:`fig-transforms-floating-axis` + + +.. --- Links ------------------------------------------------------------------ +.. _text: https://matplotlib.org/api/_as_gen/matplotlib.pyplot.text.html +.. _Transformation tutorial: https://matplotlib.org/tutorials/advanced/transforms_tutorial.html +.. ---------------------------------------------------------------------------- + + diff --git a/rst/defaults.rst b/rst/defaults.rst new file mode 100644 index 0000000..d59d8be --- /dev/null +++ b/rst/defaults.rst @@ -0,0 +1,271 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-defaults: + +Mastering the defaults +====================== + +We've just explained (see rule 5 in chapter chap-rules_) that any visualization library or software comes with a set of default settings that identifies it. For example, figure :ref:`figure-sine-cosine-variants` show the sine and cosine functions as rendered by Google calculator, Julia, Gnuplot and Matlab. Even for such simple functions, these displays are quite characteristic. + +.. figure:: defaults/sine-cosine-variants.png + :width: 100% + + Sine and cosine functions as displayed by (A) Google calculator (B) Julia, + (C) Gnuplot (D) Matlab. :label:`figure-sine-cosine-variants` + +Let's draw sine and cosine functions using Matplotlib defaults. + +.. code:: python + + import numpy as np + import matplotlib.pyplot as plt + + X = np.linspace(-np.pi, np.pi, 256, endpoint=True) + C, S = np.cos(X), np.sin(X) + plt.plot(X, C) + plt.plot(X, S) + plt.show() + + +Figure :ref:`figure-defaults-step-2` shows the result that is quite characteristic of Matplotlib. + +.. figure:: defaults/defaults-step-1.pdf + :width: 100% + + Sine and cosine functions with implicit defaults + (sources :source:`defaults/defaults-step-1.py`) + :label:`figure-defaults-step-1` + +Explicit settings +----------------- + +Let's now redo the figure but with the specification of all the different settings. This includes, figure size, line colors, widths and styles, ticks positions and labels, axes limits, etc. + +.. code:: python + + fig = plt.figure(figsize = p['figure.figsize'], + dpi = p['figure.dpi'], + facecolor = p['figure.facecolor'], + edgecolor = p['figure.edgecolor'], + frameon = p['figure.frameon']) + ax = plt.subplot(1,1,1) + + ax.plot(X, C, color="C0", + linewidth = p['lines.linewidth'], + linestyle = p['lines.linestyle']) + ax.plot(X, S, color="C1", + linewidth = p['lines.linewidth'], + linestyle = p['lines.linestyle']) + + xmin, xmax = X.min(), X.max() + xmargin = p['axes.xmargin']*(xmax - xmin) + ax.set_xlim(xmin - xmargin, xmax + xmargin) + + ymin, ymax = min(C.min(), S.min()), max(C.max(), S.max()) + ymargin = p['axes.ymargin']*(ymax - ymin) + ax.set_ylim(ymin - ymargin, ymax + ymargin) + + ax.tick_params(axis = "x", which="major", + direction = p['xtick.direction'], + length = p['xtick.major.size'], + width = p['xtick.major.width']) + ax.tick_params(axis = "y", which="major", + direction = p['ytick.direction'], + length = p['ytick.major.size'], + width = p['ytick.major.width']) + plt.show() + +.. figure:: defaults/defaults-step-2.pdf + :width: 100% + + Sine and cosine functions using matplotlib explicit defaults + (sources :source:`defaults/defaults-step-2.py`) + :label:`figure-defaults-step-2` + +The resulting figure :ref:`figure-defaults-step-2` is an exact copy of :ref:`figure-defaults-step-1`. This comes as no surprise because I took care of reading the default values that are used implicitly by Matplotlib and set them explicitly. In fact, there are many more default choices that I did not materialize in this short example. For instance, the font family, slant, weight and size of tick labels can be configured in the defaults. + +User settings +------------- + +Note that we can also do the opposite and change the defaults before creating the figure. This way, matplotlib will use our custom defaults instead of standard ones. The result is shown on figure :ref:`figure-defaults-step-3` where I changed a number of settings. Unfortunately, not every settings can be modified this way. For example, the position of markers (`markevery`) cannot yet be set. + +.. code:: python + + p["figure.figsize"] = 6,2.5 + p["figure.edgecolor"] = "black" + p["figure.facecolor"] = "#f9f9f9" + + p["axes.linewidth"] = 1 + p["axes.facecolor"] = "#f9f9f9" + p["axes.ymargin"] = 0.1 + p["axes.spines.bottom"] = True + p["axes.spines.left"] = True + p["axes.spines.right"] = False + p["axes.spines.top"] = False + p["font.sans-serif"] = ["Fira Sans Condensed"] + + p["axes.grid"] = False + p["grid.color"] = "black" + p["grid.linewidth"] = .1 + + p["xtick.bottom"] = True + p["xtick.top"] = False + p["xtick.direction"] = "out" + p["xtick.major.size"] = 5 + p["xtick.major.width"] = 1 + p["xtick.minor.size"] = 3 + p["xtick.minor.width"] = .5 + p["xtick.minor.visible"] = True + + p["ytick.left"] = True + p["ytick.right"] = False + p["ytick.direction"] = "out" + p["ytick.major.size"] = 5 + p["ytick.major.width"] = 1 + p["ytick.minor.size"] = 3 + p["ytick.minor.width"] = .5 + p["ytick.minor.visible"] = True + + p["lines.linewidth"] = 2 + p["lines.markersize"] = 5 + + fig = plt.figure(linewidth=1) + ax = plt.subplot(1,1,1,aspect=1) + ax.plot(X, C) + ax.plot(X, S) + + plt.show() + + +.. figure:: defaults/defaults-step-3.pdf + :width: 100% + + Sine and cosine functions using custom defaults + (sources :source:`defaults/defaults-step-3.py`) + :label:`figure-defaults-step-3` + + +Stylesheets +----------- + +Changing default settings is thus an easy way to customize the style of your figure. But writing such style inside the figure script as we did until now is not very convenient and this is where `style `_ comes into play. Styles are small text files describing (some) settings in the same way as they are defined in the main resource file `matplotlibrc `_: + +.. code:: text + + figure.figsize: 6,2.5 + figure.edgecolor: black + figure.facecolor: ffffff + + axes.linewidth: 1 + axes.facecolor: ffffff + axes.ymargin: 0.1 + axes.spines.bottom: True + axes.spines.left: True + axes.spines.right: False + axes.spines.top: False + font.sans-serif: Fira Sans Condensed + + axes.grid: False + grid.color: black + grid.linewidth: .1 + + xtick.bottom: True + xtick.top: False + xtick.direction: out + xtick.major.size: 5 + xtick.major.width: 1 + xtick.minor.size: 3 + xtick.minor.width: .5 + xtick.minor.visible: True + + ytick.left: True + ytick.right: False + ytick.direction: out + ytick.major.size: 5 + ytick.major.width: 1 + ytick.minor.size: 3 + ytick.minor.width: .5 + ytick.minor.visible: True + + lines.linewidth: 2 + lines.markersize: 5 + +And we can now write: + +.. code:: python + + plt.style.use("./mystyle.txt") + + fig = plt.figure(linewidth=1) + ax = plt.subplot(1,1,1,aspect=1) + ax.plot(X, C) + ax.plot(X, S) + ax.set_yticks([-1,0,1]) + + +Beyond stylesheets +------------------ + +If stylesheet allows to set a fair number of parameters, there is still plenty of other things that can be changed to improve the style of a figure even though we cannot use the stylesheet to do so. One of the reason is that these settings are specific to a given figure and it wouldn't make sense to set them in the stylesheet. In the sine and cosine case, we can for example specify explicitly the location and labels of xticks, taking advantage of the fact that we know that we're dealing with trigonometry functions: + +.. code:: python + + ax.set_yticks([-1,1]) + ax.set_xticklabels(["-1", "+1"]) + + ax.set_xticks([-np.pi, -np.pi/2, np.pi/2, np.pi]) + ax.set_xticklabels(["-π", "-π/2", "+π/2", "+π"]) + +We can also move the spines such as to center them: + +.. code:: python + + ax.spines['bottom'].set_position(('data',0)) + ax.spines['left'].set_position(('data',0)) + +And add some arrows at axis ends: + +.. code:: python + + ax.plot(1, 0, ">k", + transform=ax.get_yaxis_transform(), clip_on=False) + ax.plot(0, 1, "^k", + transform=ax.get_xaxis_transform(), clip_on=False) + + +You can see the result on figure :ref:`figure-defaults-step-5`. From this, you can start refining further the figure. But remember that if it's ok to tweak parameters a bit, you can also lose a lot of time doing that (trust me). + +.. figure:: defaults/defaults-step-5.pdf + :width: 100% + + Sine and cosine functions using custom defaults and fine tuning. + (sources :source:`defaults/defaults-step-5.py`) + :label:`figure-defaults-step-5` + +Exercise +-------- + +Starting from the code below try to reproduce figure :ref:`figure-defaults-exercise-1` +by modifying only rc settings. + +.. code:: python + + fig = plt.figure() + ax = plt.subplot(1,1,1,aspect=1) + ax.plot(X, C, markevery=(0, 64), clip_on=False, zorder=10) + ax.plot(X, S, markevery=(0, 64), clip_on=False, zorder=10) + ax.set_yticks([-1,0,1]) + ax.set_xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi]) + ax.set_xticklabels(["-π", "-π/2", "0", "+π/2", "+π"]) + ax.spines['bottom'].set_position(('data',0)) + + +.. figure:: defaults/defaults-exercice-1.pdf + :width: 100% + + Alternative rendering of sine and cosine + (solution :source:`defaults/defaults-exercice-1.py`) + :label:`figure-defaults-exercise-1` diff --git a/rst/layout.rst b/rst/layout.rst new file mode 100644 index 0000000..a998974 --- /dev/null +++ b/rst/layout.rst @@ -0,0 +1,237 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-layout: + +Size, aspect & layout +===================== + +The layout of figures and sub-figures is certainly one of the most +frustrating aspect of matplotlib and for new user, it is generally +difficult to obtain the desired layout without a lot of trials and +errors.. But this is not specific to matplotlib, it is actually +equally difficult with any software (and even worse for some). To +understand why it is difficult, it is necessary to gain a better +understanding of the underlying machinery. + +Figure and axes aspect +---------------------- + +When you create a new figure, this figure comes with a specific size, either implicitly using defaults (as explained in the previous chapter) or explicitly through the `figsize` keyword. If we take the height divided by the width of the figure we obtain the figure aspect ratio. When you create an axes, you can also specify an aspect that will be enforced by matplotlib. And here, we hit the first difficulty. You have a container with a given aspect ratio and you want to put inside an item with a possibly different aspect ratio and matplotlib has to solve these constrains. This is illustrated on figure :ref:`figure-aspects` with different cases: + +**A**: figure aspect is 1, axes aspect is 1, x and y range are equal + + .. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,1), ax.set_ylim(0,1) + + +**B**: figure aspect is 1/2, axes aspect is 1, x and y range are equal + + .. code:: python + + fig = plt.figure(figsize=(6,3)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,1), ax.set_ylim(0,1) + +**C**: figure aspect is 1/2, axes aspect is 1, x and y range are different + + .. code:: python + + fig = plt.figure(figsize=(6,3)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,2), ax.set_ylim(0,1) + + +**D**: figure aspect is 2, axes aspect is 1, x and y range are equal + + .. code:: python + + fig = plt.figure(figsize=(3,6)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,1), ax.set_ylim(0,1) + + +**E**: figure aspect is 1, axes aspect is 1, x and y range are different + + .. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,2), ax.set_ylim(0,1) + + +**F**: figure aspect is 1, axes aspect is 0.5, x and y range are equal + + .. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(1,1,1, aspect='auto') + ax.set_xlim(0,1), ax.set_ylim(0,1) + +**G**: figure aspect is 1/2, axes aspect is 1, x and y range are different + + .. code:: python + + fig = plt.figure(figsize=(3,6)) + ax = plt.subplot(1,1,1, aspect=1) + ax.set_xlim(0,1), ax.set_ylim(0,2) + +**H**: figure aspect is 1, axes aspect is 1, x and y range are different + + .. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(1,1,1, aspect='auto') + ax.set_xlim(0,1), ax.set_ylim(0,2) + +**I**: figure aspect is 1, axes aspect is 2, x and y range are equal + + .. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(1,1,1, aspect='auto') + ax.set_xlim(0,1), ax.set_ylim(0,1) + + +.. figure:: layout/aspects.pdf + :width: 100% + + Combination of figure and axes aspect ratio + :label:`figure-aspects` + + +The final layout of a figure results from a set of constraints that makes it difficult to predict the end result. This is actually even more acute when you combine several axes on the same figure as shown on figures +:ref:`figure-layout-aspect-1`, :ref:`figure-layout-aspect-2` & :ref:`figure-layout-aspect-3`. Depending on what is important in your figure (aspect, range or size), you'll privilege one of these layout. In any case, you should now have realized that if you over-constrained your layout, it might be unsolvable and matplotlib will try to find the best compromise. + +.. figure:: layout/layout-aspect-1.pdf + :width: 100% + + Same size, same range, different aspect + (sources :source:`layout/layout-aspect.py`) + :label:`figure-layout-aspect-1` + + +.. figure:: layout/layout-aspect-2.pdf + :width: 100% + + Same range, same aspect, different size + (sources :source:`layout/layout-aspect.py`) + :label:`figure-layout-aspect-2` + + +.. figure:: layout/layout-aspect-3.pdf + :width: 100% + + Same size, same aspect, different range + (sources :source:`layout/layout-aspect.py`) + :label:`figure-layout-aspect-3` + + +Axes layout +----------- + +Now that we know how figure and axes aspect may interact, it's time to organize our figure into subfigures. We've already seen one example in the previous section, but let's now look at the details. There exist indeed several `different methods `_ to create subfigures and each have their pros and cons. Let's take a very simple example where we want to have two axes side by side. To do so, we can use `add_subplot `_, `add_axes `_, `GridSpec `_ and `subplot_mosaic `_: + +.. code:: python + + fig = plt.figure(figsize=(6,2)) + + # Using subplots + ax1 = fig.add_subplot(1,2,1) + ax2 = fig.add_subplot(1,2,2) + + # Using gridspecs + G = GridSpec(1,2) + ax1 = fig.add_subplot(G[0,0]) + ax2 = fig.add_subplot(G[0,1]) + + # Using axes + ax1 = fig.add_axes([0.1, 0.1, 0.35, 0.8]) + ax2 = fig.add_axes([0.6, 0.1, 0.35, 0.8]) + + # Using mosaic + ax1, ax2 = fig.add_mosaic("AB") + + +As a general advice, I would encourage users to use the gridspec +approach because if offers a lot of flexibility compared to the +classical approach. For example, figure :ref:`figure-layout-classical` +shows a nice and simple 3x3 layout but does not offer much control +over the relative aspect of each axes whereas in figure +:ref:`figure-layout-gridspec`, we can very easily specify different +sizes for each axes. + +.. figure:: layout/layout-classical.pdf + :width: 100% + + Subplots using classical layout. + (source :source:`layout/layout-classical.py`) + :label:`figure-layout-classical` + +.. figure:: layout/layout-gridspec.pdf + :width: 100% + + Subplots using gridspec layout + (source :source:`layout/layout-gridspec.py`) + :label:`figure-layout-gridspec` + +The biggest difficulty with gridspec is to get axes right, that is, at +the right position with the right size and this depends on the initial +partition of the grid spec, taking into account the different height & +width ratios. Let's consider for example figure +:ref:`figure-complex-layout` that display a moderately complex +layout. The question is: how do we partition it? Do we need a single +axis for the small image of individual axes ? Are the text on the left +part of axes or do they use their own axes. There are indeed several +solutions and figure :ref:`figure-complex-layout-bare` shows the +solution I chose to design this figure. There are individual axes for +subplots and left label. There is also a small line of axes for titles +in order to ensure that subplots have all the same size. If I had used +a title on the first row of subplots, this would have modified their +relative size compared to others. The legend on the top is using two +axes, one axes for the color legend and another for detailed +explanation. In this case, I use the central axes and write the text +outside the axes, specifying this does not need to be clipped. + +.. figure:: layout/complex-layout.pdf + :width: 100% + + Complex layout + (source :source:`layout/complex-layout.py`) + :label:`figure-complex-layout` + +.. figure:: layout/complex-layout-bare.pdf + :width: 100% + + Complex layout structure + (source :source:`layout/complex-layout-bare.py`) + :label:`figure-complex-layout-bare` + + +Exercices +--------- + +**Standard layout 1** Using gridspec, the goal is to reproduce figure +:ref:`figure-standard-layout-1` where the colorbar is the same size as the main axes and its width is one tenth of main axis width. + +.. figure:: layout/standard-layout-1.pdf + :width: 100% + + Image and colorbar + (sources :source:`layout/standard-layout-1.py`) + :label:`figure-standard-layout-1` + +**Standard layout 2** Using gridspec, the goal is to reproduce figure +:ref:`figure-standard-layout-2` with top and right histograms aligned with the main axes. + +.. figure:: layout/standard-layout-2.pdf + :width: 100% + + Scatter plot and histograms + (sources :source:`layout/standard-layout-2.py`) + :label:`figure-standard-layout-2` diff --git a/rst/main.rst b/rst/main.rst new file mode 100644 index 0000000..49b0178 --- /dev/null +++ b/rst/main.rst @@ -0,0 +1,97 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. include:: common.rst + +============ +Fundamentals +============ + +.. include:: anatomy.rst + +:raw-latex:`\clearchapter` + +.. include:: coordinates.rst + +:raw-latex:`\clearchapter` + +.. include:: projections.rst + +:raw-latex:`\clearchapter` + +.. include:: typography.rst +.. include:: colors.rst + +:raw-latex:`\clearchapter` + +============= +Figure design +============= +.. include:: rules.rst + +:raw-latex:`\clearchapter` + +.. include:: defaults.rst + +:raw-latex:`\clearchapter` + +.. include:: layout.rst + +:raw-latex:`\clearchapter` + +.. include:: ornaments.rst + +:raw-latex:`\clearchapter` + +================= +Advanced concepts +================= + +.. include:: animation.rst + +:raw-latex:`\clearchapter` + +.. include:: threed.rst + +:raw-latex:`\clearchapter` + +.. include:: optimization.rst + +:raw-latex:`\clearchapter` + +.. include:: beyond.rst + +:raw-latex:`\clearchapter` + + +================= +Showcase +================= + +.. include:: showcase.rst + + +:raw-latex:`\clearchapter` + +========== +Conclusion +========== + +.. include:: conclusion.rst + +:raw-latex:`\clearchapter` + +======== +Appendix +======== + +:raw-latex:`\appendix` + +.. include:: resources.rst + +:raw-latex:`\clearchapter` + +.. include:: cheatsheets.rst + diff --git a/rst/optimization.rst b/rst/optimization.rst new file mode 100644 index 0000000..b70aba7 --- /dev/null +++ b/rst/optimization.rst @@ -0,0 +1,314 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-optimization: + +Architecture & optimization +=========================== + +Even though a deep understanding of the matplotlib architecture is not +necessary to use it, it is nonetheless useful to know a bit of its +architecture to optimize either speed, memory and even +rendering. + +Transparency levels +------------------- + +We've already seen how to use transparency in a scatter plot to have +a perception of data density. This works reasonably well if you don't +have too much data. But what is too much exactly? It would be hard to +give a definitive limit because it depends on a number of parameters +such as the size of your figure, the shape and size of your markers +and the alpha level (i.e. transparency). For this latter, there is +actually a limit in how much transparent a color can be and it is +exactly 0.002 (= 1/500). This means that if you plot 500 black points +with a transparency of 0.002, you obtain get a quasi black marker as +shown on figure :ref:`figure-transparency`. + +.. figure:: optimization/transparency.pdf + :width: 100% + + Transparency levels + :label:`figure-transparency` + (sources: :source:`optimization/transparency.py`). + +It is not exactly black for 500 points because it also depends on how +alpha compositing is computed internally but it provides nonetheless a +useful approximation. Knowing this limit exists, it explains why you +get a solid color in dense areas when you have a lot of data. This is +illustrated on figure :ref:`figure-scatters` where the number of data +is respectively 10,000, 100,000 and 1,000,000. For 10,000 and 100,000 +points we can adapt the transparency level to show where are the dense +areas. In this case, this is simple normal distribution and we can +observe the central are is darker. For one million points, we reached +the limit of the transparency trick (alpha=0.002) and we now have a +central dark spot that hide information. + +.. figure:: optimization/scatters.png + :width: 100% + + Scatter, hist2d and hexbin + :label:`figure-scatters` + (sources: :source:`optimization/scatters.py`). + +This means we need a new strategy to display the data. Fortunately, +matplotlib provides `hist2d +`__ +and `hexbin +`__ +that will both aggregate points into bins (with square or hex +shape) that are eventually colored according to the number in the +bins. This allows to visualize density for any number of data points +and do not require to manipulate size and/or transparency of +markers. You're now ready to reproduce Todd W. Schneider's astonishing +visualization of NYC Taxi trips (`Analyzing 1.1 Billion NYC Taxi and +Uber Trips, with a Vengeance +`__). + +Alpha compositing induces other kind of problems with line plots, +especially when a plot is self-covering itself as exemplified of a a +high-frequency signal shown on figure :ref:`figure-multisample`. The +signal is a product of two sine waves of different frequency and +reads: + +.. code:: python + + xmin, xmax = 0*np.pi, 5*np.pi + ymin, ymax = -1.1, +1.1 + def f(x): return np.sin(np.power(x,3)) * np.sin(x) + X = np.linspace(xmin,xmax,10000) + Y = f(X) + +When we plot this signal, we can see that the density of lines becomes +higher and higher from left to right. Near the right side of the plot, +the frequency is the highest and is actually higher than the screen +resolution such that there is no empty spaces between successive +waves. However, when we use a regular plot (first line of figure +:ref:`figure-multisample`) with some transparency, we do not see a +change in color (while we could expect the plot to self-cover itself). +The reason is that matplotlib rendering engine takes care of not +overdrawing an area that belong to the same plot as shown on the figure +below: + +.. figure:: optimization/self-cover.pdf + :width: 100% + + Self-covering example + :label:`figure-self-cover` + (sources: :source:`optimization/self-cover.py`). + +This explains why we do not have color change in figure +:ref:`figure-multisample`. To counter this effect, we can render the +same plot using a line collection made of individual segments. In +such case, each segment is considered separately and will influence +other segments. This corresponds to the second line on the figure and +now we can observe a change in the color with darker colors on the +right suggesting a higher frequency. + +We can also adopt a totally different strategy by multisampling the +signal, which is a standard techniques in signal processing. Instead +of plotting the signal, I created an empty image with enough +resolution and for each point (pixel) of this image, I considered 8 +samples point randomly but closely distributed around the point to +decide of its value. This is of course a slower compared to a regular +plot but the rendering is more faithful to the signal as shown on the +third line. + +.. figure:: optimization/multisample.png + :width: 100% + + High-frequency signal. + :label:`figure-multisample` + (sources: :source:`optimization/multisample.py`). + + +Speed-up rendering +------------------ + +The overall speed of rendering a given figure depends on a number of +matplotlib internal factors that are good to know. Even though the +rendering speed is pretty decent in most cases, things can degrade +very noticeably when you have a large number of objects and we've been +already experienced such slowdown with the previous scatter plot +examples. You may have noticed that there are two ways to render a +scatter plot. Either you use the plot command with only markers or you +use the dedicated scatter command. The two methods are similar and yet +different. If you need a scatter plot where the size, shape and color +of markers are the same, then you can use the plot command that is +faster (by a factor of two approximately). For any other case, the +scatter command is the one to use. We can try to measure the time to +prepare a one million scatter plot using the following code: + +.. figure:: optimization/scatter-benchmark.png + :width: 100% + + Scatter benchmark + :label:`figure-scatter-benchmark` + (sources: :source:`optimization/scatter-benchmark.py`). + +By they way, you may have noticed the difference in size between plot +(`markersize=2`) and scatter (`s=2**2`). The reason is that the size +of marker in plot is measured in points while the size of markers in +scatter is measured in squared points. + + +In the case of line plots, the difference in rendering speed between +one solution or is the other can be dramatic as illustrated on figure +:ref:`figure-line-benchmark`. In this example, I drew 1,000 line +segments using 1,000 calls to the plot method (left), a single plot +call (middle) with individual segment coordinates separated by `None` +and a line collection (right). In this specific case, the choice of +the rendering method makes a big difference such that for a large +number of lines, your rendering can takes a few seconds or several +minutes. Note that the fastest rendering (unified plot, middle) is not +exactly equivalent to the others due to the absence of self-coverage +as explained previously. + +.. figure:: optimization/line-benchmark.png + :width: 100% + + Line benchmark + :label:`figure-line-benchmark` + (sources: :source:`optimization/line-benchmark.py`). + + +File sizes +---------- + +Depending on the format you save a figure, the resulting file can be +relatively small but it can also be huge, up to several megabytes and +this does not relate to the complexity of your script but rather to +the amount of details or the number of elements. Let's consider for +example the following script: + +.. code:: python + + plt.scatter(np.random.rand(n=int(1e6), np.random.rand(n=int(1e6)) + plt.savefig("vector.pdf") + +The resulting file size is approximately 15 megabytes. The reason for +such a large file being the pdf format to be a vector format. This +means that the coordinates of each point needs to be encoded. In our +example, we have a million points and two float coordinates per +points. If we consider a float to be represented by 4 bytes, we +already need 8,000,000 bytes to store coordinates. If we now add +individual color (4 bytes, RGBA ) and size (1 float, 4 bytes) we can +easily reached 16 megabytes. + +Let me now slightly modify the code: + +.. code:: python + + plt.scatter(np.random.rand(n=int(1e6), np.random.rand(n=int(1e6), + rasterized=True) + plt.savefig("vector.pdf", dpi=600) + +The new file size is approximately 50 kilobytes and the quality is +roughly equivalent even if it is not a pure vector format anymore. In +fact, the `rasterized` keyword means that maplotlib will create a +rasterized (i.e. bitmap) representation of the scatter plot saving a +lot of memory when saved on disk. Incidentally, it will also make the +rendering of your figure much faster because your pdf viewer does not +need to render individual elements. + +However, the combination of a vector format with rasterized elements is +not always the best choice. For example, if you need to produce a huge +figure (e.g. for a poster) with a very high definition, a pure vector +format might be the best format provided you do not have too much +elements. There's no definitive recipes and the choice is mostly a +matter of experience. + + +Multithread rendering +--------------------- + +Multithread rendering is not natively supported by matplotlib but it +is possible to do it anyway. The most obvious situation happens when +you need to render several different plots. In such a case, there's no +real difficulty and it's only matter of starting several threads +concurrently. What is more interesting is to produce a single figure +using multithread rendering. To do that, we need to split the figure +into different and non overlapping parts such that each part can be +rendered independently. Let's consider, for example, a figure whose +full extent is `xlim=[0,9]` and `ylim=[0,9]`. In such as case, we can +define quit easily 9 non-overlapping parts: + +.. code:: python + + X = np.random.normal(4.5, 2, 5_000_000) + Y = np.random.normal(4.5, 2, 5_000_000) + + extents = [[x,x+3,y,y+3] for x in range(0,9,3) + for y in range(0,9,3)] + +For each of these parts, we can plot an offline figure using a `Figure +Canvas +`_ +and save the result in an image: + +.. code:: python + + def plot(extent): + + xmin, xmax, ymin, ymax = extent + fig = Figure(figsize=(2,2)) + canvas = FigureCanvas(fig) + ax = fig.add_axes([0,0,1,1], frameon=False, + xlim = [xmin,xmax], xticks=[], + ylim = [ymin,ymax], yticks=[]) + epsilon = 0.1 + I = np.argwhere((X >= (xmin-epsilon)) & + (X <= (xmax+epsilon)) & + (Y >= (ymin-epsilon)) & + (Y <= (ymax+epsilon))) + ax.scatter(X[I], Y[I], 3, clip_on=False, + color="black", edgecolor="None", alpha=.0025) + canvas.draw() + return np.array(canvas.renderer.buffer_rgba()) + +Note that I took care of selecting X and Y that are inside the +provided extent (modulo epsilon). This is quite important because we +do not want to plot all the data in each subparts. Else, this would +slow down things. + +We can now put back every parts together using several imshow: + +.. code:: python + + from multiprocessing import Pool + + extents = [[x,x+3,y,y+3] for x in range(0,9,3) + for y in range(0,9,3)] + pool = Pool() + images = pool.map(plot, extents) + pool.close() + + fig = plt.figure(figsize=(6,6)) + ax = plt.subplot(xlim=[0,9], ylim=[0,9]) + for img, extent in zip(images, extents): + ax.imshow(img, extent=extent, interpolation="None") + + plt.show() + + +If you look at the result on figure +:ref:`figure-multithread-rendering`, you can observe a flawless +montage of the different pieces. If you set the epsilon value to zero, +you'll observe white spaces appearing between the different parts. The +reason is that if you enforce very strict clipping, a marker whose +center is outside extent will not be drawn while it may overlap because +of its size. + +.. figure:: optimization/multithread.png + :width: 100% + + Multithread rendering + :label:`figure-multithread-rendering` + (sources: :source:`optimization/multithread.py`). + +Such multithread rendering is not totally straightforward to implement +because it depends on the possibility to split your in segregated +elements. However, if you have a very complex plots that take several +minutes to render, this is an option worth to be explored. diff --git a/rst/ornaments.rst b/rst/ornaments.rst new file mode 100644 index 0000000..71ff794 --- /dev/null +++ b/rst/ornaments.rst @@ -0,0 +1,231 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-ornaments: + +Ornaments +========= + +Ornaments designate all the extra elements you can add to a figure to +beautify it or to make it clearer. Ornaments include standard elements +such as legend, annotation, colorbars, text but you can also design +your own element specifically for your figure. For example, figure +:ref:`figure-bessel-functions` displays a mix of standard elements +(annotation and text) as well as specific elements (roots reported on +the x axis using vertical markers). + +.. figure:: ornaments/bessel-functions.pdf + :width: 100% + + Bessel functions + :label:`figure-bessel-functions` + (sources: :source:`ornaments/bessel-functions.py`). + +There is no theoretical limit in the number of ornaments you can add +to a figure. However, you have to take care that your figure is still +easily readable and not too overloaded. + + +Legend +------ + +`Legends +`__ +are quite easy to use and only require for the user to name +plots. Matplotlib will take care of placing the legend automatically +(which is really tricky and consequently it can fail) and display the +necessary information (see figure +:ref:`figure-legend-regular`). Legend comes with several options that +allows you to control every aspect of the legend even if, most of the +time, a simple `legend()` call is sufficient as shown below: + +.. code:: python + + fig = plt.figure(figsize=(6,2.5)) + ax = plt.subplot() + + X = np.linspace(-np.pi, np.pi, 256, endpoint=True) + ax.plot(X, np.cos(X), label="cosine") + ax.plot(X, np.sin(X), label="sine") + ax.legend() + + plt.show() + +.. figure:: ornaments/legend-regular.pdf + :width: 100% + + Regular legend + :label:`figure-legend-regular` + (sources: :source:`ornaments/legend-regular.py`). + +.. figure:: ornaments/legend-alternatives.pdf + :width: 100% + + Alternative legends + :label:`figure-legend-alternatives` + (sources: :source:`ornaments/legend-alternatives.py`). + +However, there are some cases where legend might not be the most +appropriate way to add information. For example, when you have several +plots, it may become tedious for the reader to go back and forth +between plots and legend. An alternative way is to add information +directly on the plots as shown on figure +:ref:`figure-legend-alternatives`. + +This figure introduces four different ways to directly label plots +even though there is no best alternative because it really depends on +the data. For the displayed sine / cosine example (which is quite +simple) the four solutions are appropriate, but when used with real +data, it may happen that none of these alternative suits the data. In +such case, you might need an alternate way of labelling data or you +may need to split your figure into several plots. + +Title & labels +-------------- + +We've already manipulated title and labels using `set_title`, +`set_xlabel` and `set_ylabel` methods. When used without any extra +argument, they do a fairly good job and their placement is usually +good for most figures. Nonetheless, it is possible to play with the +various parameters such as to beautify the figure as shown on figure +:ref:`figure-title-regular`. In this example, I simply displaced the +labels in order to be closer to the axis and I took care of removing +the central tick that would have else collided with the label. For the +title, I simply moved it to the right and I moved the legend box +(using two columns) accordingly, that is, under the title. Nothing +complicated here but I think the result is visually more pleasant. + +.. figure:: ornaments/title-regular.pdf + :width: 100% + + Regular title + :label:`figure-title-regular` + (sources: :source:`ornaments/title-regular.py`). + +In some cases (e.g. conference poster), you may need to have titles to +be a little more eye catching like shown on figure +:ref:`figure-latex-text-box`. This is made possible by dividing +each axis with the `make_axes_locatable` method and to reserve 15% of +height for the actual title. In this figure, I also inserted a fully +justified text using Latex that may be considered as another form or +(advanced) ornament. + +.. figure:: ornaments/latex-text-box.png + :width: 100% + + Advanced text box + :label:`figure-latex-text-box` + (sources: :source:`ornaments/latex-text-box.py`). + + +Annotations +----------- + +Annotation is probably the most difficult object to handle inside +matplotlib. The reason is that it involves a number of different +concepts that results in a potential high number of parameters. +Furthermore, there is a supplementary difficulty because annotation +involve text whose size is expressed in points. In the end, you may +have to mix absolute or relative coordinates in pixels, points, +fractions or data units. If you add the fact that you can annotate any +axis having any kind of projection, you may now realize why the annotate +method offer so many parameters. + +The simplest way to annotate a figure is to add labels very close to +the points you want to annotate as shown on figure +:ref:`figure-annotation-direct`. In this figure, I took care of adding +a white outline to the labels such that they remain readable, +independently of the data distribution. However, if you have too many +points, all the different labels may end up cluttering your figure and +hide potentially important information. + +.. figure:: ornaments/annotation-direct.pdf + :width: 65% + + Direct annotations + :label:`figure-annotation-direct` + (sources: :source:`ornaments/annotation-direct.py`). + +An alternative is to push labels on the side of the figure and to use +broken lines to establish the link between the point and the label as +shown on figure :ref:`figure-annotation-side`. But this is far from +being automatic and to design this figure, I had to compute pretty +much everything. First, to have lines to not cross each other, I order +the point I want to label: + +.. code:: python + + X = np.random.normal(0, .35, 1000) + Y = np.random.normal(0, .35, 1000) + ax.scatter(X, Y, edgecolor="None", facecolor="C1", alpha=0.5) + + I = np.random.choice(len(X), size=5, replace=False) + Px, Py = X[I], Y[I] + I = np.argsort(Y[I])[::-1] + Px, Py = Px[I], Py[I] + + +From these points, I've been able to annotate them using a quite +complex connection style: + +.. code:: python + + y, dy = .25, 0.125 + style = "arc,angleA=-0,angleB=0,armA=-100,armB=0,rad=0" + for i in range(len(I)): + ax.annotate("Point " + chr(ord("A")+i), + xy = (Px[i], Py[i]), xycoords='data', + xytext = (1.25, y-i*dy), textcoords='data', + arrowprops=dict(arrowstyle="->", color="black", + linewidth=.75, + shrinkA=20, shrinkB=5, + patchA=None, patchB=None, + connectionstyle=style)) + +.. figure:: ornaments/annotation-side.pdf + :width: 75% + + Side annotations + :label:`figure-annotation-side` + (sources: :source:`ornaments/annotation-side.py`). + + +It is also possible to annotate objects outside the axes using a +`connection patch +`__ +as shown on figure :ref:`figure-annotation-zoom`. In this example, I +created several rectangles showing areas of interest around some +points and I created a connection to the corresponding zoomed +axes. Note how the connection starts on the outside of the rectangle, +which is one of the nice feature offered by annotation: you can +specify the nature of the object you want to annotate (by providing a +patch) and matplotlib will take care of having the origin of the +connection to the border of the patch. + + +.. figure:: ornaments/annotation-zoom.pdf + :width: 75% + + Zoomed annotations + :label:`figure-annotation-zoom` + (sources: :source:`ornaments/annotation-zoom.py`). + + +Exercise +-------- + +It is now your turn to experiment with ornaments by trying to +reproduce the figure :ref:`figure-elegant-scatter` which displays +several ornaments, including a custom one on the left and bottom +side. This ornament provides a quick way to show respective +distributions along weight and height and can be rendered with a +scatter plot using large vertical and horizontal markers. + +.. figure:: ornaments/elegant-scatter.pdf + :width: 100% + + Elegant scatter plot + :label:`figure-elegant-scatter` + (sources: :source:`ornaments/elegant-scatter.py`). diff --git a/rst/projections.rst b/rst/projections.rst new file mode 100644 index 0000000..6d7b612 --- /dev/null +++ b/rst/projections.rst @@ -0,0 +1,328 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: BSD +.. ---------------------------------------------------------------------------- +.. _chap-projection: + +Scales & projections +==================== + +Beyond affine transforms, matplotlib also offers advanced transformations that +allows to drastically change the representation of your data without ever +modifying it. Those transformations correspond to a data preprocessing stage +that allows you to adapt the rendering to the nature of your data. As explained +in the matplotlib documentation, there are two main families of transforms: +separable transformations, working on a single dimension, are called +:nameref:`scales`, and non-separable transformations, that handle data in two +or more dimensions at once are called :nameref:`projections`. + + +Scales +------ + +Scales provide a mapping mechanism between the data and their representation +in the figure along a given dimension. Matplotlib offers four different scales +(`linear`, `log`, `symlog` and `logit`) and takes care, for each of them, of +modifying the figure such as to adapt the ticks positions and labels (see +figure :ref:`figure-scales`). Note that a scale can be applied to x axis only +(`set_xscale`), y axis only (`set_yscale`) or both. + +.. figure:: scales-projections/scales-comparison.pdf + :width: 100% + + Comparison of the linear_, log_ and logit_ scales. + :label:`figure-scales` (sources: :source:`scales-projections/scales-comparison.py`). + +The default (and implicit) scale is linear_ and it is thus generally not +necessary to specify anything. You can check if a scale is linear by comparing +the distance between three points in the figure coordinates (actually we should +compare every points but you get the idea) and check whether their difference +in data space is the same as in figure space modulo a given factor (see +:source:`scales-projections/scales-check.py`): + +.. code:: python + + >>> fig = plt.figure(figsize=(6,6)) + >>> ax = plt.subplot(1, 1, 1, + aspect=1, xlim=[0,100], ylim=[0,100]) + >>> P0, P1, P2, P3 = (0.1, 0.1), (1,1), (10,10), (100,100) + >>> transform = ax.transData.transform + >>> print( (transform(P1)-transform(P0))[0] ) + 4.185 + >>> print( (transform(P2)-transform(P1))[0] ) + 41.85 + >>> print( (transform(P1)-transform(P0))[0] ) + 418.5 + +Logarithmic scale (log_) is a nonlinear scale where, instead of increasing in equal +increments, each interval is increased by a factor of the base of the logarithm +(hence the name). Log scales are used for values that are strictly positive since +the logarithm is undefined for negative and null values. If we apply the previous +script to check the difference in data and figure space, you can now see the +distances are the same: + +.. code:: python + + >>> fig = plt.figure(figsize=(6,6)) + >>> ax = plt.subplot(1, 1, 1, + aspect=1, xlim=[0.1,100], ylim=[0.1,100]) + >>> ax.set_xscale("log") + >>> P0, P1, P2, P3 = (0.1, 0.1), (1,1), (10,10), (100,100) + >>> transform = ax.transData.transform + >>> print( (transform(P1)-transform(P0))[0] ) + 155.0 + >>> print( (transform(P2)-transform(P1))[0] ) + 155.0 + >>> print( (transform(P1)-transform(P0))[0] ) + 155.0 + +If your data has negative values, you have to use a symmetric log scale (symlog_) +that is a composition of both a linear and a logarithmic scale. More precisely, +values around 0 use a linear scale and values outside the vicinity of zero uses +a logarithmic scale. You can of course specify the extent of the linear zone +when you set the scale. The logit_ scale is used for values in the range ]0,1[ +and uses a logarithmic scale on the "border" and a quasi-linear scale in the +middle (around 0.5). If none of these scales suit your needs, you still have +the option to define your own custom scale: + +.. code:: python + + def forward(x): + return x**(1/2) + def inverse(x): + return x**2 + + ax.set_xscale('function', functions=(forward, inverse)) + +In such case, you have to provide both the forward and inverse function that +allows to transform your data. The inverse function is used when displaying +coordinates under the mouse pointer. + +.. figure:: scales-projections/scales-custom.pdf + :width: 100% + + Custom (user defined) scales. + :label:`figure-scales-custom` (sources: :source:`scales-projections/scales-custom.py`). + +Finally, if you need a custom scale with complex transforms, you may need to +write a proper scale object as it is explained on the `matplotlib documentation +`_. + +Projections +----------- + +Projections are a bit more complex than scales but in the meantime much more +powerful. Projections allows you to apply arbitrary transformation to your data +before rendering them in a figure. There is no real limit on the kind of +transformation you can apply as long as you know how to transform your data into +something that will be 2 dimensional (the figure space) and reciprocally. In +other words, you need to define a forward and an inverse +transformation. Matplotlib comes with only a few standard projections but offers +all the machinery to create new domain-dependent projection such as for example +cartographic projection. You might wonder why there are so few native +projections. The answer is that it would be too time-consuming and too difficult +for the developers to implement and maintain each and every projections that +are domain specific. They chose instead to restrict projection to the most +generic ones, namely polar_ and 3d_. + +We've already seen the polar projection in the previous chapter. The most simple +and straightforward way to use is to specify the projection when you create an +axis: + +.. code:: Python + + ax = plt.subplot(1, 1, 1, projection='polar') + +This axis is now equipped with a polar projection. This means that any plotting +command you apply is pre-processed such as to apply (automatically) the forward +transformation on the data. In the case of a polar projection, the forward +transformation must specify how to go from polar coordinates :math:`(\rho, +\theta)` to Cartesian coordinates :math:`(x,y) = (\rho cos(\theta), \rho +sin(\theta))`. When you declare a polar axis, you can specify limits of the axis +as we've done previously but we have also some dedicated settings such as +`set_thetamin`, `set_thetamax`, `set_rmin`, `set_rmax` and more specifically +`set_rorigin`. This allows you to have fine control over what is actually shown as illustrated on the figure :ref:`figure-projection-polar-config`. + +.. figure:: scales-projections/projection-polar-config.pdf + :width: 100% + + Polar projection + :label:`figure-projection-polar-config` + (sources: :source:`scales-projections/projection-polar-config.py`). + + +If you now try to do some plots (e.g. plot, scatter, bar), you'll see that +everything is transformed but a few elements. More precisely, the shape of +markers is not transformed (a disc marker will remains a disc visually), the +text is not transformed (such that it remains readable) and the width of lines +is kept constant. Let's have a look at a more elaborate figure to see what it +means more precisely. On figure :ref:`figure-projection-polar-histogram`, I +plotted a simple signal using mostly `fill_between +`_ +command. The concentric grey/white colored rings are made using the +`fill_between` command between two different :math:`\rho` values while the +histogram is made with various :math:`\rho` values. If you now look more closely +at the :math:`\rho` axis with ticks ranging from 100 to 900, you can observe +that the ticks have the same vertical size. It is indeed an *anomaly* I +introduced deliberately for purely aesthetic reasons. If I had specified these +ticks using a plot command, the length of each tick would correspond to a +difference of angle (for the vertical size) and they would become taller and +taller as we move away from the center. To have regulars ticks, we thus have to +do some computations using the inverse transform (remember, a projection is a +forward and an inverse transform). I won't give all the details here but you can +read the code (:source:`projection-polar-histogram.py`) to see how it is +made. Note that the actual role of the inverse transformation is to link mouse +coordinates (in Cartesian 2D coordinates) back to your data. + +.. figure:: scales-projections/projection-polar-histogram.pdf + :width: 95% + + Polar projection with better defaults. + :label:`figure-projection-polar-histogram` + (sources: :source:`scales-projections/projection-polar-histogram.py`). + +Conversely, there are some situations were we might be interested in having +the text and the markers to be transformed as illustrated on figure +:ref:`figure-text-polar`. + +.. figure:: scales-projections/text-polar.pdf + :width: 90% + + Polar projection with transformation of text and markers. + :label:`figure-text-polar` + (sources: :source:`scales-projections/text-polar.py`). + +On this example, both the markers and the text have been transformed +manually. For the markers, the trick is to use `Ellipses +`_ that are +approximated as a sequence of small line segments, each of them being +transformed. In the corresponding code, I only specify the center, and the size +of the pseudo-marker and the pre-processing stage takes care of applying the +polar projection to each individual parts composing the marker (ellipse), +resulting in a slightly curved ellipse. For the text, the process is the same +but it is a bit more complicated since we need first to convert the text into a +path that can be transformed (we'll see that in more detail in the next +chapter). + + +The second projection that matplotlib offers is the 3d projection, that is the +projection from a 3D Cartesian space to a 2 Cartesian space. To start using 3D +projection, you'll need to use the `Axis3D +`_ toolkit that is generally +shipped with matplotlib: + +.. code:: python + + from mpl_toolkits.mplot3d import Axes3D + ax = plt.subplot(1, 1, 1, projection='3d') + +With this 3D axis, you can use regular plotting commands with a big difference +though: you need now to provide 3 coordinates (x,y,z) where you previously +provided only two (x,y) as illustrated on figure +:ref:`figure-projection-3d-frame`. Note that this figure is quite different from +the default 3D axis you may get from matplotlib. Here, I tweaked every settings +I can think of to try to improve the default look and to show how things can be +changed. Have a look at the corresponding code and try to modify some settings to +see the actual effect. The `3D Axis API +`_ is fairly well +documented on the matplotlib website and I won't explain each and every command. + +.. note:: **Note** The 3D axis projection is limited by the absence of a proper + `depth-buffer `_. This is not a + bug (nor a feature) and this results in some glitches between the elements + composing a figure. + +.. figure:: scales-projections/projection-3d-frame.pdf + :width: 80% + + Three dimensional projection + :label:`figure-projection-3d-frame` + (sources: :source:`scales-projections/projection-3d-frame.py`). + +For other type of projections, you'll need to install third-party packages +depending on the type of projection you intend to use: + +`Cartopy `_ + is a Python package + designed for geospatial data processing in order to produce maps and other + geospatial data analyses. Cartopy makes use of the powerful PROJ.4, NumPy and + Shapely libraries and includes a programmatic interface built on top of + Matplotlib for the creation of publication quality maps. + +`GeoPandas `_ + is an open source project to make working + with geospatial data in python easier. GeoPandas extends the data types used by + pandas to allow spatial operations on geometric types. Geometric operations + are performed by Shapely. Geopandas further depends on fiona for file access + and descartes and matplotlib for plotting. + +`Python-ternary `_ + is a plotting library for use with matplotlib to make ternary plots plots in + the two dimensional simplex projected onto a two dimensional plane. The + library provides functions for plotting projected lines, curves + (trajectories), scatter plots, and heatmaps. There are several examples and a + short tutorial below. + +`pySmithPlot `_ + is a matplotlib extension providing a projection class for creating high + quality Smith Charts with Python. The generated plots blend seamlessly into + matplotlib's style and support almost the full range of customization options. + +`Matplotlib-3D `_ + is an experimental project that attempts to provide a better and more + versatile 3d axis for Matplotlib. + + .. + .. figure:: scales-projections/geo-projections.pdf + :width: 100% + + Some of the standard and not so standard geographic projections + :label:`figure-geo-projections` (sources: :source:`scales-projections/geo-projections.py`). + +If you're still not satisfied with existing projections, your last option is to +create your own projection but this is quite an advanced operation even though +the matplotlib documentation provides some `examples +`_ + + +Exercises +--------- + +**Exercise 1** Considering functions :math:`f(x) = 10^x`, :math:`f(x) = x` and +:math:`f(x) = log_{10}(x)`, try to reproduce figure :ref:`figure-scales-log-log`. + +.. figure:: scales-projections/scales-log-log.pdf + :width: 100% + + Combining linear and logarithmic scales. + :label:`figure-scales-log-log` (sources: :source:`scales-projections/scales-log-log.py`). + + +**Exercise 2** The goal is to produce a figure showing `microphone +polar patterns +`__ +(omnidirectional, subcardioid, cardioid, supercardioid, bidirectional +and shotgun). The first five patterns are simple functions where +radius evolvse with angle while the last pattern may require some +works. + +.. figure:: scales-projections/polar-patterns.pdf + :width: 100% + + Microphone polar patterns + :label:`figure-polar-patterns` + (sources: :source:`scales-projections/polar-patterns.py`). + + + + +.. --- Links ------------------------------------------------------------------ +.. _linear: https://matplotlib.org/api/scale_api.html?#matplotlib.scale.LinearScale +.. _log: https://matplotlib.org/api/scale_api.html?#matplotlib.scale.LogScale +.. _symlog: https://matplotlib.org/api/scale_api.html?#matplotlib.scale.SymmetricalLogScale +.. _logit: https://matplotlib.org/api/scale_api.html?#matplotlib.scale.LogitScale +.. _polar: https://matplotlib.org/api/projections_api.html#module-matplotlib.projections.polar +.. _3d: https://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html +.. ---------------------------------------------------------------------------- + diff --git a/rst/resources.rst b/rst/resources.rst new file mode 100644 index 0000000..ac9323b --- /dev/null +++ b/rst/resources.rst @@ -0,0 +1,60 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-resources: + +External resources +================== + +One of the strength of matplotlib is the extensive documentation available from +the website. But there is also a large set of external resources that are +incredibly useful when you're looking to design a specific figure: + +* The matplotlib documentation is the primary source of information whenever + you need to know the parameters of this or that function. It has also a very + well written set of tutorials that are worth to be read: + + - The `starter guide `_ helps you to get started with Matplotlib + - The `pyplot tutorial `_ is an introduction to the `pyplot` interface + - The `image tutorial `_ explains the basic of image display + - The `catalogue `_ shows some neat and standard visualizations + - The `frequently asked questions `__ are worth a read + + +* Online help is available from two mailing lists (users and + developers) and from stack overflow which has a dedicated matplotlib + tag and a lot of questions have been asked and answered: + + - The `matplotlib users mailing list `_ + - The `matplotlib developers mailing lists `_ + - `Stack overflow `__ (more than 40,000 questions related to matplotlib) + + +* There exist various galleries online among which the matplotlib gallery that + is probably the best gallery to start with. The other galleries are worth a + look because they explain the rationale for each kind of plot: + + - The `Matplotlib Gallery`_ is probably the best resource if you're looking + for how to do a specific figure. Just search for the closest example and + get the code. + - The `Data Visualization Catalogue`_ is a project developed by Severino + Ribecca to create a library of different information visualisation types. + - The `Python Graph gallery`_ displays hundreds of charts and aims to + showcase the awesome dataviz possibilities of Python. + - The `R Graph Gallery`_ is a collection of charts made displayed in several + sections, always with their (R) reproducible code available. + +* Finally, there exists many books about matplotlib and data visualization. I + selected only a few of them that are well written and open access. You can + find them in the :nameref:`chap-bibliography` appendix. + + + +.. --- Links ------------------------------------------------------------------ +.. _Matplotlib Gallery: https://matplotlib.org/gallery.html +.. _Data Visualization Catalogue: https://datavizcatalogue.com/ +.. _Python Graph Gallery: https://python-graph-gallery.com/ +.. _R Graph Gallery: https://www.r-graph-gallery.com/ +.. ---------------------------------------------------------------------------- diff --git a/rst/rules.rst b/rst/rules.rst new file mode 100644 index 0000000..b11bcb6 --- /dev/null +++ b/rst/rules.rst @@ -0,0 +1,441 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-rules: + +Ten simple rules +================ + +.. note:: + + **Note.** This chapter is an article I co-authored with Michael Droettboom + and Philip E. Bourne. It has been published in `PLOS Computational Biology + `_ under a Creative Commons + CC0 public domain dedication in September 2014. It is five years old but + still relevant and very popular. + +Scientific visualization is classically defined as the process of graphically +displaying scientific data. However, this process is far from direct or +automatic. There are so many different ways to represent the same data: scatter +plots, linear plots, bar plots, and pie charts, to name just a +few. Furthermore, the same data, using the same type of plot, may be perceived +very differently depending on who is looking at the figure. A more accurate +definition for scientific visualization would be a graphical interface between +people and data. In this short article, we do not pretend to explain everything +about this interface. Instead we aim to provide a basic set of rules to improve +figure design and to explain some of the common pitfalls. + +Rule 1: Know Your Audience +-------------------------- + +Given the definition above, problems arise when how a visual is perceived +differs significantly from the intent of the conveyer. Consequently, it is +important to identify, as early as possible in the design process, the audience +and the message the visual is to convey. The graphical design of the visual +should be informed by this intent. If you are making a figure for yourself and +your direct collaborators, you can possibly skip a number of steps in the +design process, because each of you knows what the figure is about. However, if +you intend to publish a figure in a scientific journal, you should make sure +your figure is correct and conveys all the relevant information to a broader +audience. Student audiences require special care since the goal for that +situation is to explain a concept. In that case, you may have to add extra +information to make sure the concept is fully understood. Finally, the general +public may be the most difficult audience of all since you need to design a +simple, possibly approximated, figure that reveals only the most salient part +of your research (Figure :ref:`fig-rule-1`). This has proven to be a difficult +exercise. + +.. figure:: rules/rule-1.pdf + :width: 100% + + **Know your audience.** This is a remake of a figure that was originally + published in the New York Times (NYT) in 2007. This new figure was made with + matplotlib using approximated data. The data is made of four series (men + deaths/cases, women deaths/cases) that could have been displayed using + classical double column (deaths/cases) bar plots. However, the layout used + here is better for the intended audience. It exploits the fact that the + number of new cases is always greater than the corresponding number of + deaths to mix the two values. It also takes advantage of the reading + direction (English [left-to-right] for NYT) in order to ease comparison + between men and women while the central labels give an immediate access to + the main message of the figure (cancer). This is a self-contained figure + that delivers a clear message on cancer deaths. However, it is not + precise. The chosen layout makes it actually difficult to estimate the + number of kidney cancer deaths because of its bottom position and the + location of the labelled ticks at the top. While this is acceptable for a + general-audience publication, it would not be acceptable in a scientific + publication if actual numerical values were not given elsewhere in the + article. (:source:`rules/rule-1.py`). :label:`fig-rule-1` + + +Rule 2: Identify Your Message +----------------------------- + +A figure is meant to express an idea or introduce some facts or a result that +would be too long (or nearly impossible) to explain only with words, be it for +an article or during a time-limited oral presentation. In this context, it is +important to clearly identify the role of the figure, i.e., what is the +underlying message and how can a figure best express this message? Once clearly +identified, this message will be a strong guide for the design of the figure, +as shown in Figure :ref:`fig-rule-2`. Only after identifying the message will +it be worth the time to develop your figure, just as you would take the time to +craft your words and sentences when writing an article only after deciding on +the main points of the text. If your figure is able to convey a striking +message at first glance, chances are increased that your article will draw more +attention from the community. + +.. figure:: rules/rule-2.pdf + :width: 100% + + **Identify your message.** The superior colliculus (SC) is a brainstem + structure at the crossroads of multiple functional pathways. Several + neurophysiological studies suggest that the population of active neurons in + the SC encodes the location of a visual target that induces saccadic eye + movement. The projection from the retina surface (on the left) to the + collicular surface (on the right) is based on a standard and quantitative + model in which a logarithmic mapping function ensures the projection from + retinal coordinates to collicular coordinates. This logarithmic mapping + plays a major role in saccade decision. To better illustrate this role, an + artificial checkerboard pattern has been used, even though such a pattern is + not used during experiments. This checkerboard pattern clearly demonstrates + the extreme magnification of the foveal region, which is the main message of + the figure. (:source:`rules/rule-2.py`) :label:`fig-rule-2` + + +Rule 3: Adapt the Figure to the Support Medium +---------------------------------------------- + +A figure can be displayed on a variety of media, such as a poster, a computer +monitor, a projection screen (as in an oral presentation), or a simple sheet of +paper (as in a printed article). Each of these media represents different +physical sizes for the figure, but more importantly, each of them also implies +different ways of viewing and interacting with the figure. For example, during +an oral presentation, a figure will be displayed for a limited time. Thus, the +viewer must quickly understand what is displayed and what it represents while +still listening to your explanation. In such a situation, the figure must be +kept simple and the message must be visually salient in order to grab +attention, as shown in Figure :ref:`fig-rule-3`. It is also important to keep +in mind that during oral presentations, figures will be video-projected and +will be seen from a distance, and figure elements must consequently be made +thicker (lines) or bigger (points, text), colors should have strong contrast, +and vertical text should be avoided, etc. For a journal article, the situation +is totally different, because the reader is able to view the figure as long as +necessary. This means a lot of details can be added, along with complementary +explanations in the caption. If we take into account the fact that more and +more people now read articles on computer screens, they also have the +possibility to zoom and drag the figure. Ideally, each type of support medium +requires a different figure, and you should abandon the practice of extracting +a figure from your article to be put, as is, in your oral presentation. + +.. figure:: rules/rule-3.pdf + :width: 100% + + **Adapt the figure to the support medium** These two figures represent the + same simulation of the trajectories of a dual-particle system + (:math:`\dot{x} = (1/4 + (x-y))(1-x)`, :math:`x \ge 0`, :math:`\dot{y} = + (1/4 + (y-x))(1-y)`, :math:`y \ge 0`) where each particle interacts with the + other. Depending on the initial conditions, the system may end up in three + different states. The left figure has been prepared for a journal article + where the reader is free to look at every detail. The red color has been + used consistently to indicate both initial conditions (red dots in the + zoomed panel) and trajectories (red lines). Line transparency has been + increased in order to highlight regions where trajectories overlap (high + color density). The right figure has been prepared for an oral + presentation. Many details have been removed (reduced number of + trajectories, no overlapping trajectories, reduced number of ticks, bigger + axis and tick labels, no title, thicker lines) because the time-limited + display of this figure would not allow for the audience to scrutinize every + detail. Furthermore, since the figure will be described during the oral + presentation, some parts have been modified to make them easier to reference + (e.g., the yellow box, the red dashed line). (:source:`rules/rule-3.py`) + :label:`fig-rule-3` + + +Rule 4: Captions Are Not Optional +--------------------------------- + +Whether describing an experimental setup, introducing a new model, or +presenting new results, you cannot explain everything within the figure +itself—a figure should be accompanied by a caption. The caption explains how to +read the figure and provides additional precision for what cannot be +graphically represented. This can be thought of as the explanation you would +give during an oral presentation, or in front of a poster, but with the +difference that you must think in advance about the questions people would +ask. For example, if you have a bar plot, do not expect the reader to guess the +value of the different bars by just looking and measuring relative heights on +the figure. If the numeric values are important, they must be provided +elsewhere in your article or be written very clearly on the figure. Similarly, +if there is a point of interest in the figure (critical domain, specific point, +etc.), make sure it is visually distinct but do not hesitate to point it out +again in the caption. + + +Rule 5: Do Not Trust the Defaults +--------------------------------- + +Any plotting library or software comes with a set of default settings. When the +end-user does not specify anything, these default settings are used to specify +size, font, colors, styles, ticks, markers, etc. (Figure :ref:`fig-rule-5`). +Virtually any setting can be specified, and you can usually recognize the +specific style of each software package (Matlab, Excel, Keynote, etc.) or +library (LaTeX, matplotlib, gnuplot, etc.) thanks to the choice of these +default settings. Since these settings are to be used for virtually any type of +plot, they are not fine-tuned for a specific type of plot. In other words, they +are good enough for any plot but they are best for none. All plots require at +least some manual tuning of the different settings to better express the +message, be it for making a precise plot more salient to a broad audience, or +to choose the best colormap for the nature of the data. For example, see +chapter chap-defaults_ for how to go from the default settings to a nicer +visual in the case of the matplotlib library. + +.. figure:: rules/rule-5.pdf + :width: 100% + + **Do not trust the defaults.** The left panel shows the sine and cosine + functions as rendered by matplotlib using default settings. While this + figure is clear enough, it can be visually improved by tweaking the various + available settings, as shown on the right + panel. (:source:`rules/rule-5-left.py` and :source:`rules/rule-5-right.py`) + :label:`fig-rule-5` + + +Rule 6: Use Color Effectively +----------------------------- + +Color is an important dimension in human vision and is consequently equally +important in the design of a scientific figure. However, as explained by Edward +Tufte, color can be either your greatest ally or your worst enemy if not used +properly. If you decide to use color, you should consider which colors to use +and where to use them. For example, to highlight some element of a figure, you +can use color for this element while keeping other elements gray or black. This +provides an enhancing effect. However, if you have no such need, you need to +ask yourself, “Is there any reason this plot is blue and not black?” If you +don't know the answer, just keep it black. The same holds true for +colormaps. Do not use the default colormap (e.g., jet or rainbow) unless there +is an explicit reason to do so (see Figure :ref:`fig-rule-6`). Colormaps are +traditionally classified into three main categories: + +* **Sequential**: one variation of a unique color, used for quantitative data + varying from low to high. +* **Diverging**: variation from one color to another, used to highlight deviation + from a median value. +* **Qualitative**: rapid variation of colors, used mainly for discrete or + categorical data. + +Use the colormap that is the most relevant to your data. Lastly, avoid using +too many similar colors since color blindness may make it difficult to discern +some color differences. + +.. figure:: rules/rule-6.pdf + :width: 100% + + **Use color effectively.** This figure represents the same signal, whose + frequency increases to the right and intensity increases towards the bottom, + using three different colormaps. The rainbow colormap (qualitative) and the + seismic colormap (diverging) are equally bad for such a signal because they + tend to hide details in the high frequency domain (bottom-right part). Using + a sequential colormap such as the purple one, it is easier to see details in + the high frequency domain. (:source:`rules/rule-6.py`) :label:`fig-rule-6` + + +Rule 7: Do Not Mislead the Reader +--------------------------------- + +What distinguishes a scientific figure from other graphical artwork is the +presence of data that needs to be shown as objectively as possible. A +scientific figure is, by definition, tied to the data (be it an experimental +setup, a model, or some results) and if you loosen this tie, you may +unintentionally project a different message than intended. However, +representing results objectively is not always straightforward. For example, a +number of implicit choices made by the library or software you're using that +are meant to be accurate in most situations may also mislead the viewer under +certain circumstances. If your software automatically re-scales values, you +might obtain an objective representation of the data (because title, labels, +and ticks indicate clearly what is actually displayed) that is nonetheless +visually misleading (see bar plot in Figure :ref:`fig-rule-7`); you have +inadvertently misled your readers into visually believing something that does +not exist in your data. You can also make explicit choices that are wrong by +design, such as using pie charts or 3-D charts to compare quantities. These two +kinds of plots are known to induce an incorrect perception of quantities and it +requires some expertise to use them properly. As a rule of thumb, make sure to +always use the simplest type of plots that can convey your message and make +sure to use labels, ticks, title, and the full range of values when +relevant. Lastly, do not hesitate to ask colleagues about their interpretation +of your figures. + +.. figure:: rules/rule-7.pdf + :width: 100% + + **Do not mislead the reader.** On the left part of the figure, we + represented a series of four values: 30, 20, 15, 10. On the upper left part, + we used the disc area to represent the value, while in the bottom part we + used the disc radius. Results are visually very different. In the latter + case (red circles), the last value (10) appears very small compared to the + first one (30), while the ratio between the two values is only 3∶1. This + situation is actually very frequent in the literature because the command + (or interface) used to produce circles or scatter plots (with varying point + sizes) offers to use the radius as default to specify the disc size. It thus + appears logical to use the value for the radius, but this is misleading. On + the right part of the figure, we display a series of ten values using the + full range for values on the top part (y axis goes from 0 to 100) or a + partial range in the bottom part (y axis goes from 80 to 100), and we + explicitly did not label the y-axis to enhance the confusion. The visual + perception of the two series is totally different. In the top part (black + series), we tend to interpret the values as very similar, while in the + bottom part, we tend to believe there are significant differences. Even if + we had used labels to indicate the actual range, the effect would persist + because the bars are the most salient information on these + figures. (:source:`rules/rule-7.py`) :label:`fig-rule-7` + + +Rule 8: Avoid "Chartjunk" +------------------------- + +Chartjunk refers to all the unnecessary or confusing visual elements found in a +figure that do not improve the message (in the best case) or add confusion (in +the worst case). For example, chartjunk may include the use of too many colors, +too many labels, gratuitously colored backgrounds, useless grid lines, +etc. (see left part of Figure :ref:`fig-rule-8`). The term was first coined by +Edward Tutfe, in which he argues that any decorations that do not tell the +viewer something new must be banned: "Regardless of the cause, it is all +non-data-ink or redundant data-ink, and it is often chartjunk." Thus, in order +to avoid chartjunk, try to save ink, or electrons in the computing era. Stephen +Few reminds us that graphs should ideally "represent all the data that is +needed to see and understand what's meaningful." However, an element that could +be considered chartjunk in one figure can be justified in another. For example, +the use of a background color in a regular plot is generally a bad idea because +it does not bring useful information. However, in the right part of Figure 7, +we use a gray background box to indicate the range [−1,+1] as described in the +caption. If you're in doubt, do not hesitate to consult the excellent blog of +Kaiser Fung, which explains quite clearly the concept of chartjunk through the +study of many examples. + + +.. figure:: rules/rule-8.pdf + :width: 100% + + **Avoid chartjunk.** We have seven series of samples that are equally + important, and we would like to show them all in order to visually compare + them (exact signal values are supposed to be given elsewhere). The left + figure demonstrates what is certainly one of the worst possible designs. All + the curves cover each other and the different colors (that have been badly + and automatically chosen by the software) do not help to distinguish + them. The legend box overlaps part of the graphic, making it impossible to + check if there is any interesting information in this area. There are far + too many ticks: x labels overlap each other, making them unreadable, and the + three-digit precision does not seem to carry any significant + information. Finally, the grid does not help because (among other + criticisms) it is not aligned with the signal, which can be considered + discrete given the small number of sample points. The right figure adopts a + radically different layout while using the same area on the sheet of + paper. Series have been split into seven plots, each of them showing one + series, while other series are drawn very lightly behind the main + one. Series labels have been put on the left of each plot, avoiding the use + of colors and a legend box. The number of x ticks has been reduced to three, + and a thin line indicates these three values for all plots. Finally, y ticks + have been completely removed and the height of the gray background boxes + indicate the [−1,+1] range (this should also be indicated in the figure + caption if it were to be used in an article). (:source:`rules/rule-8.py`) + :label:`fig-rule-8` + + +Rule 9: Message Trumps Beauty +----------------------------- + +Figures have been used in scientific literature since antiquity. Over the +years, a lot of progress has been made, and each scientific domain has +developed its own set of best practices. It is important to know these +standards, because they facilitate a more direct comparison between models, +studies, or experiments. More importantly, they can help you to spot obvious +errors in your results. However, most of the time, you may need to design a +brand-new figure, because there is no standard way of describing your +research. In such a case, browsing the scientific literature is a good starting +point. If some article displays a stunning figure to introduce results similar +to yours, you might want to try to adapt the figure for your own needs (note +that we did not say copy; be careful with image copyright). If you turn to the +web, you have to be very careful, because the frontiers between data +visualization, infographics, design, and art are becoming thinner and thinner +[9]. There exists a myriad of online graphics in which aesthetic is the first +criterion and content comes in second place. Even if a lot of those graphics +might be considered beautiful, most of them do not fit the scientific +framework. Remember, in science, message and readability of the figure is the +most important aspect while beauty is only an option, as dramatically shown in +Figure :ref:`fig-rule-9`. + +.. figure:: rules/rule-9.pdf + :width: 100% + + **Message trumps beauty.** This figure is an extreme case where the message + is particularly clear even if the aesthetic of the figure is + questionable. The uncanny valley is a well-known hypothesis in the field of + robotics that correlates our comfort level with the human-likeness of a + robot. To express this hypothetical nature, hypothetical data were used + (:math:`y=x^2 - 5e^{-5(x-2)^2}`) and the figure was given a sketched look + (xkcd filter on matplotlib) associated with a cartoonish font that enhances + the overall effect. Tick labels were also removed since only the overall + shape of the curve matters. Using a sketch style conveys to the viewer that + the data is approximate, and that it is the higher-level concepts rather + than low-level details that are important. (:source:`rules/rule-9.py`) + :label:`fig-rule-9` + + +Rule 10: Get the Right Tool +--------------------------- + +There exist many tools that can make your life easier when creating figures, +and knowing a few of them can save you a lot of time. Depending on the type of +visual you're trying to create, there is generally a dedicated tool that will +do what you're trying to achieve. It is important to understand at this point +that the software or library you're using to make a visualization can be +different from the software or library you're using to conduct your research +and/or analyze your data. You can always export data in order to use it in +another tool. Whether drawing a graph, designing a schema of your experiment, +or plotting some data, there are open-source tools for you. They're just +waiting to be found and used. Below is a small list of open-source tools. + +* **Matplotlib** is a python plotting library, primarily for 2-D plotting, but + with some 3-D support, which produces publication-quality figures in a + variety of hardcopy formats and interactive environments across platforms. It + comes with a huge `gallery `__ of + examples that cover virtually all scientific domains. + +* **R** is a language and environment for statistical computing and graphics. R + provides a wide variety of statistical (linear and nonlinear modeling, + classical statistical tests, time-series analysis, classification, + clustering, etc.) and graphical techniques, and is highly extensible. + +* **Inkscape** is a professional vector graphics editor. It allows you to + design complex figures and can be used, for example, to improve a + script-generated figure or to read a PDF file in order to extract figures and + transform them any way you like. + +* **TikZ and PGF** are TeX packages for creating graphics + programmatically. TikZ is built on top of PGF and allows you to create + sophisticated graphics in a rather intuitive and easy manner, as shown by the + `Tikz gallery `__. + +* **GIMP** is the GNU Image Manipulation Program. It is an application for such + tasks as photo retouching, image composition, and image authoring. If you + need to quickly retouch an image or add some legends or labels, GIMP is the + perfect tool. + +* **ImageMagick** is a software suite to create, edit, compose, or convert + bitmap images from the command line. It can be used to quickly convert an + image into another format, and the huge `script gallery + `__ by Fred Weinhaus will + provide virtually any effect you might want to achieve. + +* **D3.js** (or just D3 for Data-Driven Documents) is a JavaScript library that + offers an easy way to create and control interactive data-based graphical + forms which run in web browsers, as shown in the `gallery + `__. + +* **Cytoscape** is a software platform for visualizing complex networks and + integrating these with any type of attribute data. If your data or results + are very complex, cytoscape may help you alleviate this complexity. + +* **Circos** was originally designed for visualizing genomic data but can + create figures from data in any field. Circos is useful if you have data that + describes relationships or multilayered annotations of one or more scales. diff --git a/rst/showcase.rst b/rst/showcase.rst new file mode 100644 index 0000000..8b8e64f --- /dev/null +++ b/rst/showcase.rst @@ -0,0 +1,222 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-showcase: + +Showtime. + +:raw-latex:`\fullpagefigure{showcases/contour-dropshadow.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Filled contours with dropshadows}` + +**Filled contours with dropshadows** is a nice effet that allows you +to use a sequential colormap (here Viridis) while distinguishing +positive and negative values. If you look closely at the figure, you +can see that the drop shadow is external for positive values and +internal for negative values. To achieve this result, the contours +need to be rendered individually twice. In the first pass, I render a +contour offscreen and read the resulting image into an array. I then +use a Gaussian filter to blur it a bit and transform the image to make +it full black but the alpha channel. I can then display the image +using imshow and it will gracefully blend with elements already +presents in the figure. I then add the same contour using the colormap +and I iterate the process. The trick is to start from bottom to top +such that dropshadows remain visible. + +**Sources:** :source:`showcases/contour-dropshadow.py` + + +:raw-latex:`\fullpagefigure{showcases/domain-coloring.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Domain coloring}` + +**Domain coloring** is a "technique for visualizing complex functions +by assigning a color to each point of the complex plane. By assigning +points on the complex plane to different colors and brightness, domain +coloring allows for a four dimensional complex function to be easily +represented and understood. This provides insight to the fluidity of +complex functions and shows natural geometric extensions of real +functions" `[Wikipedia] +`__. On the figure on +the left, I represented the imaginary function :math:`z + +\nicefrac{1}{z}` in the domain :math:`[-2.5, 2.5]^2`. I used the angle +of the (complex) result for setting the color and the absolute cosine +the norm for modulating it periodically. I could have used a cyclic +colormap such as `twilight` but I think the `Spectral` is visually +more pleasant, even though it induces some discontinuities. To draw +the grid, I used a contour plot using integer values in the real and +imaginary domain. + +**Sources:** :source:`showcases/domain-coloring.py` + +:raw-latex:`\fullpagefigure{showcases/escher.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Escher like projections}` + +**Escher like projections** can be obtained using the complex exponential +function and some specific periods that can be computed quite +easily. To do the figure on the left, I mostly followed explanations +given by Craig S. Kaplan, professor at the University of Waterloo on +his blog post `Escher-like Spiral Tilings (2019) +`_. The only +difficulty in making this figure is line thickness. If you compare this +figure with the previous one, you may have noticed that in the +previous figure, lines have a constant thickness while in this figure, +thickness varies. To achieve such effect, we have to use a polygon +made of several segment with varying thickness. This poses no real +difficulty, only some geometrical computations. + +**Sources:** :source:`showcases/escher.py` + + +:raw-latex:`\fullpagefigure{showcases/VSOM.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Self-organizing maps}` + +**Self-organizing map** (SOM) "is a type of artificial neural network +that is trained using unsupervised learning to produce a +low-dimensional (typically two-dimensional), discretized +representation of the input spa-ce of the training samples, called a +map, and is therefore a method to do dimensionality reduction" +`[Wikipedia] `_. I +developed with Georgios Detorakis a `randomized self-organizing map +`_ based on blue noise +distribution of neurons. The figure on the left shows the +self-organisation of the map when fed with random RGB colors. The +figure itself is made of a polygon collection where each polygon is +painted with the color of the neuron. No real difficuly but I had to +take care fo disabling antialias, else, thin lines appear +between polygons. + +**Sources:** `github.com/rougier/VSOM `__ + + +:raw-latex:`\fullpagefigure{showcases/waterfall-3d.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Waterfall plots}` + +**Waterfall plot** "is a three-dimensional plot in which multiple +curves of data, typically spectra, are displayed +simultaneously. Typically the curves are staggered both across the +screen and vertically, with 'nearer' curves masking the ones +behind. The result is a series of "mountain" shapes that appear to be +side by side. The waterfall plot is often used to show how +two-dimensional information changes over time or some other variable" +`[Wikipedia] `__ To do +the figure, I used a 3D axis and polygons (i.e. not filled plot). The +reason to use polygon is to obtain the color gradient effect on each +curve. The only way to do that (to the best of my knowledge), is to +slice horizontally each curve in several stripes and to render the +slice using a specific color. The difficulty is to compute those +irregular slices and this is the reason I use the `Shapely library +`_ that allows, among many other +things, to compute the intersection between polygons. + +**Sources:** :source:`showcases/waterfall-3d.py` + + +:raw-latex:`\fullpagefigure{showcases/windmap.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Streamlines}` + +**Streamlines** are a "family of curves that are instantaneously +tangent to the velocity vector of the flow. These show the direction +in which a massless fluid element will travel at any point in time" +`[Wikipedia] +`__. The +figure on the left shows such stream lines and is actually a still +from an animation. Each streamline has been split into line segments +and gathered in a line collection such that each segment has its own +color. From there, it is easy to suggest stream direction using +gradients. Note that I could have used a single line collection for +all streamlines. Strangely enough, the only difficulty in this figure +are the line round caps. For the reason explained `here +`_, I had to create a +specific graphic context such as to have round caps. + +**Sources:** :source:`showcases/windmap.py` + + +:raw-latex:`\fullpagefigure{showcases/mandelbrot.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Mandelbrot set}` + +The **Mandelbrot set** "is the set of complex numbers :math:`c`for +which the function :math:`f_{c}(z) = z^{2} + c` does not diverge when +iterated from :math:`z = 0`, i.e., for which the sequence +:math:`f_{c}(0)`, :math:`f_{c}(f_{c}(0))`, etc., remains bounded in +absolute value `[Wikipedia] +`__. To plot the figure +on the left, I used a regular imshow with shading and normalized +recounts that is explained on this post `Smooth Shading for the +Mandelbrot Exterior +`__. The script is +also present in the matplotlib gallery which I contributed some years +ago. + +**Sources:** :source:`showcases/mandelbrot.py` + + +:raw-latex:`\fullpagefigure{showcases/recursive-voronoi.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Recursive Voronoi}` + +This **recursive Voronoi set** has been quite painful to design +because it requires some quite precise settings to obtain what I think +is a beautiful result. These settings are the placement of random +points with good visual properties and for that, I use the `Fast +Poisson Disk Sampling +`__ +by Robert Bridson which is simple and fast. I also use quite +extensively the shapely library to clip he different polygons and I +discovered in the meantime how to draw random points inside a +polygon. Finally, I played with lines thickness, polygons color and +transparency to achieve this result, involving 5 levels of +recursion. On my computer, it takes around 1 minute to compute. + +**Sources:** :source:`showcases/recursive-voronoi.py` + +:raw-latex:`\fullpagefigure{showcases/elevation.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{3D Heightmap}` + +A **3D heightmap** of Mount St Helens after it exploded. This has been +made with my `experimental 3D axis +`__. Nothing really +complicated here, just a bit slow because it needs to sort a bunch of +triangles. + + +:raw-latex:`\fullpagefigure{showcases/mosaic.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Voronoi mosaic}` + +This **Voronoi mosaic** is based on blue noise distribution where each +Voronoi cell has been painted according to the color of the center of +the Voronoi cell in the original image. This results in a cheap +stained glass window effect. + +**Sources:** :source:`showcases/mosaic.py` + + +:raw-latex:`\fullpagefigure{showcases/text-shadow.png}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Text shadow}` + +This **shadowed text** is harder to design than it seems. I started +from a text path object and iterated over the segments composing the +path in order to create sheared rectangles that constitute the shadow. To +make the shadow disappear in the background, I created an image with a +vertical gradient using semi-transparent color (fully transparent on +top and fully opaque on the bottom). This results in a nice fading +shadow effect. + +**Sources:** :source:`showcases/text-shadow.py` + + +:raw-latex:`\fullpagefigure{showcases/text-spiral.pdf}` +:raw-latex:`\phantomsection \addcontentsline{toc}{chapter}{Text spiral}` + + +This **spiral text** has been made using an `Archimedean spiral +`__ (:math:`r = +a + b\theta`) that guarantees a constant speed along a line that +rotates with constant angular velocity. Said differently, successive +turnings of the spiral have a constant separation distance. Starting +from a very long text path representing some of the decimals of pi +(using the `mpmath `__ +library), it's then only a matter of transforming the vertices to +follow the spiral. + +**Sources:** :source:`showcases/text-spiral.py` diff --git a/rst/threed.rst b/rst/threed.rst new file mode 100644 index 0000000..a2e2793 --- /dev/null +++ b/rst/threed.rst @@ -0,0 +1,364 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-3D: + +Going 3D +======== + +Matplotlib has a really nice `3D interface +`_ with +many capabilities (and some limitations) that is quite popular among +users. Yet, 3D is still considered to be some kind of black magic for +some users (or maybe for the majority of users). I would thus like to +explain in this chapter that 3D rendering is really easy once you've +understood a few concepts. To demonstrate that, we'll render the bunny +on figure above with 60 lines of Python and one matplotlib call. That +is, without using the 3D axis. + + +.. figure:: threed/bunny.pdf + :width: 100% + + Stanford bunny. + (sources: :source:`threed/bunny.py`). + :label:`figure-bunny` + + +Loading the bunny +----------------- + +First things first, we need to load our model. We'll use a simplified +version of the `Stanford bunny +`_. The file uses the +`wavefront format +`_ which is one of +the simplest format, so let's make a very simple (but error-prone) +loader that will just do the job for this specific model. Else, you +can use the `meshio library `_ + +.. code:: python + + V, F = [], [] + with open("bunny.obj") as f: + for line in f.readlines(): + if line.startswith('#'): continue + values = line.split() + if not values: continue + if values[0] == 'v': + V.append([float(x) for x in values[1:4]]) + elif values[0] == 'f': + F.append([int(x) for x in values[1:4]]) + V, F = np.array(V), np.array(F)-1 + +`V` is now a set of vertices (3D points if you prefer) and `F` is a set of +faces (= triangles). Each triangle is described by 3 indices relatively to the +vertices array. Now, let's normalize the vertices such that the overall bunny +fits the unit box: + +.. code:: python + + V = (V-(V.max(0)+V.min(0))/2)/max(V.max(0)-V.min(0)) + + +Now, we can have a first look at the model by getting only the x,y +coordinates of the vertices and get rid of the z coordinate. To do +this we can use the powerful `PolyCollection +`__ +object that allow to render efficiently a collection of non-regular +polygons. Since, we want to render a bunch of triangles, this is a +perfect match. So let's first extract the triangles and get rid of the +`z` coordinate: + +.. code:: python + + T = V[F][...,:2] + +We can now render it: + +.. code:: python + + fig = plt.figure(figsize=(6,6)) + ax = fig.add_axes([0,0,1,1], xlim=[-1,+1], ylim=[-1,+1], + aspect=1, frameon=False) + collection = PolyCollection(T, closed=True, linewidth=0.1, + facecolor="None", edgecolor="black") + ax.add_collection(collection) + plt.show() + +Result is shown on figure :ref:`figure-bunny-1`. + +.. figure:: threed/bunny-1.pdf + :width: 50% + + Stanford bunny without any transformation. + (sources: :source:`threed/bunny-1.py`). + :label:`figure-bunny-1` + + +Perspective Projection +---------------------- + +The rendering we've just made is actually an `orthographic projection +`_ while the +previous bunny uses a `perspective projection +`_: + +.. figure:: threed/projection.pdf + :width: 100% + + Orthographic and perspective projections. + :label:`figure-projections` + + +In both cases, the proper way of defining a projection is first to +define a viewing volume, that is, the volume in the 3d space we want +to render on the scree. To do that, we need to consider 6 clipping +planes (left, right, top, bottom, far, near) that enclose the viewing +volume (frustum) relatively to the camera. If we define a camera +position and a viewing direction, each plane can be described by a +single scalar. Once we have this viewing volume, we can project onto +the screen using either the orthographic or the perspective +projection. + +Fortunately for us, these projections are quite well known and can be +expressed using 4x4 matrices: + +.. code:: python + + def frustum(left, right, bottom, top, znear, zfar): + M = np.zeros((4, 4), dtype=np.float32) + M[0, 0] = +2.0 * znear / (right - left) + M[1, 1] = +2.0 * znear / (top - bottom) + M[2, 2] = -(zfar + znear) / (zfar - znear) + M[0, 2] = (right + left) / (right - left) + M[2, 1] = (top + bottom) / (top - bottom) + M[2, 3] = -2.0 * znear * zfar / (zfar - znear) + M[3, 2] = -1.0 + return M + + def perspective(fovy, aspect, znear, zfar): + h = np.tan(0.5*radians(fovy)) * znear + w = h * aspect + return frustum(-w, w, -h, h, znear, zfar) + +For the perspective projection, we also need to specify the aperture +angle that (more or less) sets the size of the near plane relatively +to the far plane. Consequently, for high apertures, you'll get a lot +of "deformations". + +However, if you look at the two functions above, you'll realize they +return 4x4 matrices while our coordinates are 3d. How to use these +matrices then ? The answer is `homogeneous coordinates +`_. To make a +long story short, homogeneous coordinates are best to deal with +transformation and projections in 3D. In our case, because we're +dealing with vertices (and not vectors), we only need to add 1 as the +fourth coordinates (w) to all our vertices. Then we can apply the +perspective transformation using the dot product. + +.. code:: python + + V = np.c_[V, np.ones(len(V))] @ perspective(25,1,1,100).T + + +Last step, we need to re-normalize the homogeneous coordinates. This +means we divide each transformed vertices with the last component (w) +such as to always have w=1 for each vertices. + +.. code:: python + + V /= V[:,3].reshape(-1,1) + + +Now we can check the result on figure :ref:`figure-bunny-2` that looks totally wrong. + +.. figure:: threed/bunny-2.pdf + :width: 50% + + Wrong rendering when camera is inside. + (sources: :source:`threed/bunny-2.py`). + :label:`figure-bunny-2` + + +It looks wrong because the camera is actually inside the bunny. To +have a proper rendering, we need to move the bunny away from the +camera or to move the camera away from the bunny. Let's do the +later. The camera is currently positioned at (0,0,0) and looking up in +the z direction (because of the frustum transformation). We thus need +to move the camera away a little bit in the z negative direction and +before the perspective transformation. + +.. code:: python + + V = V - (0,0,3.5) + V = np.c_[V, np.ones(len(V))] @ perspective(25,1,1,100).T + V /= V[:,3].reshape(-1,1) + + +The corrected output is shown on figure :ref:`figure-bunny-3`. + +.. figure:: threed/bunny-3.pdf + :width: 50% + + Corrected rendering with camera away. + (sources: :source:`threed/bunny-3.py`). + :label:`figure-bunny-3` + + +Model, view, projection (MVP) +----------------------------- + +It might be not obvious, but the last rendering is actually a perspective +transformation. To make it more obvious, we'll rotate the bunny around. To do +that, we need some rotation matrices (4x4) and we can as well define the +translation matrix in the meantime: + +.. code:: python + + def translate(x, y, z): + return np.array([[1, 0, 0, x], + [0, 1, 0, y], + [0, 0, 1, z], + [0, 0, 0, 1]], dtype=float) + + def xrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array([[1, 0, 0, 0], + [0, c, -s, 0], + [0, s, c, 0], + [0, 0, 0, 1]], dtype=float) + + def yrotate(theta): + t = np.pi * theta / 180 + c, s = np.cos(t), np.sin(t) + return np.array([[ c, 0, s, 0], + [ 0, 1, 0, 0], + [-s, 0, c, 0], + [ 0, 0, 0, 1]], dtype=float) + +We'll now decompose the transformations we want to apply in term of model +(local transformations), view (global transformations) and projection such that +we can compute a global MVP matrix that will do everything at once: + +.. code:: python + + model = xrotate(20) @ yrotate(45) + view = translate(0,0,-3.5) + proj = perspective(25, 1, 1, 100) + MVP = proj @ view @ model + +and we now write: + +.. code:: python + + V = np.c_[V, np.ones(len(V))] @ MVP.T + V /= V[:,3].reshape(-1,1) + +You should obtain results shown on figure :ref:`figure-bunny-4`. + +.. figure:: threed/bunny-4.pdf + :width: 50% + + Rotated and translated bunny. + (sources: :source:`threed/bunny-4.py`). + :label:`figure-bunny-4` + +Let's now play a bit with the aperture such that you can see the +difference (see figure :ref:`figure-bunny-5`). Note that we also need +to adapt the distance to the camera in order for the bunnies to have +the same apparent size + +.. figure:: threed/bunny-5.pdf + :width: 100% + + Different apertures + (sources: :source:`threed/bunny-5.py`). + :label:`figure-bunny-5` + + +Depth sorting +------------- + +Let's try now to fill the triangles and see what happens (figure +:ref:`figure-bunny-6`). + +.. figure:: threed/bunny-6.pdf + :width: 50% + + Rendering without depth sorting. + (sources: :source:`threed/bunny-6.py`). + :label:`figure-bunny-6` + +As you can see, the result is totally wrong. The problem is that the +PolyCollection draws the triangles in the order they are given +while we would like to have them from back to front. This means we +need to sort them according to their depth. The good news is that we +already computed this information when we applied the MVP +transformation. It is stored in the new z coordinates. However, these +z values are vertices based while we need to sort the triangles. We'll +thus take the mean z value as being representative of the depth of a +triangle. If triangles are relatively small and do not intersect, this +works beautifully: + +.. code:: python + + T = V[:,:,:2] + Z = -V[:,:,2].mean(axis=1) + I = np.argsort(Z) + T = T[I,:] + + +And now everything is rendered correctly as shown on figure +:ref:`figure-bunny-7`. + +.. figure:: threed/bunny-7.pdf + :width: 50% + + Rendering with depth sorting. + (sources: :source:`threed/bunny-7.py`). + :label:`figure-bunny-7` + + +Let's add some colors using the depth buffer. We'll color each triangle +according to it depth. The beauty of the PolyCollection object is that you can +specify the color of each of the triangle using a numpy array, so let's just do +that: + +.. code:: python + + zmin, zmax = Z.min(), Z.max() + Z = (Z-zmin)/(zmax-zmin) + C = plt.get_cmap("magma")(Z) + I = np.argsort(Z) + T, C = T[I,:], C[I,:] + +And our final display is show on figure :ref:`figure-bunny-8`. + +.. figure:: threed/bunny-8.pdf + :width: 100% + + Rendering with depth colors. + (sources: :source:`threed/bunny-8.py`). + :label:`figure-bunny-8` + +The final script (:source:`threed/bunny-8.py`) is 57 lines but +hardly readable. However, it shows that 3D rendering can be done quite +easily with matplotlib and a few transformations. + +Exercises +--------- + +Using the definition of the orthographic projection, try to reproduce +the figure :ref:`figure-bunnies` that mix perspective (top left) and +orthographic projections. Each bunny is contained in one axes. + +.. figure:: threed/bunnies.pdf + :width: 100% + + Stanford bunnies + (sources: :source:`threed/bunnies.py`). + :label:`figure-bunnies` diff --git a/rst/typography.rst b/rst/typography.rst new file mode 100644 index 0000000..341ccce --- /dev/null +++ b/rst/typography.rst @@ -0,0 +1,282 @@ +.. ---------------------------------------------------------------------------- +.. Title: Scientific Visualisation - Python & Matplotlib +.. Author: Nicolas P. Rougier +.. License: Creative Commons BY-NC-SA International 4.0 +.. ---------------------------------------------------------------------------- +.. _chap-typography: + +Elements of typography +====================== + +Typography is *the art of arranging type to make written language legible, +readable, and appealing when displayed* (Wikipedia). However, for the neophyte, +typography is mostly apprehended as the juxtaposition of characters displayed on +the screen while for the expert, typography means typeface, scripts, unicode, +glyphs, ascender, descender, tracking, hinting, kerning, shaping, weight, slant, +etc. Typography is actually much more than the mere rendering of glyphs and +involves many different concepts. If glyph rendering is an important part of the +rendering pipeline as it will be explained below, it is nonetheless important +to have a basic understanding of typography. Unfortunately, I cannot write here +a full course on typography and I advise the interested reader to read +`Practical Typography `_ by Matthew +Butterick. This open access book introduces the main concepts and give sound +advice to improve your written documents. + +At this point, you could object that a scientific figure possesses only a few +places with written text and it is thus not that important. And yet, it is. +Let's have a look at figure :ref:`figure-typography-matters` that differs only +at the typographic level. The top part is the default typographic choices of +Matplotlib in terms of font families, slant, weight and size. Those defaults are +actually already quite good but can be slightly improved as shown on the bottom +figure which was made using different font families (Roboto Condensed and Roboto +Slab), size and weights. The difference might appear subtle but is really an +important dimension of a scientific figure. + +.. figure:: typography/typography-matters.png + :width: 100% + + Influence of typography on the perception of a figure + :label:`figure-typography-matters` + (sources: :source:`typography/typography-matters.py`). + +Unfortunately, there's no magical recipe to tell you how to tweak typography for +a given figure and it depends on a number of factors over which you have no real +control most of the time. For example, consider a figure you make for inclusion +in an article that will be published in a scientific journal. These kind of +journals possess a template which dictate the future layout of your article (if +accepted) as well as a font stack, that is, a choice of fonts for main body, +bibliography and peripheral information. If you want your figure to have a good +appearance, you'll need to chose your fonts accordingly. To do that, you can +have a look at font installed on your system or browse online galleries such as +`Font squirrel `_, `dafont.com +`_ or `Google font `_. + +If you install a new font on your system, don't forget to rebuild the font list +cache or Matplotlib will just ignore you newly installed font: + +.. code:: python + + import matplotlib.font_manager + matplotlib.font_manager._rebuild() + + +Font stacks +----------- + +The Matplotlib font stack is defined using four different typeface families, +namely `sans `_, `serif +`_, `monospace +`_ and `cursive +`_. The default font stack is +based on the `DejaVu `_ fonts that +are based on the `Bitstream Vera +`_ fonts. DejaVu fonts offer good +unicode coverage but they come with only two weights (regular and bold) which +might be a bit limiting and the project seems to have been abandoned +since 2016. The default cursive font is `Apple Chancery +`_. Note +however that these are only the primary default choices and Matplotlib can fall +back to other typefaces if the defaults are not installed. To check which font is actually used, you can type: + +.. code:: python + + from matplotlib.font_manager import findfont, FontProperties + for family in ["serif", "sans", "monospace", "cursive"]: + font = findfont(FontProperties(family=family)) + print(family, ":" , os.path.basename(font)) + +You can also design your own own font stack by choosing a set of alternative +font families. Figure :ref:`figure-typography-font-stacks` shows some +alternative font stacks based on the Roboto and Source Pro Family which both +have serif, sans and monospace typefaces and comes with several weights. + +.. figure:: typography/typography-font-stacks.pdf + :width: 100% + + Font stack alternatives + :label:`figure-typography-font-stacks` + (sources: :source:`typography/typography-font-stacks.py`). + +This font stack can be used as the default by modifying either the `rc +`_ file or the +stylesheet (we'll see that in the section :nameref:`chap-defaults`) but you can also use a specific font face for any textual object such as tick +labels, legend, figure title, etc. However, for consistency, it's better to +use the same family of fonts (serif, sans and mono) for the whole figure. + + +Rendering mathematics +--------------------- + +The case of mathematical text is slightly more complicated because it requires +several different fonts possessing all the necessary mathematical symbols and +there are not so many such fonts. Matplotlib offers five different families, +namely `DejaVu `_ (sans and serif), +`Styx `_ (sans and serif) and +`computer modern `_: + +.. figure:: typography/typography-math-stacks.pdf + :width: 100% + + Mathematics font stacks. + :label:`figure-typography-math-stacks` + (sources: :source:`typography/typography-math-stacks.py`). + +Matplotlib possesses its own `TeX parser and layout engine +`_ which is quite capable +even though it suffers from some imperfections. For comparison, here is the +same mathematical expression as rendered by LaTeX: + +.. math:: + + \frac{\pi}{4} = \sum_{k=0}^\infty\frac{(-1)^k}{2k+1} + +We can notice some obvious differences (alignment, weights, line widths). If +this is unacceptable for your case, you still have the option to use the real +TeX engine by setting the usetex variable: + +.. code:: python + + import matplotlib as mpl + plt.rcParams.update({"text.usetex": True}) + + +A note about size +----------------- + +When you manipulate textual objects you need to specify a size (either +explicitly or through the defaults) that is expressed in point (pt). In +matplotlib, a point corresponds to 1/72 inches (0.35mm) (while for LaTeX, a +point corresponds to 1/72.27 inches). The question is then what does this size +measure exactly? It corresponds to 1 *em* which is a typographic unit and more or +less corresponds to a bounding box that can contain any glyphs. No need to say +more at this point because the important information is that font sizes are +specified in inches and the apparent size is thus directly linked to the +resolution of your figure (not the dimension) through the dots per inch (dpi) +parameter. You can thus define either a very large or tiny figure, and a font with +size 10 will have the same visual aspects on your screen. + +**Exercise** Using different fonts, weights and size, try to reproduce the figure :ref:`figure-tick-labels-variation`. + +.. figure:: typography/tick-labels-variation.pdf + :width: 100% + + Tick label variations + :label:`figure-tick-labels-variation` + (sources: :source:`typography/tick-labels-variation.py`). + +Legibility +---------- + +For a traditional document, text is usually rendered in black against a white +background that maximizes legibility. The case of scientific visualization is a +bit different because there are some situations where you cannot control the +background color since it is part of your results. + + +.. figure:: typography/typography-legibility.pdf + :width: 100% + + Typograpy legibility variations. + :label:`figure-typography-legibility` + (sources: :source:`typography/typography-legibility.py`). + +This is especially true if you add text over an image such as shown on figure +:ref:`figure-typography-legibility`. The first line shows what happens if you +add white or black text over a random grey image. The result is nearly +impossible to read unless you zoom in. The second line is a bit better thanks to +the weight of the font that has been made heavier but the text remains difficult +to read. On the third line, I added a semi-transparent background to enhance +contrast. This dramatically improves legibility but the result is not really +aesthetic and hides a lot of data in the meantime. The best option is shown on +the last line where I outlined the font with a thin border. Here the text is +legible, aesthetic and does not hide too much data. + +**Exercise** Try to reproduce exactly the figure :ref:`figure-text-outline` +which uses the `Pacifico `_ font +family. Colors come from the magma colormap. Make sure to use different outline +widths to get the thin black line between each color. + +.. figure:: typography/text-outline.pdf + :width: 100% + + Text with far too many outlines. + :label:`figure-text-outline` + (sources: :source:`typography/text-outline.py`). + + +At this point, it is important to understand that Matplotlib offers two types of +textual object. The first and most commonly used is the regular `Text +`__ that is used +for labels, titles or annotations. It cannot be heavily transformed and most of +the time, the text is rendered following a single direction (e.g.horizontal or +vertical) even though it can be freely rotated. There exists however another +type of textual object which is the `TextPath +`__. Usage is very simple: + +.. code:: python + + from matplotlib.textpath import TextPath + from matplotlib.patches import PathPatch + path = TextPath((0,0), "ABC", size=12) + +The result is a path object that can be inserted in a figure + +.. code:: python + + patch = PathPatch(path) + ax.add_artist(patch) + +What is really interesting with such path objects is that it can now be +transformed at the level of individual vertices composing a glyph as shown on +figure :ref:`figure-typography-text-path`. + +.. figure:: typography/typography-text-path.pdf + :width: 100% + + Better contour labels using text path. + :label:`figure-typography-text-path` + (sources: :source:`typography/typography-text-path.py`). + +In this example, I replaced the regular contour labels with text path objects +that follow the path. It requires some computations but not that much +actually. The results is aesthetically better to me but it must be used +wisely. If your contour lines are too small or possesses sharp turns, it will +make the text unreadable. + +.. figure:: typography/projection-3D-gaussian.pdf + :width: 70% + + Example of 3D text paths. + :label:`figure-projection-3d-gaussian` + (sources: :source:`typography/projection-3d-gaussian.py`). + +Another interesting usage of text path is the case of 3D projection as +illustrated on figure :ref:`figure-projection-3d-gaussian`. On this figure, I +took advantage of the `3D text API +`_ to orient and project +tick labels and axes titles. Note that such projection is fine a long as the +figure is properly oriented. If you rotate, text might be difficult to read and +this the reason why the default for 3d projection is to have text that always +face the camera, ensuring legibility. + + +**Exercise** Try to reproduce figures :ref:`figure-text-starwars`. A simple +*compression* on X vertices depending on the Y level should work. Vertices +of a path can be accessed with `path.vertices`. + +.. figure:: typography/text-starwars.pdf + :width: 100% + + In a far distant galaxy. + :label:`figure-text-starwars` (sources: :source:`typography/text-starwars.py`). + + +.. + .. figure:: typography/text-spiral.pdf + :width: 50% + + Text shaped along a spiral + :label:`figure-text-spiral` (sources: :source:`typography/text-spiral.py`). + + + diff --git a/tex/.DS_Store b/tex/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/tex/.DS_Store differ diff --git a/tex/back-cover.pdf b/tex/back-cover.pdf new file mode 100644 index 0000000..de5a422 Binary files /dev/null and b/tex/back-cover.pdf differ diff --git a/tex/back-cover.tex b/tex/back-cover.tex new file mode 100644 index 0000000..a89196d --- /dev/null +++ b/tex/back-cover.tex @@ -0,0 +1,36 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\makeatletter +\newcommand{\definetrim}[2]{% + \define@key{Gin}{#1}[]{\setkeys{Gin}{trim=#2,clip}}% +} +\makeatother + +\newlength{\coverheight} +\newlength{\coverwidth} +\newlength{\bleedwidth} +\newlength{\spinewidth} + +\newlength{\trimleft} +\newlength{\trimright} +\newlength{\trimbottom} +\newlength{\trimtop} + +\setlength{\coverheight}{210mm} +\setlength{\coverwidth}{148mm} +\setlength{\bleedwidth}{3mm} +\setlength{\spinewidth}{10.5mm} + +\setlength{\trimleft}{\dimexpr \bleedwidth \relax } +\setlength{\trimright}{\dimexpr \bleedwidth + \coverwidth + \spinewidth \relax } +\setlength{\trimbottom}{\dimexpr \bleedwidth \relax } +\setlength{\trimtop}{\dimexpr \bleedwidth \relax } +\definetrim{front}{\trimleft{} \trimbottom{} \trimright{} \trimtop{}} + +\begin{document} +\includegraphics[front,height=216mm]{softcover-cover.pdf} +\end{document} + +% Largeur: 371.7 - 210.7 - 22 +% Hauteur: 260 mm diff --git a/tex/book.bbl b/tex/book.bbl new file mode 100644 index 0000000..a6c8824 --- /dev/null +++ b/tex/book.bbl @@ -0,0 +1,290 @@ +% $ biblatex auxiliary file $ +% $ biblatex bbl format version 3.1 $ +% Do not modify the above lines! +% +% This is an auxiliary file used by the 'biblatex' package. +% This file may safely be deleted. It will be recreated by +% biber as required. +% +\begingroup +\makeatletter +\@ifundefined{ver@biblatex.sty} + {\@latex@error + {Missing 'biblatex' package} + {The bibliography requires the 'biblatex' package.} + \aftergroup\endinput} + {} +\endgroup + + +\refsection{0} + \datalist[entry]{nyt/global//global/global} + \entry{Butterick:2013}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=15906aa9ea8f28e899c6d015206420ca}{% + family={Butterick}, + familyi={B\bibinitperiod}, + given={Matthew}, + giveni={M\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Online}% + } + \strng{namehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{fullhash}{15906aa9ea8f28e899c6d015206420ca} + \strng{bibnamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authorbibnamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authornamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authorfullhash}{15906aa9ea8f28e899c6d015206420ca} + \field{sortinit}{B} + \field{sortinithash}{d7095fff47cda75ca2589920aae98399} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$$\bullet$ This open access book will make you a better typographer. I’m not here to tell you that typography is more important than the substance of your writing. It’s not. But typography can enhance your writing. Typography can create a better first impression. Typography can reinforce your key points. Typography can extend reader attention.} + \field{title}{Practical Typography} + \field{year}{2013} + \true{nocite} + \verb{urlraw} + \verb https://practicaltypography.com/ + \endverb + \verb{url} + \verb https://practicaltypography.com/ + \endverb + \endentry + \entry{Hunter:2007}{article}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=a2f53dcc6bb05a06b8ad94176f80fe26}{% + family={Hunter}, + familyi={H\bibinitperiod}, + given={John\bibnamedelima D.}, + giveni={J\bibinitperiod\bibinitdelim D\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{2}{% + {Institute of Electrical}% + {Electronics Engineers ({IEEE})}% + } + \strng{namehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{fullhash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{bibnamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authorbibnamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authornamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authorfullhash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \field{sortinit}{H} + \field{sortinithash}{23a3aa7c24e56cfa16945d55545109b5} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{journaltitle}{Computing in Science {\&} Engineering} + \field{number}{3} + \field{title}{Matplotlib: A 2D Graphics Environment} + \field{volume}{9} + \field{year}{2007} + \true{nocite} + \field{pages}{90\bibrangedash 95} + \range{pages}{6} + \verb{doi} + \verb 10.1109/mcse.2007.55 + \endverb + \endentry + \entry{Rougier:2017}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Zenodo}% + } + \strng{namehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{fullhash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{bibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorbibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authornamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorfullhash}{75c00f0c05731e74db1167b4a8fe5936} + \field{extraname}{1} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ There are a lot of techniques that you don't find in books and such techniques are mostly learned through experience. The goal of this open access book is to explain some of these techniques and to provide an opportunity for making this experience in the process.} + \field{title}{{From Python to Numpy}} + \field{year}{2017} + \true{nocite} + \verb{urlraw} + \verb https://www.labri.fr/perso/nrougier/from-python-to-numpy/ + \endverb + \verb{url} + \verb https://www.labri.fr/perso/nrougier/from-python-to-numpy/ + \endverb + \endentry + \entry{Rougier:2018}{article}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Frontiers Media {SA}}% + } + \strng{namehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{fullhash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{bibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorbibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authornamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorfullhash}{75c00f0c05731e74db1167b4a8fe5936} + \field{extraname}{2} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{month}{3} + \field{title}{A Density-Driven Method for the Placement of Biological Cells Over Two-Dimensional Manifolds} + \field{volume}{12} + \field{year}{2018} + \true{nocite} + \verb{doi} + \verb 10.3389/fninf.2018.00012 + \endverb + \verb{urlraw} + \verb https://doi.org/10.3389/fninf.2018.00012 + \endverb + \verb{url} + \verb https://doi.org/10.3389/fninf.2018.00012 + \endverb + \endentry + \entry{Rougier:2014}{article}{} + \name{author}{3}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + {{un=0,uniquepart=base,hash=4778156ee049a7c63b5dfde9beea6d36}{% + family={Droettboom}, + familyi={D\bibinitperiod}, + given={Michael}, + giveni={M\bibinitperiod}, + givenun=0}}% + {{un=0,uniquepart=base,hash=e8f011b361a44c99e0b3cd105b7553dc}{% + family={Bourne}, + familyi={B\bibinitperiod}, + given={Philip\bibnamedelima E.}, + giveni={P\bibinitperiod\bibinitdelim E\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Public Library of Science ({PLoS})}% + } + \strng{namehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{fullhash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{bibnamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authorbibnamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authornamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authorfullhash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$$\bullet$ This classic and popular article provides a basic set of rules to improve figure design and explain some of the common pitfalls.} + \field{journaltitle}{{PLoS} Computational Biology} + \field{number}{9} + \field{title}{{Ten Simple Rules for Better Figures}} + \field{volume}{10} + \field{year}{2014} + \true{nocite} + \verb{doi} + \verb 10.1371/journal.pcbi.1003833 + \endverb + \endentry + \entry{Vanderplas:2016}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=0fd9a0e34f1b2adda41357c948d14986}{% + family={VanderPlas}, + familyi={V\bibinitperiod}, + given={Jake}, + giveni={J\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {O'Reilly}% + } + \strng{namehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{fullhash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{bibnamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authorbibnamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authornamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authorfullhash}{0fd9a0e34f1b2adda41357c948d14986} + \field{sortinit}{V} + \field{sortinithash}{afb52128e5b4dc4b843768c0113d673b} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ This book is meant to help Python users learn to use Python's data science stack–libraries such as IPython, NumPy, Pandas, Matplotlib, Scikit-Learn, and related tools–to effectively store, manipulate, and gain insight from data.} + \field{title}{{Python Data Science Handbook}} + \field{year}{2016} + \true{nocite} + \verb{urlraw} + \verb https://jakevdp.github.io/PythonDataScienceHandbook/index.html + \endverb + \verb{url} + \verb https://jakevdp.github.io/PythonDataScienceHandbook/index.html + \endverb + \endentry + \entry{Wilke:2019}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=8e86aa1915156f8016600d977acef303}{% + family={Wilke}, + familyi={W\bibinitperiod}, + given={Claus\bibnamedelima O.}, + giveni={C\bibinitperiod\bibinitdelim O\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {O'Reilly}% + } + \strng{namehash}{8e86aa1915156f8016600d977acef303} + \strng{fullhash}{8e86aa1915156f8016600d977acef303} + \strng{bibnamehash}{8e86aa1915156f8016600d977acef303} + \strng{authorbibnamehash}{8e86aa1915156f8016600d977acef303} + \strng{authornamehash}{8e86aa1915156f8016600d977acef303} + \strng{authorfullhash}{8e86aa1915156f8016600d977acef303} + \field{sortinit}{W} + \field{sortinithash}{4315d78024d0cea9b57a0c6f0e35ed0d} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ This open access book takes you through many commonly encountered visualization problems, and it provides guidelines on how to turn large datasets into clear and compelling figures.} + \field{title}{Fundamentals of Data Visualization} + \field{year}{2019} + \true{nocite} + \verb{urlraw} + \verb https://serialmentor.com/dataviz/ + \endverb + \verb{url} + \verb https://serialmentor.com/dataviz/ + \endverb + \endentry + \enddatalist +\endrefsection +\endinput + diff --git a/tex/book.bib b/tex/book.bib new file mode 100644 index 0000000..192f7df --- /dev/null +++ b/tex/book.bib @@ -0,0 +1,93 @@ + +@Book{Butterick:2013, + author = {Butterick, Matthew}, + title = {Practical Typography}, + publisher = {Online}, + year = {2013}, + url = {https://practicaltypography.com/}, + abstract = {$\bullet$$\bullet$$\bullet$ This open access book will make + you a better typographer. I’m not here to tell you that + typography is more important than the substance of your + writing. It’s not. But typography can enhance your writing. + Typography can create a better first impression. Typography + can reinforce your key points. Typography can extend reader + attention.}, +} + +@Book{Wilke:2019, + author = {Wilke, Claus O.}, + title = {Fundamentals of Data Visualization}, + publisher = {O'Reilly}, + year = {2019}, + url = {https://serialmentor.com/dataviz/}, + abstract = {$\bullet$$\bullet$ This open access book takes you through + many commonly encountered visualization problems, and it + provides guidelines on how to turn large datasets into clear + and compelling figures.}, +} + +@article{Rougier:2014, + doi = {10.1371/journal.pcbi.1003833}, + year = 2014, + publisher = {Public Library of Science ({PLoS})}, + volume = 10, + number = 9, + author = {Nicolas P. Rougier and Michael Droettboom and Philip + E. Bourne}, + title = {{Ten Simple Rules for Better Figures}}, + journal = {{PLoS} Computational Biology}, + abstract = {$\bullet$$\bullet$$\bullet$ This classic and popular article + provides a basic set of rules to improve figure design and + explain some of the common pitfalls.} +} + + +@Book{Rougier:2017, + author = {Nicolas P. Rougier}, + title = {{From Python to Numpy}}, + publisher = {Zenodo}, + year = {2017}, + url = {https://www.labri.fr/perso/nrougier/from-python-to-numpy/}, + abstract = {$\bullet$$\bullet$ There are a lot of techniques that you + don't find in books and such techniques are mostly learned + through experience. The goal of this open access book is to + explain some of these techniques and to provide an + opportunity for making this experience in the process.} +} + +@Book{Vanderplas:2016, + author = {Jake VanderPlas}, + title = {{Python Data Science Handbook}}, + publisher = {O'Reilly}, + year = {2016}, + url = {https://jakevdp.github.io/PythonDataScienceHandbook/index.html}, + abstract = {$\bullet$$\bullet$ This book is meant to help Python users + learn to use Python's data science stack–libraries such as + IPython, NumPy, Pandas, Matplotlib, Scikit-Learn, and related + tools–to effectively store, manipulate, and gain insight + from data.} +} + +@article{Hunter:2007, + author = {John D. Hunter}, + title = {Matplotlib: A 2D Graphics Environment}, + journal = {Computing in Science {\&} Engineering}, + year = {2007}, + publisher = {Institute of Electrical and Electronics Engineers ({IEEE})}, + volume = {9}, + number = {3}, + pages = {90--95}, + doi = {10.1109/mcse.2007.55}, +} + + +@article{Rougier:2018, + doi = {10.3389/fninf.2018.00012}, + url = {https://doi.org/10.3389/fninf.2018.00012}, + year = {2018}, + month = mar, + publisher = {Frontiers Media {SA}}, + volume = {12}, + author = {Nicolas P. Rougier}, + title = {A Density-Driven Method for the Placement of Biological Cells Over Two-Dimensional Manifolds} +} \ No newline at end of file diff --git a/tex/book.tex b/tex/book.tex new file mode 100644 index 0000000..361389b --- /dev/null +++ b/tex/book.tex @@ -0,0 +1,671 @@ +% \documentclass[10pt,a5paper]{book} +\documentclass[10pt]{book} + + +% --- Page layout ------------------------------------------------------------- +\usepackage[a5paper,includeheadfoot]{geometry} +% \usepackage[pass,paperwidth=148mm,paperheight=210mm,includeheadfoot]{geometry} +\geometry{right=15mm, left=20mm, top=15mm, bottom=15mm} + +\usepackage{xcolor} + +% --- ISBN -------------------------------------------------------------------- +\usepackage[ISBN=978-2-9579901-0-8,SC2]{ean13isbn} + + +%% --- Hardcover book --------------------------------------------------------- +\ifdefined\hardcover +\usepackage[height=21.634truecm, width=15.434truecm, axes, pdftex, center]{crop} +\definecolor{codecolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{stringcolor}{cmyk}{0, 0, 0, 0.8} +\definecolor{commentcolor}{cmyk}{0, 0, 0, 0.5} +\definecolor{admcolor}{cmyk}{0, 0, 0, 0.1} +\definecolor{citecolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{linkcolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{urlcolor}{cmyk}{0, 0, 0, 0.8} + +\else +%% --- Softcover book --------------------------------------------------------- +\ifdefined\softcover +\usepackage[height=21.6truecm, width=15.4truecm, axes, pdftex, center]{crop} +\definecolor{codecolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{stringcolor}{cmyk}{0, 0, 0, 0.8} +\definecolor{commentcolor}{cmyk}{0, 0, 0, 0.5} +\definecolor{admcolor}{cmyk}{0, 0, 0, 0.1} +\definecolor{citecolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{linkcolor}{cmyk}{0, 0, 0, 1.0} +\definecolor{urlcolor}{cmyk}{0, 0, 0, 0.8} + +\else +%% --- PDF book --------------------------------------------------------- +\definecolor{codecolor}{HTML}{311b92} %% Material Deep purple 900 +\definecolor{commentcolor}{HTML}{607d8b} %% Material Blue grey 500 +\definecolor{stringcolor}{HTML}{e65100} %% Material Orange 900 +\definecolor{admcolor}{HTML}{fffde7} %% Material Yellow 50 +\definecolor{citecolor}{HTML}{37474f} %% Material Blue grey 800 +\definecolor{linkcolor}{HTML}{37474f} %% Material Blue grey 800 +\definecolor{urlcolor}{HTML}{37474f} %% Material Blue grey 800 +\fi +\fi + +%% \DeclareGraphicsExtensions{.png,.pdf} + + + +% --- Specialized packages ---------------------------------------------------- +% \usepackage{ulem} %% Strike-through font (\sout and \xout) +\usepackage{enumitem} %% Fine control over itemize/enumerate/description +\usepackage{multicol} %% Local multi-columns +\usepackage{pdfpages} %% For inclusion of PDF (\includepdf) +\usepackage{hologo} %% Latex logos +\usepackage{float} %% Fine control of floats +\usepackage{tikz} %% Nice inline graphics +\usepackage{alltt} %% tt mode +\usepackage{parskip} %% tt mode +\usepackage{amsmath} %% math (equation) +\usepackage{hologo} %% Latex logos +\usepackage{nicefrac} %% Nice (simple) fractions in math mode + + + +% --- English stuff ----------------------------------------------------------- +\usepackage[english]{babel} +\usepackage{xspace} +% \usepackage{csquotes} + +% --- Graphics ---------------------------------------------------------------- +\usepackage{graphicx} +\graphicspath{{../figures/}{../cover/}} +\floatplacement{figure}{htb!} % (h)ere (t)op (b)ottom (p)age + +% Reduce Space Around Floats (Algorithm, Figures, etc) in Latex +\setlength\floatsep{1.\baselineskip plus 3pt minus 2pt} +\setlength\textfloatsep{1.\baselineskip plus 3pt minus 2pt} +\setlength\intextsep{1.\baselineskip plus 3pt minus 2 pt} +% The First Line is for length between two adjacent floats +% The Second Line - for floats on top and bottom of text only +% For floats at top - length between float and text below it +% For floats at bottom - length between float and text above it +% The Third Line- for floats in the middle of text only - +% length between text above it, and text below it. + + +% --- Header & footer --------------------------------------------------------- +\usepackage{fancyhdr} +\fancyhf{} +\pagestyle{fancy} +\renewcommand{\chaptermark}[1]{\markboth{#1}{}} +\fancyhead[LE,RO]{\footnotesize\bfseries\sffamily \thepage} +\fancyhead[RE]{\footnotesize\bfseries\sffamily \chaptername~\thechapter} +\fancyhead[LO]{\footnotesize\bfseries\sffamily \leftmark} +\fancyfoot{} +\renewcommand{\headrulewidth}{0pt} + +\fancypagestyle{frontmatter}{ + \fancyhead[LE,RO]{\footnotesize\bfseries\sffamily \thepage} + \fancyhead[RE]{\footnotesize\bfseries\sffamily \leftmark} + \fancyhead[LO]{\footnotesize\bfseries\sffamily \leftmark} + \fancyfoot{} + \renewcommand{\headrulewidth}{0pt} +} + +\fancypagestyle{backmatter}{ + \fancyhead[LE,RO]{\footnotesize\bfseries\sffamily \thepage} + \fancyhead[RE]{\footnotesize\bfseries\sffamily \leftmark} + \fancyhead[LO]{\footnotesize\bfseries\sffamily \leftmark} + \fancyfoot{} + \renewcommand{\headrulewidth}{0pt} +} + +\fancypagestyle{mainmatter}{ + \fancyhead[LE,RO]{\footnotesize\bfseries\sffamily \thepage} + \fancyhead[RE]{\footnotesize\bfseries\sffamily \chaptername~\thechapter} + \fancyhead[LO]{\footnotesize\bfseries\sffamily \leftmark} + \fancyfoot{} + \renewcommand{\headrulewidth}{0pt} +} + +\fancypagestyle{plain}{ + \renewcommand{\headrulewidth}{0pt} + \fancyhf{} +} +\usepackage{emptypage} % No header & footer on empty pages + + +% --- Fonts ------------------------------------------------------------------- +\usepackage{fontspec} +\usepackage[babel=true]{microtype} +%\defaultfontfeatures{Ligatures=TeX} +\setmainfont{Source Serif Pro}[ + Path = ../fonts/source-serif-pro/SourceSerifPro-, Extension = .otf, + Ligatures=TeX, + UprightFont= Light, + ItalicFont = LightIt, + BoldFont = Regular, + BoldItalicFont = It ] +\setsansfont{Roboto}[ + Ligatures=TeX, + Path = ../fonts/roboto/Roboto-, Extension = .ttf, + UprightFont = Light, + ItalicFont = LightItalic, + BoldFont = Regular ] +\setmonofont{Source Code Pro}[ + Path = ../fonts/source-code-pro/SourceCodePro-, Extension = .otf, + UprightFont = Light, + BoldFont = Regular ] +\newfontfamily\RobotoCon{Roboto Condensed}[ + Path = ../fonts/roboto/RobotoCondensed-, Extension = .ttf, + Ligatures=TeX, + UprightFont = Regular, + ItalicFont = Italic, + BoldFont = Bold ] +\newfontfamily\Roboto{Roboto}[ + Path = ../fonts/roboto/Roboto-, Extension = .ttf, + Ligatures=TeX, + UprightFont = Regular, + ItalicFont = Italic, + BoldFont = Black ] + + +% --- URL, href and colors ---------------------------------------------------- +% \usepackage{xcolor} +%% \definecolor{darkred}{HTML}{CF232B} +%% \definecolor{darkblue}{HTML}{000099} + +%% See https://tex.stackexchange.com/questions/75451/xcolor-black-isnt-black-enough +% \definecolor{darkgray}{cmyk}{0,0,0,.8} %%{HTML}{555555} + +%% \definecolor{lightgray}{HTML}{cccccc} +%\colorlet{citecolor}{black} +%\colorlet{linkcolor}{black} +% \colorlet{urlcolor}{darkblue} +%\colorlet{urlcolor}{darkgray} + +\usepackage[ + %% pdfusetitle, + pdfauthor={Nicolas P. Rougier}, + pdftitle={Scientific Visualization: Python & Matplotlib}, + pdfsubject={Scientific Visualization}, + pdfkeywords={Python, Matplotlib, Open Science, 42}, + %% + bookmarks=true, + breaklinks=true, + pdfborder={0 0 0}, + citecolor=citecolor, + linkcolor=linkcolor, + urlcolor=urlcolor, + colorlinks=true, + linktocpage=false, + hyperindex=true, + colorlinks=true, + linktocpage=false, + linkbordercolor=white]{hyperref} +% \urlstyle{sf} +\usepackage{bookmark} + +\newsavebox{\ExternalLinkIcon} + +\begin{lrbox}{\ExternalLinkIcon} + \begin{tikzpicture}[x=1.2ex, y=1.2ex, baseline=-0.5ex, scale=0.75] + \begin{scope}[x=1ex, y=1ex] + \clip (-0.1,-0.1) + --++ (-0, 1.2) + --++ (0.6, 0) + --++ (0, -0.6) + --++ (0.6, 0) + --++ (0, -1); + \path[draw=urlcolor, line width = 0.5, + rounded corners=0.5] (0,0) rectangle (1,1); + \end{scope} + \path[draw=urlcolor, line width = 0.5] (0.5, 0.5) -- (1, 1); + \path[draw=urlcolor, line width = 0.5] (0.6, 1) -- (1, 1) -- (1, 0.6); + \end{tikzpicture}% <--- important! +\end{lrbox} + + +\makeatletter +\def\cleartoevenpage{\clearpage\if@twoside \ifodd\c@page + \hbox{}\newpage\if@twocolumn\hbox{}\newpage\fi\fi\fi} +\makeatother + +\newcommand{\fullpagefigure}[1]{ + \cleartoevenpage + %% \begin{tikzpicture}[remember picture,overlay, shift={(current page.north west)}] + %% \node[anchor=north west, xshift=-3mm, yshift=-23mm,inner sep=0pt]{ + %% \makebox[154mm][c]{\includegraphics[height=230mm]{#1}}}; + %% \end{tikzpicture} +\begin{tikzpicture}[remember picture,overlay, shift={(current page.center)}] + \node[anchor=center, xshift=0mm, yshift=0mm,inner sep=0pt]{ + \includegraphics[height=220mm]{#1}}; +\end{tikzpicture} +% \begin{figure}[p] +% \vspace*{-4cm} +% \makebox[\linewidth]{ +% \includegraphics[height=230mm]{#1}} +% \end{figure} + \clearpage + \vspace*{\fill} +} + +\newcommand{\pdfinclude}[2]{ + \clearpage + \thispagestyle{empty} + \begin{tikzpicture}[remember picture,overlay, shift={(current page.center)}] + \node[anchor=center, xshift=0mm, yshift=0mm,inner sep=0pt]{ + \includegraphics[height=#1]{#2}}; + \end{tikzpicture} +} + +% --- Pagination -------------------------------------------------------------- +\def\cleared{\empty} +\def\cleartorecto{\clearpage\if at twoside \ifodd\c at page\else + \hbox{}\thispagestyle{cleared}% + \newpage\if at twocolumn\hbox{}\newpage\fi\fi\fi} +\def\cleartoverso{\clearpage\if at twoside + \ifodd\c at page\hbox{}\thispagestyle{cleared}% + \newpage\if at twocolumn\hbox{}\newpage\fi\fi\fi} +\def\clearchapter{\clearpage~\thispagestyle{empty}\mbox{}\cleartorecto} + +% --- Listings ---------------------------------------------------------------- +\usepackage{listings} + + +\lstset{% + language=Python, + basicstyle=\ttfamily\footnotesize\color{codecolor}, + keywordstyle=\color{codecolor}\bfseries, + commentstyle=\color{commentcolor}, + identifierstyle=\color{codecolor}, + stringstyle=\color{stringcolor}, + backgroundcolor=\color{white}, + numbers=none, + upquote=true, + numberstyle=\ttfamily, + stepnumber=2, + showspaces=false, + showstringspaces=false, + showtabs=false, + frame=none, + framerule=0.5pt, + tabsize=2, + rulesep=5em, + captionpos=b, + breaklines=true, + breakatwhitespace=false, + framexleftmargin=0em, + xleftmargin=0em, + framexrightmargin=0em, + xrightmargin=0em, + aboveskip=10pt, + belowskip=0pt, +} + +% --- Bibliography ------------------------------------------------------------ +\usepackage[ + backend=biber, + % style = numeric, + style=authoryear, + % refsection=chapter, + % backref=true, + % backrefstyle=three, + sorting = nyt, + giveninits = true, + maxcitenames=3, + mincitenames=1, + maxbibnames=10, + isbn = false, + url = false, + doi = false, + natbib = true]{biblatex} +\usepackage{bibentry} + +% Some space between items +\setlength\bibitemsep{.5\baselineskip} + +% Small font for bib entries +\renewcommand*{\bibfont}{\small \sffamily} +\addbibresource{book.bib} + +% Make sure rightmark and leftmark are right +\defbibheading{bibliography}[\bibname]{% + \chapter{#1}% + \markboth{References}{References}} + +% Make bib entry title clickable +\newcommand{\doiorurl}{% + \iffieldundef{doi} + {\iffieldundef{url} + {} + {\strfield{url}}} + {http://dx.doi.org/\strfield{doi}}% +} +\newcommand{\myhref}[1]{% + \ifboolexpr{% + test {\ifhyperref} + and not test {\iftoggle{bbx:url}} + and not test {\iftoggle{bbx:doi}} + } + {\href{\doiorurl}{#1}} + {#1}% +} +\DeclareFieldFormat{title}{\myhref{\mkbibemph{#1}}} +\DeclareFieldFormat + [article,inbook,incollection,inproceedings,patent,thesis,unpublished] + {title}{\myhref{\mkbibquote{#1\isdot}}} + +% Add abstract when available +\DeclareFieldFormat{abstract}{\par\footnotesize#1} +\renewbibmacro*{finentry}{\printfield{abstract}\finentry} + + +% --- Captions ---------------------------------------------------------------- +\usepackage[labelsep=newline,singlelinecheck=false]{caption} +\renewcommand{\captionfont}{\small} +\renewcommand{\captionlabelfont}{\small\sffamily\bfseries} + + +% --- Style -------------------------------------------------------------------- +%% Default itemize aspect +\setlist[itemize]{leftmargin=*, noitemsep, topsep=5pt} +\setlist[description]{font=\bfseries\sffamily,labelindent=0pt, leftmargin=15pt} + +%% No indent on paragraph +\setlength\parindent{0pt} + +%% A more versatile quoting environment +\usepackage[font={small},vskip=5pt,leftmargin=0pt,rightmargin=0pt]{quoting} + + + +% --- Part, chapter and sections ---------------------------------------------- +\usepackage{titlesec} +\titleformat{\part} + {\large\bfseries\sffamily}{\thepart}{1em}{} +% this alters "before" spacing (the second length argument) to 0 +\titlespacing*{\part}{0pt}{-15pt}{100pt} + +\titleformat{\chapter} + {\large\bfseries\sffamily}{\thechapter}{1em}{} +% this alters "before" spacing (the second length argument) to 0 +\titlespacing*{\chapter}{0pt}{-15pt}{100pt} + +\titleformat{name=\chapter,numberless} + {\large\bfseries\sffamily}{}{1em}{} +% this alters "before" spacing (the second length argument) to 0 +\titlespacing*{name=\chapter, numberless}{0pt}{-15pt}{100pt} + +\titleformat{\section} + {\normalfont\bfseries\sffamily}{}{0em}{} +\titlespacing*{\section}{0pt}{10pt}{10pt} + + +% --- Table of content -------------------------------------------------------- +% ToC font +\usepackage{tocloft} +\renewcommand{\cftpartfont}{\normalfont\sffamily\bfseries} +\renewcommand{\cftchapfont}{\normalfont\small\sffamily} +\renewcommand{\cftsecfont}{\normalfont\sffamily} +\renewcommand{\cftsubsecfont}{\normalfont\sffamily} +\renewcommand{\cftsubsubsecfont}{\normalfont\sffamily} +\renewcommand{\cfttoctitlefont}{\large\sffamily\bfseries} +\cftpagenumbersoff{part} +\renewcommand\cftpartafterpnum{\vskip 0pt} +\renewcommand\cftchapafterpnum{\vskip -10pt} + + +% --- Macros ------------------------------------------------------------------ +\newcommand*\cleartoleftpage{% + \clearpage + \ifodd\value{page}\hbox{}\newpage\fi +} + + +% --- Docutils ---------------------------------------------------------------- +\usepackage{booktabs} +\usepackage{longtable,ltcaption,array} +\newlength{\DUtablewidth} % internal use in tables +\providecommand*{\DUrole}[2]{{\csname #1\endcsname {#2}}} +\providecommand*{\DUlegend}[2]{#2} +\newcommand{\code}[1]{\ttfamily\small\color{darkgray} {#1}} +\newcommand{\source}[1]{\href{https://github.com/rougier/scientific-visualization-book/blob/master/code/#1}{#1}} + + + +% class handling for environments (block-level elements) +% \begin{DUclass}{spam} tries \DUCLASSspam and +% \end{DUclass}{spam} tries \endDUCLASSspam +\ifx\DUclass\undefined % poor man's "provideenvironment" + \newenvironment{DUclass}[1]% + {\def\DocutilsClassFunctionName{#1}% arg cannot be used in end-part of environment. + \csname \DocutilsClassFunctionName \endcsname}% + {\csname end\DocutilsClassFunctionName \endcsname}% +\fi + + +%%% Fallback definitions for Docutils-specific commands +% numeric or symbol footnotes with hyperlinks +\providecommand*{\DUfootnotemark}[3]{% + \raisebox{1em}{\hypertarget{#1}{}}% + \hyperlink{#2}{\textsuperscript{#3}}% +} +\providecommand{\DUfootnotetext}[4]{% + \begingroup% + \renewcommand{\thefootnote}{% + \protect\raisebox{1em}{\protect\hypertarget{#1}{}}% + \protect\hyperlink{#2}{#3}}% + \footnotetext{#4}% + \endgroup% +} + +\renewenvironment{quote}{}{} + +\newenvironment{epigraph}{\thispagestyle{empty} + \vspace*{\stretch{1}} % some space at the top + \itshape % the text is in italics +% \raggedleft + \small}{\vspace{\stretch{3}} + \cleardoublepage} + +\usepackage[framemethod=TikZ]{mdframed} +\mdfdefinestyle{Highlights}{% + fontcolor=black, + linecolor=white, + outerlinewidth=0.0pt, + roundcorner=3pt, + innertopmargin=.5\baselineskip, + innerbottommargin=.5\baselineskip, + innerrightmargin=5pt, + innerleftmargin=5pt, + backgroundcolor=admcolor, + skipabove=10pt, +} + +\newenvironment{highlights}{\begin{mdframed}[style=Highlights]\small }{\end{mdframed}} +% \newenvironment{warning}{\begin{mdframed}[style=Highlights]\small }{\end{mdframed}} + +% admonition (specially marked topic) +\providecommand{\DUadmonition}[2][class-arg]{% + % try \DUadmonition#1{#2}: + \ifcsname DUadmonition#1\endcsname% + \csname DUadmonition#1\endcsname{#2}% + \else +% \begin{center} + % \fbox{\parbox{1.0\linewidth}{\small #2}} + \begin{mdframed}[style=Highlights]\small #2 \end{mdframed} +% \end{center} + \fi +} + +% title for topics, admonitions, unsupported section levels, and sidebar +\providecommand*{\DUtitle}[2][class-arg]{% + % call \DUtitle#1{#2} if it exists: + \ifcsname DUtitle#1\endcsname% + \csname DUtitle#1\endcsname{#2}% + \else + % \smallskip\noindent\textbf{#2}\smallskip% + % \textbf{#2} + {} + \fi +} + + +\let\originalltt\alltt +\let\originendalltt\endalltt + +\renewenvironment{alltt}{\originalltt \footnotesize}{\originendalltt} +\let\orighref\href + +\ifdefined\hardcover +\else +\ifdefined\softdcover +\else +\renewcommand*{\href}[2]{\orighref{#1}{#2\,\usebox{\ExternalLinkIcon}}} +\fi +\fi + +% ----------------------------------------------------------------------------- +\title{ {\sffamily \bfseries Scientific Visualisation}\\ + {\Large \sffamily Python \& Matplotlib} } +\author{\normalsize \scshape Nicolas P. Rougier} +\date{~\vfill \small \scshape Scientific Python -- Volume II} + +%\raggedbottom +\flushbottom +\begin{document} + +% ----------------------------------------------------------------------------- +\frontmatter \pagestyle{frontmatter} +% ----------------------------------------------------------------------------- + +%% --- Softcover book --------------------------------------------------------- +\ifdefined\hardcover + \thispagestyle{empty} \mbox{} + \else + +%% --- Softcover book --------------------------------------------------------- +\ifdefined\softcover +\thispagestyle{empty} \mbox{} + + +%% --- PDF book --------------------------------------------------------- +\else + \pdfinclude{216mm}{front-cover.pdf} + +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + + \fi +\fi + +\newpage + +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + +\input{copyright} +\maketitle + +\newpage +\ % The empty page +\newpage +\input{00-dedication} + +\clearpage +\vspace*{-60pt} +\setcounter{tocdepth}{0} +\tableofcontents +\addtocontents{toc}{\vspace{50pt}} + +\newpage +\ % The empty page +\newpage + +\input{00-preface} + +\newpage +\ % The empty page +\newpage + +\input{00-acknowledgments} + +\newpage +\ % The empty page +\newpage +\input{00-introduction} + +% ----------------------------------------------------------------------------- +\mainmatter \pagestyle{mainmatter} +% ----------------------------------------------------------------------------- + +\input{main} + +% ----------------------------------------------------------------------------- +\backmatter \pagestyle{backmatter} +% ----------------------------------------------------------------------------- + +\chapter{Bibliography \label{chap-bibliography}} +\nocite{*} +% \printbibliography[title=Bibliography,heading=bibliography] +\printbibliography[heading=none] +\newpage + +% Empty page +\thispagestyle{empty} +\mbox{} +\newpage + +\vspace*{\fill} + +%% \begin{footnotesize} +%% \noindent Achevé d’imprimer en Novembre 2021 en France par Pumbo pour le compte +%% de Nicolas P. Rougier, Bordeaux.\\ +%% Dépôt légal : Novembre 2021. +%% \end{footnotesize} + +%% % Empty page +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + + +% Empty page +%% \thispagestyle{empty} +%% \mbox{} +%% \newpage + +% Empty page +%% \thispagestyle{empty} +%% \mbox{} +%% \cleartoleftpage + + + +%% --- Softcover book --------------------------------------------------------- +\ifdefined\hardcover + \thispagestyle{empty} \mbox{} + \else + +%% --- Softcover book --------------------------------------------------------- +\ifdefined\softcover +\thispagestyle{empty} \mbox{} + + +%% --- PDF book --------------------------------------------------------- +\else + \pdfinclude{216mm}{back-cover.pdf} +\fi +\fi + + +\end{document} + diff --git a/tex/copyright.tex b/tex/copyright.tex new file mode 100644 index 0000000..3da72bf --- /dev/null +++ b/tex/copyright.tex @@ -0,0 +1,27 @@ +~\vfill +{\footnotesize \linespread{1.0} + + \noindent {\scshape Scientific Visualisation, Python \& Matplotlib}\par + \noindent Copyright © 2021 Nicolas P. Rougier. Some rights reserved.\\ + + \noindent This volume is licensed under a Creative Commons Attribution + Non-Commercial Share-Alike 4.0 International License, which permits use, + sharing, adaptation, distribution and reproduction in any medium or format, + as long as you give appropriate credit to the original author(s) and the + source, provide a link to the Creative Commons license, and indicate if + changes were made. You may not use the material for commercial purposes. If + you remix, transform, or build upon the material, you must distribute your + contributions under the same license as the original. To learn more, visit + \href{https://creativecommons.org}{creativecommons.org}.\par + ~\\ + \noindent First Edition: November 2021.\\ + \ISBN\\ + ~\\ + \noindent This book is typeset in + {\em Roboto}, {\em Source Serif Pro} \& {\em Source Code pro}. It has been written in reStructuredText, converted to \hologo{LaTeX} using docutils and exported to Portable Document Format using \hologo{XeLaTeX}.\par +~\\ +\noindent Printing: +~\\ +\noindent 1 2 3 4 5 6 7 8 9 10\\ +} +\cleardoublepage diff --git a/tex/cover-pattern.pdf b/tex/cover-pattern.pdf new file mode 100644 index 0000000..93459ce Binary files /dev/null and b/tex/cover-pattern.pdf differ diff --git a/tex/cover-pattern.png b/tex/cover-pattern.png new file mode 100644 index 0000000..b184ac3 Binary files /dev/null and b/tex/cover-pattern.png differ diff --git a/tex/cover-pattern.py b/tex/cover-pattern.py new file mode 100644 index 0000000..7841095 --- /dev/null +++ b/tex/cover-pattern.py @@ -0,0 +1,28 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.collections import PatchCollection + +inch = 25.4 +width = 2*148 + 10 +2*3 +height = 210 + 2*3 + +fig = plt.figure(figsize=(width/inch,height/inch)) +ax = fig.add_axes([0,0,1,1], aspect=1, frameon=False) + +radius = 25 +patches = [] +for y in range(11,-1,-1): + for x in range(8,-1,-1): + center = 2*x*radius + y%2*radius, y*radius + for i,r in enumerate(np.linspace(25, 2.5, 10)): + patches.append(plt.Circle(center, r)) + +collection = PatchCollection(patches, edgecolors="0.1", facecolors='black') +ax.add_collection(collection) + +ax.set_xlim(0, width) +ax.set_ylim(0, height) + +plt.savefig("cover-pattern.pdf") +plt.savefig("cover-pattern.png", dpi=600) +plt.show() diff --git a/tex/front-cover.pdf b/tex/front-cover.pdf new file mode 100644 index 0000000..26a68ea Binary files /dev/null and b/tex/front-cover.pdf differ diff --git a/tex/front-cover.tex b/tex/front-cover.tex new file mode 100644 index 0000000..e51023a --- /dev/null +++ b/tex/front-cover.tex @@ -0,0 +1,36 @@ +\documentclass{standalone} +\usepackage{graphicx} + +\makeatletter +\newcommand{\definetrim}[2]{% + \define@key{Gin}{#1}[]{\setkeys{Gin}{trim=#2,clip}}% +} +\makeatother + +\newlength{\coverheight} +\newlength{\coverwidth} +\newlength{\bleedwidth} +\newlength{\spinewidth} + +\newlength{\trimleft} +\newlength{\trimright} +\newlength{\trimbottom} +\newlength{\trimtop} + +\setlength{\coverheight}{210mm} +\setlength{\coverwidth}{148mm} +\setlength{\bleedwidth}{3mm} +\setlength{\spinewidth}{10.5mm} + +\setlength{\trimleft}{\dimexpr \bleedwidth + \coverwidth + \spinewidth \relax } +\setlength{\trimright}{\dimexpr \bleedwidth \relax } +\setlength{\trimbottom}{\dimexpr \bleedwidth \relax } +\setlength{\trimtop}{\dimexpr \bleedwidth \relax } +\definetrim{front}{\trimleft{} \trimbottom{} \trimright{} \trimtop{}} + +\begin{document} +\includegraphics[front,height=210mm]{softcover-cover.pdf} +\end{document} + +% Largeur: 371.7 - 210.7 - 22 +% Hauteur: 260 mm diff --git a/tex/full-cover.tex b/tex/full-cover.tex new file mode 100644 index 0000000..e11aedb --- /dev/null +++ b/tex/full-cover.tex @@ -0,0 +1,136 @@ + + +%% --- Hard cover ------------------------------------------------------------- +\ifdefined\hardcover +\documentclass[ + coverheight=248mm, %% 210mm + 2x19mm + coverwidth=167mm, %% 148mm + 19mm + bleedwidth=3mm, % + spinewidth=23mm, % (13mm + 10mm) + marklength=0mm, % + markcolor=white +]{bookcover} +\usepackage[ISBN=978-2-9579901-0-8,SC2]{ean13isbn} +\letnamebookcoverpart{back typing area}{back}[19mm,19mm,0mm,19mm] +\letnamebookcoverpart{front typing area}{front}[0mm,19mm,19mm,19mm] +\letnamebookcoverpart{spine typing area}{spine}[5mm,19mm,5mm,19mm] +\else + +%% --- Soft cover ------------------------------------------------------------- +\ifdefined\softcover +\documentclass[ + coverheight=210mm, % 210mm + coverwidth=148mm, % 148mm + bleedwidth=3.17mm, % + spinewidth=15.14mm, % 12.7mm (254 pages, paper 115g) + marklength=0mm, % + markcolor=white +]{bookcover} +\usepackage[ISBN=978-2-9579901-0-8,SC2]{ean13isbn} +\letnamebookcoverpart{back typing area}{back}[5mm,5mm,5mm,5mm] +\letnamebookcoverpart{front typing area}{front}[5mm,5mm,5mm,5mm] +\letnamebookcoverpart{spine typing area}{spine}[0mm,5mm,0mm,5mm] +\fi +\fi + +\usepackage{setspace} +\usepackage{xcolor} +\usepackage{fontspec} +\setmainfont{Avenir} +\renewcommand{\baselinestretch}{1.5} + +\begin{document} + + \begin{bookcover} + + \ifdefined\softcover + \bookcovercomponent{picture}{whole page}{cover-pattern.pdf} + \else + \ifdefined\hardcover + \bookcovercomponent{color}{whole page}{black} + \fi + \fi + + + %% DEBUG + %% \bookcovercomponent{color}{bg whole}{white,opacity=0.05} + %% \bookcovercomponent{color}{back}{white,opacity=0.05} + %% \bookcovercomponent{color}{back typing area}{white,opacity=0.05} + %% \bookcovercomponent{color}{front}{white,opacity=0.05} + %% \bookcovercomponent{color}{front typing area}{white,opacity=0.05} + %% \bookcovercomponent{color}{spine}{white,opacity=0.05} + %% \bookcovercomponent{color}{spine typing area}{white,opacity=0.05} + + %% \begin{bookcoverelement}{tikz}{bg whole} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{front} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{front typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{back} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{back typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{spine} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% \begin{bookcoverelement}{tikz}{spine typing area} + %% \draw [fill=none, white, opacity=0.5] (0,0) rectangle (\partwidth,\partheight); + %% \end{bookcoverelement} + %% END DEBUG + + + \bookcovercomponent{center}{spine}{ + \vspace{-31mm} + \addfontfeature{LetterSpace=45.0} + \rotatebox[origin=c]{-90}{\textcolor{yellow!80!white}{\Large + SCIENTIFIC~~PYTHON~~VOLUME~~II}}} + + \bookcovercomponent{normal}{spine typing area}[,,,184mm]{ + \centering + \includegraphics[width=0.8\linewidth]{./gravatar.png} + } + + \bookcovercomponent{normal}{front typing area}[,,,20mm]{ + \centering + \addfontfeature{LetterSpace=52.0} + \textcolor{yellow!80!white}{\small SCIENTIFIC~~PYTHON~~VOLUME~~II} + + \addfontfeature{LetterSpace=22.0} + \textcolor{white}{\Large \bfseries SCIENTIFIC VISUALIZATION} + + \vspace{-1pt} + \addfontfeature{LetterSpace=65.0} + \textcolor{white}{\large PYTHON \& MATPLOTLIB} + } + + \bookcovercomponent{normal}{front typing area}[,,,187mm]{ + \centering + \addfontfeature{LetterSpace=25.0} + \textcolor{white}{NICOLAS~~P.~~ROUGIER} + } + + \bookcovercomponent{normal}{back typing area}[,,,20mm]{ +%% \bookcovercomponent{normal}{back typing area}[,,,180mm]{ + \setstretch{1.0} + \centering + \textcolor{white}{In memory of\\ + \textbf{John D. Hunter} + \& + \textbf{Maxim Shemanarev}\\ + Two brilliant minds that are dearly missed.} + } + + \bookcovercomponent{normal}{back typing area}[,,,170mm]{ +%% \bookcovercomponent{normal}{back typing area}[,,,180mm]{ + \centering \fcolorbox{white}{white}{\EANisbn}\\ +%% \centering \textcolor{white}{39 euros}\\ + } + + \end{bookcover} +\end{document} diff --git a/tex/gravatar-2.png b/tex/gravatar-2.png new file mode 100644 index 0000000..bd1eb84 Binary files /dev/null and b/tex/gravatar-2.png differ diff --git a/tex/gravatar.png b/tex/gravatar.png new file mode 100644 index 0000000..02d1e41 Binary files /dev/null and b/tex/gravatar.png differ diff --git a/tex/softcover-body.bbl b/tex/softcover-body.bbl new file mode 100644 index 0000000..a6c8824 --- /dev/null +++ b/tex/softcover-body.bbl @@ -0,0 +1,290 @@ +% $ biblatex auxiliary file $ +% $ biblatex bbl format version 3.1 $ +% Do not modify the above lines! +% +% This is an auxiliary file used by the 'biblatex' package. +% This file may safely be deleted. It will be recreated by +% biber as required. +% +\begingroup +\makeatletter +\@ifundefined{ver@biblatex.sty} + {\@latex@error + {Missing 'biblatex' package} + {The bibliography requires the 'biblatex' package.} + \aftergroup\endinput} + {} +\endgroup + + +\refsection{0} + \datalist[entry]{nyt/global//global/global} + \entry{Butterick:2013}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=15906aa9ea8f28e899c6d015206420ca}{% + family={Butterick}, + familyi={B\bibinitperiod}, + given={Matthew}, + giveni={M\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Online}% + } + \strng{namehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{fullhash}{15906aa9ea8f28e899c6d015206420ca} + \strng{bibnamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authorbibnamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authornamehash}{15906aa9ea8f28e899c6d015206420ca} + \strng{authorfullhash}{15906aa9ea8f28e899c6d015206420ca} + \field{sortinit}{B} + \field{sortinithash}{d7095fff47cda75ca2589920aae98399} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$$\bullet$ This open access book will make you a better typographer. I’m not here to tell you that typography is more important than the substance of your writing. It’s not. But typography can enhance your writing. Typography can create a better first impression. Typography can reinforce your key points. Typography can extend reader attention.} + \field{title}{Practical Typography} + \field{year}{2013} + \true{nocite} + \verb{urlraw} + \verb https://practicaltypography.com/ + \endverb + \verb{url} + \verb https://practicaltypography.com/ + \endverb + \endentry + \entry{Hunter:2007}{article}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=a2f53dcc6bb05a06b8ad94176f80fe26}{% + family={Hunter}, + familyi={H\bibinitperiod}, + given={John\bibnamedelima D.}, + giveni={J\bibinitperiod\bibinitdelim D\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{2}{% + {Institute of Electrical}% + {Electronics Engineers ({IEEE})}% + } + \strng{namehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{fullhash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{bibnamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authorbibnamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authornamehash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \strng{authorfullhash}{a2f53dcc6bb05a06b8ad94176f80fe26} + \field{sortinit}{H} + \field{sortinithash}{23a3aa7c24e56cfa16945d55545109b5} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{journaltitle}{Computing in Science {\&} Engineering} + \field{number}{3} + \field{title}{Matplotlib: A 2D Graphics Environment} + \field{volume}{9} + \field{year}{2007} + \true{nocite} + \field{pages}{90\bibrangedash 95} + \range{pages}{6} + \verb{doi} + \verb 10.1109/mcse.2007.55 + \endverb + \endentry + \entry{Rougier:2017}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Zenodo}% + } + \strng{namehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{fullhash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{bibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorbibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authornamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorfullhash}{75c00f0c05731e74db1167b4a8fe5936} + \field{extraname}{1} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ There are a lot of techniques that you don't find in books and such techniques are mostly learned through experience. The goal of this open access book is to explain some of these techniques and to provide an opportunity for making this experience in the process.} + \field{title}{{From Python to Numpy}} + \field{year}{2017} + \true{nocite} + \verb{urlraw} + \verb https://www.labri.fr/perso/nrougier/from-python-to-numpy/ + \endverb + \verb{url} + \verb https://www.labri.fr/perso/nrougier/from-python-to-numpy/ + \endverb + \endentry + \entry{Rougier:2018}{article}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Frontiers Media {SA}}% + } + \strng{namehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{fullhash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{bibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorbibnamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authornamehash}{75c00f0c05731e74db1167b4a8fe5936} + \strng{authorfullhash}{75c00f0c05731e74db1167b4a8fe5936} + \field{extraname}{2} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{month}{3} + \field{title}{A Density-Driven Method for the Placement of Biological Cells Over Two-Dimensional Manifolds} + \field{volume}{12} + \field{year}{2018} + \true{nocite} + \verb{doi} + \verb 10.3389/fninf.2018.00012 + \endverb + \verb{urlraw} + \verb https://doi.org/10.3389/fninf.2018.00012 + \endverb + \verb{url} + \verb https://doi.org/10.3389/fninf.2018.00012 + \endverb + \endentry + \entry{Rougier:2014}{article}{} + \name{author}{3}{}{% + {{un=0,uniquepart=base,hash=75c00f0c05731e74db1167b4a8fe5936}{% + family={Rougier}, + familyi={R\bibinitperiod}, + given={Nicolas\bibnamedelima P.}, + giveni={N\bibinitperiod\bibinitdelim P\bibinitperiod}, + givenun=0}}% + {{un=0,uniquepart=base,hash=4778156ee049a7c63b5dfde9beea6d36}{% + family={Droettboom}, + familyi={D\bibinitperiod}, + given={Michael}, + giveni={M\bibinitperiod}, + givenun=0}}% + {{un=0,uniquepart=base,hash=e8f011b361a44c99e0b3cd105b7553dc}{% + family={Bourne}, + familyi={B\bibinitperiod}, + given={Philip\bibnamedelima E.}, + giveni={P\bibinitperiod\bibinitdelim E\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {Public Library of Science ({PLoS})}% + } + \strng{namehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{fullhash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{bibnamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authorbibnamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authornamehash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \strng{authorfullhash}{7a77fd6d2261e11e7d0213e48ac6d76b} + \field{sortinit}{R} + \field{sortinithash}{5e1c39a9d46ffb6bebd8f801023a9486} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$$\bullet$ This classic and popular article provides a basic set of rules to improve figure design and explain some of the common pitfalls.} + \field{journaltitle}{{PLoS} Computational Biology} + \field{number}{9} + \field{title}{{Ten Simple Rules for Better Figures}} + \field{volume}{10} + \field{year}{2014} + \true{nocite} + \verb{doi} + \verb 10.1371/journal.pcbi.1003833 + \endverb + \endentry + \entry{Vanderplas:2016}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=0fd9a0e34f1b2adda41357c948d14986}{% + family={VanderPlas}, + familyi={V\bibinitperiod}, + given={Jake}, + giveni={J\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {O'Reilly}% + } + \strng{namehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{fullhash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{bibnamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authorbibnamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authornamehash}{0fd9a0e34f1b2adda41357c948d14986} + \strng{authorfullhash}{0fd9a0e34f1b2adda41357c948d14986} + \field{sortinit}{V} + \field{sortinithash}{afb52128e5b4dc4b843768c0113d673b} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ This book is meant to help Python users learn to use Python's data science stack–libraries such as IPython, NumPy, Pandas, Matplotlib, Scikit-Learn, and related tools–to effectively store, manipulate, and gain insight from data.} + \field{title}{{Python Data Science Handbook}} + \field{year}{2016} + \true{nocite} + \verb{urlraw} + \verb https://jakevdp.github.io/PythonDataScienceHandbook/index.html + \endverb + \verb{url} + \verb https://jakevdp.github.io/PythonDataScienceHandbook/index.html + \endverb + \endentry + \entry{Wilke:2019}{book}{} + \name{author}{1}{}{% + {{un=0,uniquepart=base,hash=8e86aa1915156f8016600d977acef303}{% + family={Wilke}, + familyi={W\bibinitperiod}, + given={Claus\bibnamedelima O.}, + giveni={C\bibinitperiod\bibinitdelim O\bibinitperiod}, + givenun=0}}% + } + \list{publisher}{1}{% + {O'Reilly}% + } + \strng{namehash}{8e86aa1915156f8016600d977acef303} + \strng{fullhash}{8e86aa1915156f8016600d977acef303} + \strng{bibnamehash}{8e86aa1915156f8016600d977acef303} + \strng{authorbibnamehash}{8e86aa1915156f8016600d977acef303} + \strng{authornamehash}{8e86aa1915156f8016600d977acef303} + \strng{authorfullhash}{8e86aa1915156f8016600d977acef303} + \field{sortinit}{W} + \field{sortinithash}{4315d78024d0cea9b57a0c6f0e35ed0d} + \field{extradatescope}{labelyear} + \field{labeldatesource}{year} + \field{labelnamesource}{author} + \field{labeltitlesource}{title} + \field{abstract}{$\bullet$$\bullet$ This open access book takes you through many commonly encountered visualization problems, and it provides guidelines on how to turn large datasets into clear and compelling figures.} + \field{title}{Fundamentals of Data Visualization} + \field{year}{2019} + \true{nocite} + \verb{urlraw} + \verb https://serialmentor.com/dataviz/ + \endverb + \verb{url} + \verb https://serialmentor.com/dataviz/ + \endverb + \endentry + \enddatalist +\endrefsection +\endinput +