├── .gitattributes ├── pandoc ├── abbreviations.txt ├── pandoc-crossref.yaml └── revtex.template ├── figures ├── algo.pdf ├── isoline.pdf ├── loss_1D.pdf ├── Learner1D.pdf ├── Learner2D.pdf ├── line_loss.pdf └── line_loss_error.pdf ├── .gitmodules ├── environment.yml ├── paper.yaml ├── Makefile ├── README.md ├── .gitlab-ci.yml ├── ipynb_filter.py ├── Dockerfile ├── replacements.yaml ├── not_on_crossref.bib ├── .gitignore ├── phase_diagram.ipynb ├── paper.bib ├── pfaffian.py ├── phase_diagram.py ├── figures.ipynb └── paper.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb filter=ipynb_filter 2 | -------------------------------------------------------------------------------- /pandoc/abbreviations.txt: -------------------------------------------------------------------------------- 1 | Fig. 2 | Eq. 3 | i.e. 4 | e.g. 5 | -------------------------------------------------------------------------------- /figures/algo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/algo.pdf -------------------------------------------------------------------------------- /figures/isoline.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/isoline.pdf -------------------------------------------------------------------------------- /figures/loss_1D.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/loss_1D.pdf -------------------------------------------------------------------------------- /figures/Learner1D.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/Learner1D.pdf -------------------------------------------------------------------------------- /figures/Learner2D.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/Learner2D.pdf -------------------------------------------------------------------------------- /figures/line_loss.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/line_loss.pdf -------------------------------------------------------------------------------- /pandoc/pandoc-crossref.yaml: -------------------------------------------------------------------------------- 1 | figPrefix: "Fig." 2 | secPrefix: "Sec." 3 | autoSectionLabels: false 4 | -------------------------------------------------------------------------------- /figures/line_loss_error.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-adaptive/paper/HEAD/figures/line_loss_error.pdf -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "adaptive"] 2 | path = adaptive_repo 3 | url = https://github.com/python-adaptive/adaptive/ 4 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: revtex-markdown-paper 2 | 3 | channels: 4 | - conda-forge 5 | 6 | dependencies: 7 | - python 8 | - pandoc 9 | - pandoc-crossref 10 | - pip: 11 | - pandoc-fignos 12 | -------------------------------------------------------------------------------- /paper.yaml: -------------------------------------------------------------------------------- 1 | Alliez2003: 10.1145/882262.882296 2 | Berger1984: 10.1016/0021-9991(84)90073-1 3 | Berger1989: 10.1016/0021-9991(89)90035-1 4 | Bommer2019: 10.1103/physrevlett.122.187702 5 | Chen2017: 10.1088/1361-6501/aa7d31 6 | Clenshaw1960: 10.1007/bf01386223 7 | DeRose1998: 10.1145/280814.280826 8 | Dyn1990: 10.1093/imanum/10.1.137 9 | Emery1998: 10.1088/0957-0233/9/6/003 10 | Figueiredo1995: 10.1016/B978-0-12-543457-7.50032-2 11 | Gonnet2010: 10.1145/1824801.1824804 12 | Gramacy2004: 10.1145/1015330.1015367 13 | Hu2006: 10.1002/047005588x 14 | Klein1999: 10.1016/s0377-0427(99)00156-9 15 | Melo2019: 10.21468/SciPostPhys.7.3.039 16 | Nijholt2016: 10.1103/PhysRevB.93.235434 17 | Visvalingam1990: 10.1111/j.1467-8659.1990.tb00398.x 18 | Vuik2018: 10.21468/SciPostPhys.7.5.061 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | paper.pdf: paper.bbl paper.tex figures/*.pdf 2 | pdflatex paper.tex 3 | pdflatex paper.tex 4 | 5 | paper.bbl: paper.tex paper.bib 6 | pdflatex paper.tex 7 | bibtex paper.aux 8 | 9 | paper.tex: Makefile paper.md pandoc/revtex.template 10 | pandoc \ 11 | --read=markdown-auto_identifiers \ 12 | --filter=pandoc-fignos \ 13 | --filter=pandoc-citeproc \ 14 | --filter=pandoc-crossref \ 15 | --metadata="crossrefYaml=pandoc/pandoc-crossref.yaml" \ 16 | --output=paper.tex \ 17 | --bibliography=paper.bib \ 18 | --abbreviations=pandoc/abbreviations.txt \ 19 | --wrap=preserve \ 20 | --template=pandoc/revtex.template \ 21 | --standalone \ 22 | --natbib \ 23 | --listings \ 24 | paper.md 25 | 26 | .PHONY: all clean 27 | 28 | clean: 29 | rm -f paper.pdf paper.aux paper.blg paper.bbl paper.log paper.tex paperNotes.bib 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adaptive 2 | 3 | This is the companion paper to the [adaptive](https://adaptive.readthedocs.io/en/latest/) Python library. 4 | 5 | See the latest draft [here](https://gitlab.kwant-project.org/qt/adaptive-paper/builds/artifacts/master/file/paper.pdf?job=make). 6 | 7 | ### Building the paper 8 | 9 | The simplest way to build the paper is with Docker, to make sure that all the necessary dependencies are installed. 10 | First build the Docker image: 11 | 12 | ``` 13 | docker build -t adaptive-paper . 14 | ``` 15 | 16 | Then run `make` inside a docker container using the image you just built: 17 | 18 | ``` 19 | docker run -it --rm -v $(pwd):/work -w /work adaptive-paper make 20 | ``` 21 | 22 | ### Update the bibliography (using [`yaml2bib`](https://github.com/basnijholt/yaml2bib)) 23 | 24 | ```bash 25 | yaml2bib \ 26 | --bib_fname "paper.bib" \ 27 | --dois_yaml "paper.yaml" \ 28 | --replacements_yaml "replacements.yaml" \ 29 | --static_bib "not_on_crossref.bib" \ 30 | --email "bas@nijho.lt" 31 | ``` 32 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: gitlab.kwant-project.org:5005/qt/adaptive-paper 2 | 3 | stages: 4 | - build-env 5 | - test 6 | 7 | ## Building Docker environments 8 | ## Only runs when docker specifications change 9 | 10 | .build-env: &build-env 11 | stage: build-env 12 | only: 13 | changes: 14 | - Dockerfile 15 | - environment.yml 16 | image: 17 | name: gcr.io/kaniko-project/executor:debug 18 | entrypoint: [""] 19 | artifacts: 20 | untracked: true 21 | expire_in: 1 hour 22 | before_script: 23 | - mkdir -p /kaniko/.docker 24 | - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json 25 | 26 | build-env:ubuntu: &build-docker 27 | <<: *build-env 28 | only: 29 | changes: 30 | - Dockerfile 31 | - environment.yml 32 | script: 33 | - /kaniko/executor 34 | --context $CI_PROJECT_DIR 35 | --dockerfile $CI_PROJECT_DIR/Dockerfile 36 | --destination $CI_REGISTRY_IMAGE 37 | 38 | 39 | ## Test Jobs 40 | 41 | make: 42 | stage: test 43 | script: 44 | - source activate revtex-markdown-paper && make 45 | artifacts: 46 | paths: 47 | - paper.pdf 48 | - paper.tex 49 | -------------------------------------------------------------------------------- /ipynb_filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # `ipynb_filter.py`: 4 | # This is a git filters that strips out the outputs and 5 | # meta data of a Jupyer notebook using `nbconvert`. 6 | # Execute the following line in order to activate this filter: 7 | # python ipynb_filter.py 8 | # 9 | # The following line should be in `.gitattributes`: 10 | # *.ipynb filter=ipynb_filter 11 | # 12 | # from github.com/basnijholt/ipynb_git_filters 13 | 14 | from nbconvert.preprocessors import Preprocessor 15 | 16 | 17 | class RemoveMetadata(Preprocessor): 18 | def preprocess(self, nb, resources): 19 | nb.metadata = { 20 | "language_info": {"name": "python", "pygments_lexer": "ipython3"} 21 | } 22 | return nb, resources 23 | 24 | 25 | if __name__ == "__main__": 26 | # The filter is getting activated 27 | import os 28 | 29 | git_cmd = 'git config filter.ipynb_filter.clean "jupyter nbconvert --to notebook --config ipynb_filter.py --stdin --stdout"' 30 | os.system(git_cmd) 31 | else: 32 | # This script is used as config 33 | c.Exporter.preprocessors = [RemoveMetadata] 34 | c.ClearOutputPreprocessor.enabled = True 35 | c.ClearOutputPreprocessor.remove_metadata_fields = [ 36 | "deletable", 37 | "editable", 38 | "collapsed", 39 | "scrolled", 40 | ] 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | MAINTAINER Bas Nijholt 3 | 4 | ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 5 | ENV PATH /opt/conda/bin:$PATH 6 | 7 | RUN apt-get update --fix-missing && \ 8 | apt-get install -y \ 9 | # for miniconda 10 | wget bzip2 ca-certificates curl git \ 11 | # for TeX 12 | texlive-full python-pygments gnuplot make \ 13 | # for gcc 14 | build-essential \ 15 | && \ 16 | apt-get clean && \ 17 | rm -rf /var/lib/apt/lists/* 18 | 19 | RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-4.5.11-Linux-x86_64.sh -O ~/miniconda.sh && \ 20 | /bin/bash ~/miniconda.sh -b -p /opt/conda && \ 21 | rm ~/miniconda.sh && \ 22 | /opt/conda/bin/conda clean -tipsy && \ 23 | ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \ 24 | echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc 25 | 26 | ENV TINI_VERSION v0.16.1 27 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/bin/tini 28 | RUN chmod +x /usr/bin/tini 29 | 30 | RUN mkdir /environments 31 | COPY environment.yml /environments/ 32 | 33 | RUN conda-env create -f /environments/environment.yml && \ 34 | # ensure that we activate the environment in any shell (Gitlab CI uses 'sh') 35 | echo "conda activate revtex-markdown-paper" >> ~/.bashrc 36 | 37 | ENTRYPOINT [ "/usr/bin/tini", "--" ] 38 | CMD [ "/bin/bash" ] 39 | -------------------------------------------------------------------------------- /replacements.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # `from: to` format 3 | replacements: 4 | Andreev: '{A}ndreev' 5 | Josephson: '{J}osephson' 6 | Kitaev: '{K}itaev' 7 | Kramers: '{K}ramers' 8 | Land{\'{e}}{gFactors}: Land{\'{e}} {$g$} Factors # fix for PhysRevLett.96.026804 9 | Majorana: '{M}ajorana' 10 | apx$\mathplus$ipysuperconductor: a $p_x + i p_y$ superconductor # fix for 10.1103/physrevb.73.220502 11 | apx$\mathplus${ipySuperfluid}: $p_x + i p_y$ superfluid # fix for 10.1103/physrevlett.98.010506 12 | a{\r}: \r{a} # "Nyga{\r}rd" -> "Nyg\r{a}rd", bug in doi.org 13 | metastable0and$\uppi$states: metastable $0$ and $\pi$ states # fix for 10.1103/physrevb.63.214512 14 | NIRA DYN and DAVID LEVIN and SAMUEL RIPPA: Nira Dyn and David Levin and Samuel Rippa # fix for 10.1093/imanum/10.1.137 15 | 16 | journals: 17 | Advanced Materials: Adv. Mater. 18 | Applied Physics Letters: Appl. Phys. Lett. 19 | Journal of Computational Physics: J. Comput. Phys. 20 | Journal of Low Temperature Physics: J. Low Temp. Phys. 21 | Nature Communications: Nat. Commun. 22 | Nature Materials: Nat. Mater. 23 | Nature Nanotechnology: Nat. Nanotechnol. 24 | Nature Physics: Nat. Phys. 25 | Physics-Uspekhi: Phys. Usp. 26 | Review of Scientific Instruments: Rev. Sci. Instrum. 27 | Scientific Reports: Sci. Rep. 28 | '{EPL} (Europhysics Letters)': '{EPL}' 29 | Nature Reviews Materials: Nat. Rev. Mater. 30 | Physics Letters: Phys. Lett. 31 | '{SIAM} Journal on Numerical Analysis': '{SIAM} J. Numer. Anal.' 32 | '{AIP} Conference Proceedings': '{AIP} Conf. Proc.' 33 | '{IMA} Journal of Numerical Analysis': '{IMA} J. Appl. Math.' 34 | Journal of Computational and Applied Mathematics: J. Comput. Appl. Math 35 | '{SciPost} Physics': '{SciPost} Phys.' 36 | '{ACM} Transactions on Graphics': '{ACM} Trans. Graph.' 37 | Computer Graphics Forum: Comput. Graphics Forum 38 | '{ACM} Transactions on Mathematical Software': '{ACM} Trans. Math. Softw.' -------------------------------------------------------------------------------- /not_on_crossref.bib: -------------------------------------------------------------------------------- 1 | @misc{papercode, 2 | author = {Bas Nijholt and Joseph Weston and Anton Akhmerov}, 3 | title = {Adaptive: parallel active learning of mathematical functions}, 4 | year = {2020}, 5 | publisher = {GitHub}, 6 | journal = {GitHub repository}, 7 | howpublished = {\url{https://github.com/python-adaptive/paper/}}, 8 | commit = {ebf381e9e0018808684c6a4199d04d96b35e936c} 9 | } 10 | 11 | @Article{Nijholt2019a, 12 | doi = {10.5281/zenodo.1182437}, 13 | author = {Bas Nijholt and Joseph Weston and Jorn Hoofwijk and Anton Akhmerov}, 14 | title = {\textit{Adaptive}: parallel active learning of mathematical functions}, 15 | journal = {Zenodo} 16 | } 17 | 18 | @PhdThesis{Castro2008, 19 | author = {Castro, Rui M}, 20 | title = {Active learning and adaptive sampling for non-parametric inference}, 21 | year = {2008}, 22 | school = {Rice University}, 23 | } 24 | 25 | @Book{Cormen2009, 26 | author = {Cormen, Thomas H. and Leiserson, Charles E. and Rivest, Ronald L. and Stein, Clifford}, 27 | title = {Introduction to Algorithms}, 28 | year = {2009}, 29 | edition = {3rd}, 30 | publisher = {The MIT Press}, 31 | isbn = {0262033844, 9780262033848}, 32 | } 33 | 34 | @Article{Galassi1996, 35 | author = {Galassi, Mark and Davies, Jim and Theiler, James and Gough, Brian and Jungman, Gerard and Alken, Patrick and Booth, Michael and Rossi, Fabrice}, 36 | title = {GNU scientific library}, 37 | journal = {No. Release}, 38 | year = {1996}, 39 | volume = {2}, 40 | } 41 | 42 | @Online{Jenks2014, 43 | author = {Grant Jenks}, 44 | title = {Python Sorted Containers}, 45 | year = {2014}, 46 | url = {http://www.grantjenks.com/docs/sortedcontainers}, 47 | urldate = {2019-10-04}, 48 | } 49 | 50 | @Article{Laeven2019, 51 | author = {Laeven, Tom and Nijholt, Bas and Wimmer, Michael and Akhmerov, Anton R}, 52 | title = {Enhanced proximity effect in zigzag-shaped Majorana Josephson junctions}, 53 | journal = {arXiv preprint arXiv:1903.06168}, 54 | year = {2019}, 55 | } 56 | 57 | @Misc{Nijholt2018, 58 | author = {Bas Nijholt and Joseph Weston and Anton Akhmerov}, 59 | title = {Adaptive documentation}, 60 | year = {2018}, 61 | note = {https://adaptive.readthedocs.io}, 62 | } 63 | 64 | @Article{Takhtaganov2018, 65 | author = {Takhtaganov, Timur and Müller, Juliane}, 66 | title = {Adaptive Gaussian process surrogates for Bayesian inference}, 67 | journal = {arXiv preprint arXiv:1809.10784}, 68 | year = {2018}, 69 | } 70 | 71 | @Online{Wolfram2011, 72 | author = {Stephen Wolfram}, 73 | title = {Mathematica: Adaptive Plotting}, 74 | year = {2011}, 75 | url = {http://demonstrations.wolfram.com/AdaptivePlotting/}, 76 | urldate = {2019-09-10}, 77 | } 78 | 79 | @Misc{WolframResearch, 80 | author = {Wolfram Research, Inc.}, 81 | title = {Mathematica, Version 12.0}, 82 | note = {Champaign, IL, 2019}, 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Core latex/pdflatex auxiliary files: 2 | *.aux 3 | *.lof 4 | *.log 5 | *.lot 6 | *.fls 7 | *.out 8 | *.toc 9 | *.fmt 10 | *.fot 11 | *.cb 12 | *.cb2 13 | .*.lb 14 | 15 | ## Intermediate documents: 16 | *.dvi 17 | *.xdv 18 | *-converted-to.* 19 | # these rules might exclude image files for figures etc. 20 | # *.ps 21 | # *.eps 22 | # *.pdf 23 | 24 | ## Generated if empty string is given at "Please type another file name for output:" 25 | .pdf 26 | 27 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 28 | *.bbl 29 | *.bcf 30 | *.blg 31 | *-blx.aux 32 | *-blx.bib 33 | *.run.xml 34 | 35 | ## Build tool auxiliary files: 36 | *.fdb_latexmk 37 | *.synctex 38 | *.synctex(busy) 39 | *.synctex.gz 40 | *.synctex.gz(busy) 41 | *.pdfsync 42 | 43 | ## Auxiliary and intermediate files from other packages: 44 | # algorithms 45 | *.alg 46 | *.loa 47 | 48 | # achemso 49 | acs-*.bib 50 | 51 | # amsthm 52 | *.thm 53 | 54 | # beamer 55 | *.nav 56 | *.pre 57 | *.snm 58 | *.vrb 59 | 60 | # changes 61 | *.soc 62 | 63 | # cprotect 64 | *.cpt 65 | 66 | # elsarticle (documentclass of Elsevier journals) 67 | *.spl 68 | 69 | # endnotes 70 | *.ent 71 | 72 | # fixme 73 | *.lox 74 | 75 | # feynmf/feynmp 76 | *.mf 77 | *.mp 78 | *.t[1-9] 79 | *.t[1-9][0-9] 80 | *.tfm 81 | 82 | #(r)(e)ledmac/(r)(e)ledpar 83 | *.end 84 | *.?end 85 | *.[1-9] 86 | *.[1-9][0-9] 87 | *.[1-9][0-9][0-9] 88 | *.[1-9]R 89 | *.[1-9][0-9]R 90 | *.[1-9][0-9][0-9]R 91 | *.eledsec[1-9] 92 | *.eledsec[1-9]R 93 | *.eledsec[1-9][0-9] 94 | *.eledsec[1-9][0-9]R 95 | *.eledsec[1-9][0-9][0-9] 96 | *.eledsec[1-9][0-9][0-9]R 97 | 98 | # glossaries 99 | *.acn 100 | *.acr 101 | *.glg 102 | *.glo 103 | *.gls 104 | *.glsdefs 105 | 106 | # gnuplottex 107 | *-gnuplottex-* 108 | 109 | # gregoriotex 110 | *.gaux 111 | *.gtex 112 | 113 | # htlatex 114 | *.4ct 115 | *.4tc 116 | *.idv 117 | *.lg 118 | *.trc 119 | *.xref 120 | 121 | # hyperref 122 | *.brf 123 | 124 | # knitr 125 | *-concordance.tex 126 | # TODO Comment the next line if you want to keep your tikz graphics files 127 | *.tikz 128 | *-tikzDictionary 129 | 130 | # listings 131 | *.lol 132 | 133 | # makeidx 134 | *.idx 135 | *.ilg 136 | *.ind 137 | *.ist 138 | 139 | # minitoc 140 | *.maf 141 | *.mlf 142 | *.mlt 143 | *.mtc[0-9]* 144 | *.slf[0-9]* 145 | *.slt[0-9]* 146 | *.stc[0-9]* 147 | 148 | # minted 149 | _minted* 150 | *.pyg 151 | 152 | # morewrites 153 | *.mw 154 | 155 | # nomencl 156 | *.nlg 157 | *.nlo 158 | *.nls 159 | 160 | # pax 161 | *.pax 162 | 163 | # pdfpcnotes 164 | *.pdfpc 165 | 166 | # sagetex 167 | *.sagetex.sage 168 | *.sagetex.py 169 | *.sagetex.scmd 170 | 171 | # scrwfile 172 | *.wrt 173 | 174 | # sympy 175 | *.sout 176 | *.sympy 177 | sympy-plots-for-*.tex/ 178 | 179 | # pdfcomment 180 | *.upa 181 | *.upb 182 | 183 | # pythontex 184 | *.pytxcode 185 | pythontex-files-*/ 186 | 187 | # thmtools 188 | *.loe 189 | 190 | # TikZ & PGF 191 | *.dpth 192 | *.md5 193 | *.auxlock 194 | 195 | # todonotes 196 | *.tdo 197 | 198 | # easy-todo 199 | *.lod 200 | 201 | # xmpincl 202 | *.xmpi 203 | 204 | # xindy 205 | *.xdy 206 | 207 | # xypic precompiled matrices 208 | *.xyc 209 | 210 | # endfloat 211 | *.ttt 212 | *.fff 213 | 214 | # Latexian 215 | TSWLatexianTemp* 216 | 217 | ## Editors: 218 | # WinEdt 219 | *.bak 220 | *.sav 221 | 222 | # Texpad 223 | .texpadtmp 224 | 225 | # Kile 226 | *.backup 227 | 228 | # KBibTeX 229 | *~[0-9]* 230 | 231 | # auto folder when using emacs and auctex 232 | ./auto/* 233 | *.el 234 | 235 | # expex forward references with \gathertags 236 | *-tags.tex 237 | 238 | # standalone packages 239 | *.sta 240 | 241 | # generated if using elsarticle.cls 242 | *.spl 243 | -------------------------------------------------------------------------------- /pandoc/revtex.template: -------------------------------------------------------------------------------- 1 | \documentclass[english, twocolumn, 10pt, aps, superscriptaddress, floatfix, prb, citeautoscript]{revtex4-1} 2 | \pdfoutput=1 3 | \usepackage[utf8]{inputenc} 4 | \usepackage[T1]{fontenc} 5 | \usepackage{listings} 6 | \usepackage{units} 7 | \usepackage{mathtools} 8 | \usepackage{amsmath} 9 | \usepackage{amssymb} 10 | \usepackage{graphicx} 11 | \usepackage{wasysym} 12 | \usepackage{layouts} 13 | \usepackage{siunitx} 14 | \usepackage{bm} 15 | \usepackage{xcolor} 16 | \usepackage[colorlinks, citecolor={blue!50!black}, urlcolor={blue!50!black}, linkcolor={red!50!black}]{hyperref} 17 | \usepackage{bookmark} 18 | \usepackage{tabularx} 19 | \usepackage{microtype} 20 | \usepackage{babel} 21 | \usepackage{textcomp} 22 | \hypersetup{pdfauthor={$for(author)$$author.name$$sep$, $endfor$},pdftitle={$if(title)$$title$$endif$}} 23 | 24 | \setcounter{secnumdepth}{4} 25 | \setcounter{tocdepth}{4} 26 | 27 | \newcounter{CommentNumber} 28 | % \renewcommand{\paragraph}[1]{\stepcounter{CommentNumber}\belowpdfbookmark{#1}{\arabic{CommentNumber}}} 29 | 30 | \DeclarePairedDelimiter\abs{\lvert}{\rvert} 31 | \DeclarePairedDelimiter\norm{\lVert}{\rVert} 32 | 33 | \makeatletter 34 | \let\oldabs\abs 35 | \def\abs{\@ifstar{\oldabs}{\oldabs*}} 36 | \let\oldnorm\norm 37 | \def\norm{\@ifstar{\oldnorm}{\oldnorm*}} 38 | \makeatother 39 | 40 | \newcommand{\ev}[1]{\langle#1\rangle} 41 | \newcommand{\bra}[1]{\langle#1|} 42 | \newcommand{\ket}[1]{|#1\rangle} 43 | \newcommand{\bracket}[2]{\langle#1|#2\rangle} 44 | 45 | \newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}} 46 | \newcolumntype{C}[1]{>{\centering\arraybackslash}p{#1}} 47 | \newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}} 48 | 49 | % workaround for https://github.com/jgm/pandoc/issues/2392#issuecomment-140114736 50 | \renewcommand{\citep}{\cite} 51 | 52 | % workaround for https://github.com/jgm/pandoc/issues/4716 53 | \newcommand{\passthrough}[1]{\lstset{mathescape=false}#1\lstset{mathescape=true}} 54 | 55 | % listing settings, from https://tex.stackexchange.com/a/179956 56 | \lstset{ 57 | basicstyle=\ttfamily, 58 | numbers=left, 59 | keywordstyle=\color[rgb]{0.13,0.29,0.53}\bfseries, 60 | stringstyle=\color[rgb]{0.31,0.60,0.02}, 61 | commentstyle=\color[rgb]{0.56,0.35,0.01}\itshape, 62 | numberstyle=\footnotesize, 63 | stepnumber=1, 64 | numbersep=5pt, 65 | backgroundcolor=\color[RGB]{248,248,248}, 66 | showspaces=false, 67 | showstringspaces=false, 68 | showtabs=false, 69 | tabsize=2, 70 | captionpos=b, 71 | breaklines=true, 72 | breakatwhitespace=true, 73 | breakautoindent=true, 74 | escapeinside={\%*}{*)}, 75 | linewidth=\columnwidth, 76 | basewidth=0.5em, 77 | } 78 | 79 | 80 | \begin{document} 81 | 82 | \title{$if(title)$$title$$endif$} 83 | 84 | $for(author)$ 85 | $if(author.name)$ 86 | \author{$author.name$} 87 | \email[Electronic address: ]{$author.email$} 88 | $for(author.affiliation)$ 89 | \affiliation{$author.affiliation$} 90 | $endfor$ 91 | $endif$ 92 | $endfor$ 93 | 94 | \date{\today} 95 | 96 | $if(abstract)$ 97 | \begin{abstract} 98 | $abstract$ 99 | \end{abstract} 100 | $endif$ 101 | 102 | \flushbottom 103 | \maketitle 104 | 105 | $body$ 106 | 107 | $if(acknowledgements)$ 108 | \section*{Acknowledgements} 109 | $acknowledgements$ 110 | $endif$ 111 | 112 | $if(contribution)$ 113 | \section*{Author contributions statement} 114 | $contribution$ 115 | $endif$ 116 | 117 | $if(additionalinformation)$ 118 | \section*{Additional information} 119 | $additionalinformation$ 120 | $additionalinformation$ 121 | $endif$ 122 | 123 | \bibliographystyle{apsrev4-1} 124 | \bibliography{$bibliography$} 125 | 126 | \end{document} 127 | -------------------------------------------------------------------------------- /phase_diagram.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%writefile learners_file.py\n", 10 | "\n", 11 | "import adaptive\n", 12 | "from functools import partial\n", 13 | "\n", 14 | "import phase_diagram\n", 15 | "\n", 16 | "lead_pars = dict(\n", 17 | " a=10, r1=50, r2=70, coverage_angle=135, angle=45, with_shell=True, which_lead=\"\"\n", 18 | ")\n", 19 | "\n", 20 | "params = dict(\n", 21 | " alpha=20,\n", 22 | " B_x=0,\n", 23 | " B_y=0,\n", 24 | " B_z=0,\n", 25 | " Delta=110,\n", 26 | " g=50,\n", 27 | " orbital=True,\n", 28 | " mu_sc=100,\n", 29 | " c_tunnel=3 / 4,\n", 30 | " V_r=-50,\n", 31 | " mu_=\"lambda x0, sigma, mu_lead, mu_wire: mu_lead\",\n", 32 | " V_=\"lambda z, V_0, V_r, V_l, x0, sigma, r1: 0\",\n", 33 | " V_0=None,\n", 34 | " V_l=None,\n", 35 | " mu_lead=10,\n", 36 | " mu_wire=None,\n", 37 | " r1=None,\n", 38 | " sigma=None,\n", 39 | " x0=None,\n", 40 | " **phase_diagram.constants.__dict__\n", 41 | ")\n", 42 | "\n", 43 | "\n", 44 | "def pf(xy, params=params, lead_pars=lead_pars):\n", 45 | " import phase_diagram\n", 46 | "\n", 47 | " params[\"B_x\"], params[\"mu_lead\"] = xy\n", 48 | " lead = phase_diagram.make_lead(**lead_pars).finalized()\n", 49 | " return phase_diagram.calculate_pfaffian(lead, params)\n", 50 | "\n", 51 | "\n", 52 | "def smallest_gap(xy, params=params, lead_pars=lead_pars):\n", 53 | " import phase_diagram\n", 54 | "\n", 55 | " params[\"B_x\"], params[\"mu_lead\"] = xy\n", 56 | " params = phase_diagram.parse_params(params)\n", 57 | " lead = phase_diagram.make_lead(**lead_pars).finalized()\n", 58 | " pf = phase_diagram.calculate_pfaffian(lead, params)\n", 59 | " gap = phase_diagram.gap_from_modes(lead, params)\n", 60 | " return pf * gap\n", 61 | "\n", 62 | "\n", 63 | "fnames = [\n", 64 | "# \"phase_diagram_gap.pickle\",\n", 65 | "# \"phase_diagram_gap_no_orbital.pickle\",\n", 66 | "# \"phase_diagram_gap_sc_inside.pickle\",\n", 67 | " \"phase_diagram_gap_sc_inside_no_orbital.pickle\",\n", 68 | "]\n", 69 | "loss = adaptive.learner.learnerND.curvature_loss_function()\n", 70 | "\n", 71 | "learners = []\n", 72 | "for sc_inside_wire, orbital, Delta in (\n", 73 | "# [False, True, 110],\n", 74 | "# [False, False, 110],\n", 75 | "# [True, True, 0.25],\n", 76 | " [True, False, 0.25],\n", 77 | "):\n", 78 | " f = partial(\n", 79 | " smallest_gap,\n", 80 | " params=dict(params, orbital=orbital, Delta=Delta),\n", 81 | " lead_pars=dict(\n", 82 | " lead_pars, sc_inside_wire=sc_inside_wire, with_shell=(not sc_inside_wire)\n", 83 | " ),\n", 84 | " )\n", 85 | " learners.append(adaptive.Learner2D(f, bounds=[(0, 2), (0, 35)]))\n", 86 | "learner = adaptive.BalancingLearner(learners, strategy=\"npoints\")\n", 87 | "\n", 88 | "learner.load(fnames)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "from learners_file import *" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "import adaptive\n", 107 | "\n", 108 | "adaptive.notebook_extension()\n", 109 | "runner = adaptive.Runner(learner, goal=lambda l: l.learners[-1].npoints > 20000)\n", 110 | "runner.live_info()" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "import kwant\n", 120 | "%matplotlib inline\n", 121 | "kwant.plot(phase_diagram.make_lead(**f.keywords['lead_pars']))" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "runner.start_periodic_saving(dict(fname=fnames), 900)" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": {}, 137 | "outputs": [], 138 | "source": [ 139 | "learners[0].plot(n=200, tri_alpha=0.2).Image.I[:, 6:]" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "%%output size=100\n", 149 | "%%opts Image [colorbar=True clim=(-0.1, 0)] \n", 150 | "learners[1].plot(tri_alpha=0.4).Image.I[:, 10:]" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "runner.live_info()" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "learners[1].npoints" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "for l, f in zip(learners, fnames):\n", 178 | " l.save(f)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": null, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "import adaptive_scheduler\n", 188 | "\n", 189 | "def goal(learner):\n", 190 | " return learner.npoints > 200\n", 191 | "\n", 192 | "scheduler = adaptive_scheduler.scheduler.DefaultScheduler(\n", 193 | " cores=40,\n", 194 | " executor_type=\"ipyparallel\",\n", 195 | ") # PBS or SLURM\n", 196 | "\n", 197 | "run_manager = adaptive_scheduler.server_support.RunManager(\n", 198 | " scheduler=scheduler,\n", 199 | " learners_file=\"learners_file.py\",\n", 200 | " goal=goal,\n", 201 | " log_interval=30,\n", 202 | " save_interval=30,\n", 203 | " job_name='phase-diagram'\n", 204 | ")\n", 205 | "run_manager.start()" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "from learners_file import *" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": {}, 221 | "outputs": [], 222 | "source": [ 223 | "adaptive.notebook_extension()" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": null, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "learner = learners[0]\n", 233 | "learner.plot(n=100)" 234 | ] 235 | } 236 | ], 237 | "metadata": { 238 | "language_info": { 239 | "name": "python", 240 | "pygments_lexer": "ipython3" 241 | } 242 | }, 243 | "nbformat": 4, 244 | "nbformat_minor": 2 245 | } 246 | -------------------------------------------------------------------------------- /paper.bib: -------------------------------------------------------------------------------- 1 | @preamble{ {\providecommand{\BIBYu}{Yu} } } 2 | 3 | 4 | % Below is from `not_on_crossref.bib`. 5 | 6 | @misc{papercode, 7 | author = {Bas Nijholt and Joseph Weston and Anton Akhmerov}, 8 | title = {Adaptive: parallel active learning of mathematical functions}, 9 | year = {2020}, 10 | publisher = {GitHub}, 11 | journal = {GitHub repository}, 12 | howpublished = {\url{https://github.com/python-adaptive/paper/}}, 13 | commit = {ebf381e9e0018808684c6a4199d04d96b35e936c} 14 | } 15 | 16 | @Article{Nijholt2019a, 17 | doi = {10.5281/zenodo.1182437}, 18 | author = {Bas Nijholt and Joseph Weston and Jorn Hoofwijk and Anton Akhmerov}, 19 | title = {\textit{Adaptive}: parallel active learning of mathematical functions}, 20 | journal = {Zenodo} 21 | } 22 | 23 | @PhdThesis{Castro2008, 24 | author = {Castro, Rui M}, 25 | title = {Active learning and adaptive sampling for non-parametric inference}, 26 | year = {2008}, 27 | school = {Rice University}, 28 | } 29 | 30 | @Book{Cormen2009, 31 | author = {Cormen, Thomas H. and Leiserson, Charles E. and Rivest, Ronald L. and Stein, Clifford}, 32 | title = {Introduction to Algorithms}, 33 | year = {2009}, 34 | edition = {3rd}, 35 | publisher = {The MIT Press}, 36 | isbn = {0262033844, 9780262033848}, 37 | } 38 | 39 | @Article{Galassi1996, 40 | author = {Galassi, Mark and Davies, Jim and Theiler, James and Gough, Brian and Jungman, Gerard and Alken, Patrick and Booth, Michael and Rossi, Fabrice}, 41 | title = {GNU scientific library}, 42 | journal = {No. Release}, 43 | year = {1996}, 44 | volume = {2}, 45 | } 46 | 47 | @Online{Jenks2014, 48 | author = {Grant Jenks}, 49 | title = {Python Sorted Containers}, 50 | year = {2014}, 51 | url = {http://www.grantjenks.com/docs/sortedcontainers}, 52 | urldate = {2019-10-04}, 53 | } 54 | 55 | @Article{Laeven2019, 56 | author = {Laeven, Tom and Nijholt, Bas and Wimmer, Michael and Akhmerov, Anton R}, 57 | title = {Enhanced proximity effect in zigzag-shaped Majorana Josephson junctions}, 58 | journal = {arXiv preprint arXiv:1903.06168}, 59 | year = {2019}, 60 | } 61 | 62 | @Misc{Nijholt2018, 63 | author = {Bas Nijholt and Joseph Weston and Anton Akhmerov}, 64 | title = {Adaptive documentation}, 65 | year = {2018}, 66 | note = {https://adaptive.readthedocs.io}, 67 | } 68 | 69 | @Article{Takhtaganov2018, 70 | author = {Takhtaganov, Timur and Müller, Juliane}, 71 | title = {Adaptive Gaussian process surrogates for Bayesian inference}, 72 | journal = {arXiv preprint arXiv:1809.10784}, 73 | year = {2018}, 74 | } 75 | 76 | @Online{Wolfram2011, 77 | author = {Stephen Wolfram}, 78 | title = {Mathematica: Adaptive Plotting}, 79 | year = {2011}, 80 | url = {http://demonstrations.wolfram.com/AdaptivePlotting/}, 81 | urldate = {2019-09-10}, 82 | } 83 | 84 | @Misc{WolframResearch, 85 | author = {Wolfram Research, Inc.}, 86 | title = {Mathematica, Version 12.0}, 87 | note = {Champaign, IL, 2019}, 88 | } 89 | 90 | % Below is from all `yaml` files. 91 | 92 | @article{Alliez2003, 93 | doi = {10.1145/882262.882296}, 94 | year = 2003, 95 | month = {jul}, 96 | publisher = {Association for Computing Machinery ({ACM})}, 97 | volume = {22}, 98 | number = {3}, 99 | pages = {485}, 100 | author = {Pierre Alliez and David Cohen-Steiner and Olivier Devillers and Bruno L{\'{e}}vy and Mathieu Desbrun}, 101 | title = {Anisotropic polygonal remeshing}, 102 | journal = {{ACM} Trans. Graph.} 103 | } 104 | 105 | @article{Berger1984, 106 | doi = {10.1016/0021-9991(84)90073-1}, 107 | year = 1984, 108 | month = {mar}, 109 | publisher = {Elsevier {BV}}, 110 | volume = {53}, 111 | number = {3}, 112 | pages = {484--512}, 113 | author = {Marsha J Berger and Joseph Oliger}, 114 | title = {Adaptive mesh refinement for hyperbolic partial differential equations}, 115 | journal = {J. Comput. Phys.} 116 | } 117 | 118 | @article{Berger1989, 119 | doi = {10.1016/0021-9991(89)90035-1}, 120 | year = 1989, 121 | month = {may}, 122 | publisher = {Elsevier {BV}}, 123 | volume = {82}, 124 | number = {1}, 125 | pages = {64--84}, 126 | author = {M.J. Berger and P. Colella}, 127 | title = {Local adaptive mesh refinement for shock hydrodynamics}, 128 | journal = {J. Comput. Phys.} 129 | } 130 | 131 | @article{Bommer2019, 132 | doi = {10.1103/physrevlett.122.187702}, 133 | pages = {187702}, 134 | year = 2019, 135 | month = {may}, 136 | publisher = {American Physical Society ({APS})}, 137 | volume = {122}, 138 | number = {18}, 139 | author = {Jouri D.{\hspace{0.167em}}S. Bommer and Hao Zhang and \"{O}nder G\"{u}l and Bas Nijholt and Michael Wimmer and Filipp N. Rybakov and Julien Garaud and Donjan Rodic and Egor Babaev and Matthias Troyer and Diana Car and S{\'{e}}bastien R. Plissard and Erik P.{\hspace{0.167em}}A.{\hspace{0.167em}}M. Bakkers and Kenji Watanabe and Takashi Taniguchi and Leo P. Kouwenhoven}, 140 | title = {Spin-Orbit Protection of Induced Superconductivity in {M}ajorana Nanowires}, 141 | journal = {Phys. Rev. Lett.} 142 | } 143 | 144 | @article{Chen2017, 145 | doi = {10.1088/1361-6501/aa7d31}, 146 | year = 2017, 147 | month = {sep}, 148 | publisher = {{IOP} Publishing}, 149 | volume = {28}, 150 | number = {10}, 151 | pages = {105005}, 152 | author = {Yuhang Chen and Chaoyang Peng}, 153 | title = {Intelligent adaptive sampling guided by Gaussian process inference}, 154 | journal = {Meas. Sci. Technol.} 155 | } 156 | 157 | @article{Clenshaw1960, 158 | doi = {10.1007/bf01386223}, 159 | year = 1960, 160 | month = {dec}, 161 | publisher = {Springer Science and Business Media {LLC}}, 162 | volume = {2}, 163 | number = {1}, 164 | pages = {197--205}, 165 | author = {C. W. Clenshaw and A. R. Curtis}, 166 | title = {A method for numerical integration on an automatic computer}, 167 | journal = {Numer. Math.} 168 | } 169 | 170 | @inproceedings{DeRose1998, 171 | doi = {10.1145/280814.280826}, 172 | year = 1998, 173 | publisher = {{ACM} Press}, 174 | author = {Tony DeRose and Michael Kass and Tien Truong}, 175 | title = {Subdivision surfaces in character animation}, 176 | booktitle = {Proceedings of the 25th annual conference on Computer graphics and interactive techniques - {SIGGRAPH} {\textquotesingle}98} 177 | } 178 | 179 | @article{Dyn1990, 180 | doi = {10.1093/imanum/10.1.137}, 181 | year = 1990, 182 | publisher = {Oxford University Press ({OUP})}, 183 | volume = {10}, 184 | number = {1}, 185 | pages = {137--154}, 186 | author = {Nira Dyn and David Levin and Samuel Rippa}, 187 | title = {Data Dependent Triangulations for Piecewise Linear Interpolation}, 188 | journal = {{IMA} J. Appl. Math.} 189 | } 190 | 191 | @article{Emery1998, 192 | doi = {10.1088/0957-0233/9/6/003}, 193 | year = 1998, 194 | month = {jun}, 195 | publisher = {{IOP} Publishing}, 196 | volume = {9}, 197 | number = {6}, 198 | pages = {864--876}, 199 | author = {A F Emery and Aleksey V Nenarokomov}, 200 | title = {Optimal experiment design}, 201 | journal = {Meas. Sci. Technol.} 202 | } 203 | 204 | @incollection{Figueiredo1995, 205 | doi = {10.1016/b978-0-12-543457-7.50032-2}, 206 | year = 1995, 207 | publisher = {Elsevier}, 208 | pages = {173--178}, 209 | author = {Luiz Henrique de Figueiredo}, 210 | title = {Adaptive Sampling of Parametric Curves}, 211 | booktitle = {Graphics Gems V} 212 | } 213 | 214 | @article{Gonnet2010, 215 | doi = {10.1145/1824801.1824804}, 216 | year = 2010, 217 | month = {sep}, 218 | publisher = {Association for Computing Machinery ({ACM})}, 219 | volume = {37}, 220 | number = {3}, 221 | pages = {1--32}, 222 | author = {Pedro Gonnet}, 223 | title = {Increasing the Reliability of Adaptive Quadrature Using Explicit Interpolants}, 224 | journal = {{ACM} Trans. Math. Softw.} 225 | } 226 | 227 | @inproceedings{Gramacy2004, 228 | doi = {10.1145/1015330.1015367}, 229 | year = 2004, 230 | publisher = {{ACM} Press}, 231 | author = {Robert B. Gramacy and Herbert K. H. Lee and William G. Macready}, 232 | title = {Parameter space exploration with Gaussian process trees}, 233 | booktitle = {Twenty-first international conference on Machine learning - {ICML} {\textquotesingle}04} 234 | } 235 | 236 | @book{Hu2006, 237 | doi = {10.1002/047005588x}, 238 | year = 2006, 239 | month = {apr}, 240 | publisher = {John Wiley {\&} Sons, Inc.}, 241 | author = {Feifang Hu and William F. Rosenberger}, 242 | title = {The Theory of Response-Adaptive Randomization in Clinical Trials} 243 | } 244 | 245 | @article{Klein1999, 246 | doi = {10.1016/s0377-0427(99)00156-9}, 247 | year = 1999, 248 | month = {sep}, 249 | publisher = {Elsevier {BV}}, 250 | volume = {109}, 251 | number = {1-2}, 252 | pages = {123--152}, 253 | author = {Richard I. Klein}, 254 | title = {Star formation with 3-D adaptive mesh refinement: the collapse and fragmentation of molecular clouds}, 255 | journal = {J. Comput. Appl. Math} 256 | } 257 | 258 | @article{Melo2019, 259 | doi = {10.21468/scipostphys.7.3.039}, 260 | pages = {039}, 261 | year = 2019, 262 | month = {sep}, 263 | publisher = {Stichting {SciPost}}, 264 | volume = {7}, 265 | number = {3}, 266 | author = {Andr{\'{e}} Melo and Sebastian Rubbert and Anton Akhmerov}, 267 | title = {Supercurrent-induced {M}ajorana bound states in a planar geometry}, 268 | journal = {{SciPost} Phys.} 269 | } 270 | 271 | @article{Nijholt2016, 272 | doi = {10.1103/physrevb.93.235434}, 273 | pages = {235434}, 274 | year = 2016, 275 | month = {jun}, 276 | publisher = {American Physical Society ({APS})}, 277 | volume = {93}, 278 | number = {23}, 279 | author = {Bas Nijholt and Anton R. Akhmerov}, 280 | title = {Orbital effect of magnetic field on the {M}ajorana phase diagram}, 281 | journal = {Phys. Rev. B} 282 | } 283 | 284 | @article{Visvalingam1990, 285 | doi = {10.1111/j.1467-8659.1990.tb00398.x}, 286 | year = 1990, 287 | month = {sep}, 288 | publisher = {Wiley}, 289 | volume = {9}, 290 | number = {3}, 291 | pages = {213--225}, 292 | author = {M. Visvalingam and J. D. Whyatt}, 293 | title = {The Douglas-Peucker Algorithm for Line Simplification: Re-evaluation through Visualization}, 294 | journal = {Comput. Graphics Forum} 295 | } 296 | 297 | @article{Vuik2018, 298 | doi = {10.21468/scipostphys.7.5.061}, 299 | pages = {061}, 300 | year = 2019, 301 | month = {nov}, 302 | publisher = {Stichting {SciPost}}, 303 | volume = {7}, 304 | number = {5}, 305 | author = {Adriaan Vuik and Bas Nijholt and Anton Akhmerov and Michael Wimmer}, 306 | title = {Reproducing topological properties with quasi-{M}ajorana states}, 307 | journal = {{SciPost} Phys.} 308 | } 309 | 310 | -------------------------------------------------------------------------------- /pfaffian.py: -------------------------------------------------------------------------------- 1 | """A package for computing Pfaffians""" 2 | 3 | 4 | import cmath 5 | import math 6 | 7 | import numpy as np 8 | import scipy.linalg as la 9 | import scipy.sparse as sp 10 | 11 | 12 | def householder_real(x): 13 | """(v, tau, alpha) = householder_real(x) 14 | 15 | Compute a Householder transformation such that 16 | (1-tau v v^T) x = alpha e_1 17 | where x and v a real vectors, tau is 0 or 2, and 18 | alpha a real number (e_1 is the first unit vector) 19 | """ 20 | 21 | assert x.shape[0] > 0 22 | 23 | sigma = x[1:] @ x[1:] 24 | 25 | if sigma == 0: 26 | return (np.zeros(x.shape[0]), 0, x[0]) 27 | else: 28 | norm_x = math.sqrt(x[0] ** 2 + sigma) 29 | 30 | v = x.copy() 31 | 32 | # depending on whether x[0] is positive or negatvie 33 | # choose the sign 34 | if x[0] <= 0: 35 | v[0] -= norm_x 36 | alpha = +norm_x 37 | else: 38 | v[0] += norm_x 39 | alpha = -norm_x 40 | 41 | v /= np.linalg.norm(v) 42 | 43 | return (v, 2, alpha) 44 | 45 | 46 | def householder_complex(x): 47 | """(v, tau, alpha) = householder_real(x) 48 | 49 | Compute a Householder transformation such that 50 | (1-tau v v^T) x = alpha e_1 51 | where x and v a complex vectors, tau is 0 or 2, and 52 | alpha a complex number (e_1 is the first unit vector) 53 | """ 54 | assert x.shape[0] > 0 55 | 56 | sigma = np.conj(x[1:]) @ x[1:] 57 | 58 | if sigma == 0: 59 | return (np.zeros(x.shape[0]), 0, x[0]) 60 | else: 61 | norm_x = cmath.sqrt(x[0].conjugate() * x[0] + sigma) 62 | 63 | v = x.copy() 64 | 65 | phase = cmath.exp(1j * math.atan2(x[0].imag, x[0].real)) 66 | 67 | v[0] += phase * norm_x 68 | 69 | v /= np.linalg.norm(v) 70 | 71 | return (v, 2, -phase * norm_x) 72 | 73 | 74 | def skew_tridiagonalize(A, overwrite_a=False, calc_q=True): 75 | """ T, Q = skew_tridiagonalize(A, overwrite_a, calc_q=True) 76 | 77 | or 78 | 79 | T = skew_tridiagonalize(A, overwrite_a, calc_q=False) 80 | 81 | Bring a real or complex skew-symmetric matrix (A=-A^T) into 82 | tridiagonal form T (with zero diagonal) with a orthogonal 83 | (real case) or unitary (complex case) matrix U such that 84 | A = Q T Q^T 85 | (Note that Q^T and *not* Q^dagger also in the complex case) 86 | 87 | A is overwritten if overwrite_a=True (default: False), and 88 | Q only calculated if calc_q=True (default: True) 89 | """ 90 | 91 | # Check if matrix is square 92 | assert A.shape[0] == A.shape[1] > 0 93 | # Check if it's skew-symmetric 94 | assert abs((A + A.T).max()) < 1e-14 95 | 96 | n = A.shape[0] 97 | A = np.asarray(A) # the slice views work only properly for arrays 98 | 99 | # Check if we have a complex data type 100 | if np.issubdtype(A.dtype, np.complexfloating): 101 | householder = householder_complex 102 | elif not np.issubdtype(A.dtype, np.number): 103 | raise TypeError("pfaffian() can only work on numeric input") 104 | else: 105 | householder = householder_real 106 | 107 | if not overwrite_a: 108 | A = A.copy() 109 | 110 | if calc_q: 111 | Q = np.eye(A.shape[0], dtype=A.dtype) 112 | 113 | for i in range(A.shape[0] - 2): 114 | # Find a Householder vector to eliminate the i-th column 115 | v, tau, alpha = householder(A[i + 1 :, i]) 116 | A[i + 1, i] = alpha 117 | A[i, i + 1] = -alpha 118 | A[i + 2 :, i] = 0 119 | A[i, i + 2 :] = 0 120 | 121 | # Update the matrix block A(i+1:N,i+1:N) 122 | w = tau * A[i + 1 :, i + 1 :] @ v.conj() 123 | A[i + 1 :, i + 1 :] += np.outer(v, w) - np.outer(w, v) 124 | 125 | if calc_q: 126 | # Accumulate the individual Householder reflections 127 | # Accumulate them in the form P_1*P_2*..., which is 128 | # (..*P_2*P_1)^dagger 129 | y = tau * Q[:, i + 1 :] @ v 130 | Q[:, i + 1 :] -= np.outer(y, v.conj()) 131 | 132 | if calc_q: 133 | return (np.asmatrix(A), np.asmatrix(Q)) 134 | else: 135 | return np.asmatrix(A) 136 | 137 | 138 | def skew_LTL(A, overwrite_a=False, calc_L=True, calc_P=True): 139 | """ T, L, P = skew_LTL(A, overwrite_a, calc_q=True) 140 | 141 | Bring a real or complex skew-symmetric matrix (A=-A^T) into 142 | tridiagonal form T (with zero diagonal) with a lower unit 143 | triangular matrix L such that 144 | P A P^T= L T L^T 145 | 146 | A is overwritten if overwrite_a=True (default: False), 147 | L and P only calculated if calc_L=True or calc_P=True, 148 | respectively (default: True). 149 | """ 150 | 151 | # Check if matrix is square 152 | assert A.shape[0] == A.shape[1] > 0 153 | # Check if it's skew-symmetric 154 | assert abs((A + A.T).max()) < 1e-14 155 | 156 | n = A.shape[0] 157 | A = np.asarray(A) # the slice views work only properly for arrays 158 | 159 | if not overwrite_a: 160 | A = A.copy() 161 | 162 | if calc_L: 163 | L = np.eye(n, dtype=A.dtype) 164 | 165 | if calc_P: 166 | Pv = np.arange(n) 167 | 168 | for k in range(n - 2): 169 | # First, find the largest entry in A[k+1:,k] and 170 | # permute it to A[k+1,k] 171 | kp = k + 1 + np.abs(A[k + 1 :, k]).argmax() 172 | 173 | # Check if we need to pivot 174 | if kp != k + 1: 175 | # interchange rows k+1 and kp 176 | temp = A[k + 1, k:].copy() 177 | A[k + 1, k:] = A[kp, k:] 178 | A[kp, k:] = temp 179 | 180 | # Then interchange columns k+1 and kp 181 | temp = A[k:, k + 1].copy() 182 | A[k:, k + 1] = A[k:, kp] 183 | A[k:, kp] = temp 184 | 185 | if calc_L: 186 | # permute L accordingly 187 | temp = L[k + 1, 1 : k + 1].copy() 188 | L[k + 1, 1 : k + 1] = L[kp, 1 : k + 1] 189 | L[kp, 1 : k + 1] = temp 190 | 191 | if calc_P: 192 | # accumulate the permutation matrix 193 | temp = Pv[k + 1] 194 | Pv[k + 1] = Pv[kp] 195 | Pv[kp] = temp 196 | 197 | # Now form the Gauss vector 198 | if A[k + 1, k] != 0.0: 199 | tau = A[k + 2 :, k].copy() 200 | tau /= A[k + 1, k] 201 | 202 | # clear eliminated row and column 203 | A[k + 2 :, k] = 0.0 204 | A[k, k + 2 :] = 0.0 205 | 206 | # Update the matrix block A(k+2:,k+2) 207 | A[k + 2 :, k + 2 :] += np.outer(tau, A[k + 2 :, k + 1]) 208 | A[k + 2 :, k + 2 :] -= np.outer(A[k + 2 :, k + 1], tau) 209 | 210 | if calc_L: 211 | L[k + 2 :, k + 1] = tau 212 | 213 | if calc_P: 214 | # form the permutation matrix as a sparse matrix 215 | P = sp.csr_matrix((np.ones(n), (np.arange(n), Pv))) 216 | 217 | if calc_L: 218 | if calc_P: 219 | return (np.asmatrix(A), np.asmatrix(L), P) 220 | else: 221 | return (np.asmatrix(A), np.asmatrix(L)) 222 | else: 223 | if calc_P: 224 | return (np.asmatrix(A), P) 225 | else: 226 | return np.asmatrix(A) 227 | 228 | 229 | def pfaffian(A, overwrite_a=False, method="P", sign_only=False): 230 | """ pfaffian(A, overwrite_a=False, method='P') 231 | 232 | Compute the Pfaffian of a real or complex skew-symmetric 233 | matrix A (A=-A^T). If overwrite_a=True, the matrix A 234 | is overwritten in the process. This function uses 235 | either the Parlett-Reid algorithm (method='P', default), 236 | or the Householder tridiagonalization (method='H') 237 | """ 238 | # Check if matrix is square 239 | assert A.shape[0] == A.shape[1] > 0 240 | # Check if it's skew-symmetric 241 | assert abs((A + A.T).max()) < 1e-14, abs((A + A.T).max()) 242 | # Check that the method variable is appropriately set 243 | assert method == "P" or method == "H" 244 | if method == "H" and sign_only: 245 | raise Exception("Use `method='P'` when using `sign_only=True`") 246 | if method == "P": 247 | return pfaffian_LTL(A, overwrite_a, sign_only) 248 | else: 249 | return pfaffian_householder(A, overwrite_a) 250 | 251 | 252 | def pfaffian_LTL(A, overwrite_a=False, sign_only=False): 253 | """ pfaffian_LTL(A, overwrite_a=False) 254 | 255 | Compute the Pfaffian of a real or complex skew-symmetric 256 | matrix A (A=-A^T). If overwrite_a=True, the matrix A 257 | is overwritten in the process. This function uses 258 | the Parlett-Reid algorithm. 259 | """ 260 | # Check if matrix is square 261 | assert A.shape[0] == A.shape[1] > 0 262 | # Check if it's skew-symmetric 263 | assert abs((A + A.T).max()) < 1e-14 264 | 265 | n = A.shape[0] 266 | A = np.asarray(A) # the slice views work only properly for arrays 267 | 268 | # Quick return if possible 269 | if n % 2 == 1: 270 | return 0 271 | 272 | if not overwrite_a: 273 | A = A.copy() 274 | 275 | pfaffian_val = 1.0 276 | 277 | for k in range(0, n - 1, 2): 278 | # First, find the largest entry in A[k+1:,k] and 279 | # permute it to A[k+1,k] 280 | kp = k + 1 + np.abs(A[k + 1 :, k]).argmax() 281 | 282 | # Check if we need to pivot 283 | if kp != k + 1: 284 | # interchange rows k+1 and kp 285 | temp = A[k + 1, k:].copy() 286 | A[k + 1, k:] = A[kp, k:] 287 | A[kp, k:] = temp 288 | 289 | # Then interchange columns k+1 and kp 290 | temp = A[k:, k + 1].copy() 291 | A[k:, k + 1] = A[k:, kp] 292 | A[k:, kp] = temp 293 | 294 | # every interchange corresponds to a "-" in det(P) 295 | pfaffian_val *= -1 296 | 297 | # Now form the Gauss vector 298 | if A[k + 1, k] != 0.0: 299 | tau = A[k, k + 2 :].copy() 300 | tau /= A[k, k + 1] 301 | 302 | if sign_only: 303 | pfaffian_val *= np.sign(A[k, k + 1]) 304 | else: 305 | pfaffian_val *= A[k, k + 1] 306 | 307 | if k + 2 < n: 308 | # Update the matrix block A(k+2:,k+2) 309 | A[k + 2 :, k + 2 :] += np.outer(tau, A[k + 2 :, k + 1]) 310 | A[k + 2 :, k + 2 :] -= np.outer(A[k + 2 :, k + 1], tau) 311 | else: 312 | # if we encounter a zero on the super/subdiagonal, the 313 | # Pfaffian is 0 314 | return 0.0 315 | 316 | return pfaffian_val 317 | 318 | 319 | def pfaffian_householder(A, overwrite_a=False): 320 | """ pfaffian(A, overwrite_a=False) 321 | 322 | Compute the Pfaffian of a real or complex skew-symmetric 323 | matrix A (A=-A^T). If overwrite_a=True, the matrix A 324 | is overwritten in the process. This function uses the 325 | Householder tridiagonalization. 326 | 327 | Note that the function pfaffian_schur() can also be used in the 328 | real case. That function does not make use of the skew-symmetry 329 | and is only slightly slower than pfaffian_householder(). 330 | """ 331 | 332 | # Check if matrix is square 333 | assert A.shape[0] == A.shape[1] > 0 334 | # Check if it's skew-symmetric 335 | assert abs((A + A.T).max()) < 1e-14 336 | 337 | n = A.shape[0] 338 | 339 | # Quick return if possible 340 | if n % 2 == 1: 341 | return 0 342 | 343 | # Check if we have a complex data type 344 | if np.issubdtype(A.dtype, np.complexfloating): 345 | householder = householder_complex 346 | elif not np.issubdtype(A.dtype, np.number): 347 | raise TypeError("pfaffian() can only work on numeric input") 348 | else: 349 | householder = householder_real 350 | 351 | A = np.asarray(A) # the slice views work only properly for arrays 352 | 353 | if not overwrite_a: 354 | A = A.copy() 355 | 356 | pfaffian_val = 1.0 357 | 358 | for i in range(A.shape[0] - 2): 359 | # Find a Householder vector to eliminate the i-th column 360 | v, tau, alpha = householder(A[i + 1 :, i]) 361 | A[i + 1, i] = alpha 362 | A[i, i + 1] = -alpha 363 | A[i + 2 :, i] = 0 364 | A[i, i + 2 :] = 0 365 | 366 | # Update the matrix block A(i+1:N,i+1:N) 367 | w = tau * A[i + 1 :, i + 1 :] @ v.conj() 368 | A[i + 1 :, i + 1 :] += np.outer(v, w) - np.outer(w, v) 369 | 370 | if tau != 0: 371 | pfaffian_val *= 1 - tau 372 | if i % 2 == 0: 373 | pfaffian_val *= -alpha 374 | 375 | pfaffian_val *= A[n - 2, n - 1] 376 | 377 | return pfaffian_val 378 | 379 | 380 | def pfaffian_schur(A, overwrite_a=False): 381 | """Calculate Pfaffian of a real antisymmetric matrix using 382 | the Schur decomposition. (Hessenberg would in principle be faster, 383 | but scipy-0.8 messed up the performance for scipy.linalg.hessenberg()). 384 | 385 | This function does not make use of the skew-symmetry of the matrix A, 386 | but uses a LAPACK routine that is coded in FORTRAN and hence faster 387 | than python. As a consequence, pfaffian_schur is only slightly slower 388 | than pfaffian(). 389 | """ 390 | 391 | assert np.issubdtype(A.dtype, np.number) and not np.issubdtype( 392 | A.dtype, np.complexfloating 393 | ) 394 | 395 | assert A.shape[0] == A.shape[1] > 0 396 | 397 | assert abs(A + A.T).max() < 1e-14 398 | 399 | # Quick return if possible 400 | if A.shape[0] % 2 == 1: 401 | return 0 402 | 403 | (t, z) = la.schur(A, output="real", overwrite_a=overwrite_a) 404 | l = np.diag(t, 1) 405 | return np.prod(l[::2]) * la.det(z) 406 | 407 | 408 | def pfaffian_sign(A, overwrite_a=False): 409 | """ pfaffian(A, overwrite_a=False, method='P') 410 | 411 | Compute the Pfaffian of a real or complex skew-symmetric 412 | matrix A (A=-A^T). If overwrite_a=True, the matrix A 413 | is overwritten in the process. This function uses 414 | either the Parlett-Reid algorithm (method='P', default), 415 | or the Householder tridiagonalization (method='H') 416 | """ 417 | # Check if matrix is square 418 | assert A.shape[0] == A.shape[1] > 0 419 | # Check if it's skew-symmetric 420 | assert abs((A + A.T).max()) < 1e-14, abs((A + A.T).max()) 421 | 422 | return pfaffian_LTL_sign(A, overwrite_a) 423 | 424 | 425 | def pfaffian_LTL_sign(A, overwrite_a=False): 426 | """MODIFIED FROM pfaffian_LTL(A, overwrite_a=False) 427 | 428 | Compute the Pfaffian of a real or complex skew-symmetric 429 | matrix A (A=-A^T). If overwrite_a=True, the matrix A 430 | is overwritten in the process. This function uses 431 | the Parlett-Reid algorithm. 432 | """ 433 | # Check if matrix is square 434 | assert A.shape[0] == A.shape[1] > 0 435 | # Check if it's skew-symmetric 436 | assert abs((A + A.T).max()) < 1e-14 437 | 438 | n = A.shape[0] 439 | A = np.asarray(A) # the slice views work only properly for arrays 440 | 441 | # Quick return if possible 442 | if n % 2 == 1: 443 | return 0 444 | 445 | if not overwrite_a: 446 | A = A.copy() 447 | 448 | pfaffian_val = 1.0 449 | 450 | for k in range(0, n - 1, 2): 451 | # First, find the largest entry in A[k+1:,k] and 452 | # permute it to A[k+1,k] 453 | kp = k + 1 + np.abs(A[k + 1 :, k]).argmax() 454 | 455 | # Check if we need to pivot 456 | if kp != k + 1: 457 | # interchange rows k+1 and kp 458 | temp = A[k + 1, k:].copy() 459 | A[k + 1, k:] = A[kp, k:] 460 | A[kp, k:] = temp 461 | 462 | # Then interchange columns k+1 and kp 463 | temp = A[k:, k + 1].copy() 464 | A[k:, k + 1] = A[k:, kp] 465 | A[k:, kp] = temp 466 | 467 | # every interchange corresponds to a "-" in det(P) 468 | pfaffian_val *= -1 469 | 470 | # Now form the Gauss vector 471 | if A[k + 1, k] != 0.0: 472 | tau = A[k, k + 2 :].copy() 473 | tau /= A[k, k + 1] 474 | 475 | pfaffian_val *= A[k, k + 1] 476 | 477 | if k + 2 < n: 478 | # Update the matrix block A(k+2:,k+2) 479 | A[k + 2 :, k + 2 :] += np.outer(tau, A[k + 2 :, k + 1]) 480 | A[k + 2 :, k + 2 :] -= np.outer(A[k + 2 :, k + 1], tau) 481 | else: 482 | # if we encounter a zero on the super/subdiagonal, the 483 | # Pfaffian is 0 484 | return 0.0 485 | 486 | return pfaffian_val 487 | -------------------------------------------------------------------------------- /phase_diagram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | import operator 4 | import sys 5 | from collections import OrderedDict 6 | from copy import deepcopy 7 | from functools import wraps 8 | from types import SimpleNamespace 9 | 10 | import kwant 11 | import numpy as np 12 | import scipy.constants 13 | import scipy.sparse 14 | import scipy.sparse.linalg as sla 15 | from kwant.continuum.discretizer import discretize 16 | 17 | import pfaffian as pf 18 | 19 | assert sys.version_info >= (3, 6), "Use Python ≥3.6" 20 | 21 | # Parameters taken from arXiv:1204.2792 22 | # All constant parameters, mostly fundamental constants, in a SimpleNamespace. 23 | constants = SimpleNamespace( 24 | m_eff=0.015 * scipy.constants.m_e, # effective mass in kg 25 | hbar=scipy.constants.hbar, 26 | m_e=scipy.constants.m_e, 27 | eV=scipy.constants.eV, 28 | e=scipy.constants.e, 29 | c=1e18 / (scipy.constants.eV * 1e-3), # to get to meV * nm^2 30 | mu_B=scipy.constants.physical_constants["Bohr magneton in eV/T"][0] * 1e3, 31 | ) 32 | 33 | constants.t = (constants.hbar ** 2 / (2 * constants.m_eff)) * constants.c 34 | 35 | 36 | def get_names(sig): 37 | names = [ 38 | (name, value) 39 | for name, value in sig.parameters.items() 40 | if value.kind 41 | in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) 42 | ] 43 | return OrderedDict(names) 44 | 45 | 46 | def filter_kwargs(sig, names, kwargs): 47 | names_in_kwargs = [(name, value) for name, value in kwargs.items() if name in names] 48 | return OrderedDict(names_in_kwargs) 49 | 50 | 51 | def skip_pars(names1, names2, num_skipped): 52 | skipped_pars1 = list(names1.keys())[:num_skipped] 53 | skipped_pars2 = list(names2.keys())[:num_skipped] 54 | if skipped_pars1 == skipped_pars2: 55 | pars1 = list(names1.values())[num_skipped:] 56 | pars2 = list(names2.values())[num_skipped:] 57 | else: 58 | raise Exception("First {} arguments " "have to be the same".format(num_skipped)) 59 | return pars1, pars2 60 | 61 | 62 | def combine(f, g, operator, num_skipped=0): 63 | if not callable(f) or not callable(g): 64 | raise Exception("One of the functions is not a function") 65 | 66 | sig1 = inspect.signature(f) 67 | sig2 = inspect.signature(g) 68 | 69 | names1 = get_names(sig1) 70 | names2 = get_names(sig2) 71 | 72 | pars1, pars2 = skip_pars(names1, names2, num_skipped) 73 | skipped_pars = list(names1.values())[:num_skipped] 74 | 75 | pars1_names = {p.name for p in pars1} 76 | pars2 = [p for p in pars2 if p.name not in pars1_names] 77 | 78 | parameters = pars1 + pars2 79 | kind = inspect.Parameter.POSITIONAL_OR_KEYWORD 80 | parameters = [p.replace(kind=kind) for p in parameters] 81 | parameters = skipped_pars + parameters 82 | 83 | def wrapped(*args): 84 | d = {p.name: arg for arg, p in zip(args, parameters)} 85 | fval = f(*[d[name] for name in names1.keys()]) 86 | gval = g(*[d[name] for name in names2.keys()]) 87 | return operator(fval, gval) 88 | 89 | wrapped.__signature__ = inspect.Signature(parameters=parameters) 90 | return wrapped 91 | 92 | 93 | def memoize(obj): 94 | cache = obj.cache = {} 95 | 96 | @wraps(obj) 97 | def memoizer(*args, **kwargs): 98 | key = str(args) + str(kwargs) 99 | if key not in cache: 100 | cache[key] = obj(*args, **kwargs) 101 | return cache[key] 102 | 103 | return memoizer 104 | 105 | 106 | def parse_params(params): 107 | for k, v in params.items(): 108 | if isinstance(v, str): 109 | try: 110 | params[k] = eval(v) 111 | except NameError: 112 | pass 113 | return params 114 | 115 | 116 | @memoize 117 | def discretized_hamiltonian(a, which_lead=None, subst_sm=None): 118 | ham = ( 119 | "(0.5 * hbar**2 * (k_x**2 + k_y**2 + k_z**2) / m_eff * c - mu + V) * kron(sigma_0, sigma_z) + " 120 | "alpha * (k_y * kron(sigma_x, sigma_z) - k_x * kron(sigma_y, sigma_z)) + " 121 | "0.5 * g * mu_B * (B_x * kron(sigma_x, sigma_0) + B_y * kron(sigma_y, sigma_0) + B_z * kron(sigma_z, sigma_0)) + " 122 | "Delta * kron(sigma_0, sigma_x)" 123 | ) 124 | if subst_sm is None: 125 | subst_sm = {"Delta": 0} 126 | 127 | if which_lead is not None: 128 | subst_sm["V"] = f"V_{which_lead}(z, V_0, V_r, V_l, x0, sigma, r1)" 129 | subst_sm["mu"] = f"mu_{which_lead}(x0, sigma, mu_lead, mu_wire)" 130 | else: 131 | subst_sm["V"] = "V(x, z, V_0, V_r, V_l, x0, sigma, r1)" 132 | subst_sm["mu"] = "mu(x, x0, sigma, mu_lead, mu_wire)" 133 | 134 | subst_sc = {"g": 0, "alpha": 0, "mu": "mu_sc", "V": 0} 135 | subst_interface = {"c": "c * c_tunnel", "alpha": 0, "V": 0} 136 | 137 | templ_sm = discretize(ham, locals=subst_sm, grid_spacing=a) 138 | templ_sc = discretize(ham, locals=subst_sc, grid_spacing=a) 139 | templ_interface = discretize(ham, locals=subst_interface, grid_spacing=a) 140 | 141 | return templ_sm, templ_sc, templ_interface 142 | 143 | 144 | def cylinder_sector(r_out, r_in=0, L=1, L0=0, coverage_angle=360, angle=0, a=10): 145 | """Returns the shape function and start coords for a wire with 146 | as cylindrical cross section. 147 | 148 | Parameters 149 | ---------- 150 | r_out : int 151 | Outer radius in nm. 152 | r_in : int, optional 153 | Inner radius in nm. 154 | L : int, optional 155 | Length of wire from L0 in nm, -1 if infinite in x-direction. 156 | L0 : int, optional 157 | Start position in x. 158 | coverage_angle : int, optional 159 | Coverage angle in degrees. 160 | angle : int, optional 161 | Angle of tilting from top in degrees. 162 | a : int, optional 163 | Discretization constant in nm. 164 | 165 | Returns 166 | ------- 167 | (shape_func, *(start_coords)) 168 | """ 169 | coverage_angle *= np.pi / 360 170 | angle *= np.pi / 180 171 | r_out_sq, r_in_sq = r_out ** 2, r_in ** 2 172 | 173 | def shape(site): 174 | try: 175 | x, y, z = site.pos 176 | except AttributeError: 177 | x, y, z = site 178 | n = (y + 1j * z) * np.exp(1j * angle) 179 | y, z = n.real, n.imag 180 | rsq = y ** 2 + z ** 2 181 | shape_yz = r_in_sq <= rsq < r_out_sq and z >= np.cos(coverage_angle) * np.sqrt( 182 | rsq 183 | ) 184 | return (shape_yz and L0 <= x < L) if L > 0 else shape_yz 185 | 186 | r_mid = (r_out + r_in) / 2 187 | start_coords = np.array([L - a, r_mid * np.sin(angle), r_mid * np.cos(angle)]) 188 | 189 | return shape, start_coords 190 | 191 | 192 | def is_antisymmetric(H): 193 | return np.allclose(-H, H.T) 194 | 195 | 196 | def cell_mats(lead, params, bias=0): 197 | h = lead.cell_hamiltonian(params=params) 198 | h -= bias * np.identity(len(h)) 199 | t = lead.inter_cell_hopping(params=params) 200 | return h, t 201 | 202 | 203 | def get_h_k(lead, params): 204 | h, t = cell_mats(lead, params) 205 | 206 | def h_k(k): 207 | return h + t * np.exp(1j * k) + t.T.conj() * np.exp(-1j * k) 208 | 209 | return h_k 210 | 211 | 212 | def make_skew_symmetric(ham): 213 | """ 214 | Makes a skew symmetric matrix by a matrix multiplication of a unitary 215 | matrix U. This unitary matrix is taken from the Topology MOOC 0D, but 216 | that is in a different basis. To get to the right basis one multiplies 217 | by [[np.eye(2), 0], [0, sigma_y]]. 218 | 219 | Parameters: 220 | ----------- 221 | ham : numpy.ndarray 222 | Hamiltonian matrix gotten from sys.cell_hamiltonian() 223 | 224 | Returns: 225 | -------- 226 | skew_ham : numpy.ndarray 227 | Skew symmetrized Hamiltonian 228 | """ 229 | W = ham.shape[0] // 4 230 | I = np.eye(2, dtype=complex) 231 | sigma_y = np.array([[0, 1j], [-1j, 0]], dtype=complex) 232 | U_1 = np.bmat([[I, I], [1j * I, -1j * I]]) 233 | U_2 = np.bmat([[I, 0 * I], [0 * I, sigma_y]]) 234 | U = U_1 @ U_2 235 | U = np.kron(np.eye(W, dtype=complex), U) 236 | skew_ham = U @ ham @ U.H 237 | 238 | assert is_antisymmetric(skew_ham) 239 | 240 | return skew_ham 241 | 242 | 243 | def calculate_pfaffian(lead, params): 244 | """ 245 | Calculates the Pfaffian for the infinite system by computing it at k = 0 246 | and k = pi. 247 | 248 | Parameters: 249 | ----------- 250 | lead : kwant.builder.InfiniteSystem object 251 | The finalized system. 252 | 253 | """ 254 | h_k = get_h_k(lead, params) 255 | 256 | skew_h0 = make_skew_symmetric(h_k(0)) 257 | skew_h_pi = make_skew_symmetric(h_k(np.pi)) 258 | 259 | pf_0 = np.sign(pf.pfaffian(1j * skew_h0, sign_only=True).real) 260 | pf_pi = np.sign(pf.pfaffian(1j * skew_h_pi, sign_only=True).real) 261 | pfaf = pf_0 * pf_pi 262 | 263 | return pfaf 264 | 265 | 266 | def at_interface(site1, site2, shape1, shape2): 267 | return (shape1[0](site1) and shape2[0](site2)) or ( 268 | shape2[0](site1) and shape1[0](site2) 269 | ) 270 | 271 | 272 | def change_hopping_at_interface(syst, template, shape1, shape2): 273 | for (site1, site2), hop in syst.hopping_value_pairs(): 274 | if at_interface(site1, site2, shape1, shape2): 275 | syst[site1, site2] = template[site1, site2] 276 | return syst 277 | 278 | 279 | @memoize 280 | def make_lead(a, r1, r2, coverage_angle, angle, with_shell, which_lead, sc_inside_wire=False, wraparound=False): 281 | """Create an infinite cylindrical 3D wire partially covered with a 282 | superconducting (SC) shell. 283 | 284 | Parameters 285 | ---------- 286 | a : int 287 | Discretization constant in nm. 288 | r1 : int 289 | Radius of normal part of wire in nm. 290 | r2 : int 291 | Radius of superconductor in nm. 292 | coverage_angle : int 293 | Coverage angle of superconductor in degrees. 294 | angle : int 295 | Angle of tilting of superconductor from top in degrees. 296 | with_shell : bool 297 | Adds shell to the scattering area. If False no SC shell is added and 298 | only a cylindrical wire will be created. 299 | which_lead : str 300 | Name of the potential function of the lead, e.g. `which_lead = 'left'` will 301 | require a function `V_left(z, V_0)` and 302 | `mu_left(mu_func(x, x0, sigma, mu_lead, mu_wire)`. 303 | sc_inside_wire : bool 304 | Put superconductivity inside the wire. 305 | wraparound : bool 306 | Apply wraparound to the lead. 307 | 308 | Returns 309 | ------- 310 | syst : kwant.builder.InfiniteSystem 311 | The finilized kwant system. 312 | 313 | Examples 314 | -------- 315 | This doesn't use default parameters because the variables need to be saved, 316 | to a file. So I create a dictionary that is passed to the function. 317 | 318 | >>> syst_params = dict(a=10, angle=0, coverage_angle=185, r1=50, 319 | ... r2=70, with_shell=True) 320 | >>> syst, hopping = make_lead(**syst_params) 321 | """ 322 | 323 | shape_normal_lead = cylinder_sector(r_out=r1, angle=angle, L=-1, a=a) 324 | shape_sc_lead = cylinder_sector( 325 | r_out=r2, r_in=r1, coverage_angle=coverage_angle, angle=angle, L=-1, a=a 326 | ) 327 | 328 | sz = np.array([[1, 0], [0, -1]]) 329 | cons_law = np.kron(np.eye(2), -sz) 330 | symmetry = kwant.TranslationalSymmetry((a, 0, 0)) 331 | lead = kwant.Builder( 332 | symmetry, conservation_law=cons_law if not with_shell else None 333 | ) 334 | 335 | templ_sm, templ_sc, templ_interface = discretized_hamiltonian( 336 | a, which_lead=which_lead, subst_sm={} if sc_inside_wire else None 337 | ) 338 | templ_sm = apply_peierls_to_template(templ_sm) 339 | lead.fill(templ_sm, *shape_normal_lead) 340 | 341 | if with_shell: 342 | lat = templ_sc.lattice 343 | shape_sc = cylinder_sector( 344 | r_out=r2, r_in=r1, coverage_angle=coverage_angle, angle=angle, L=a, a=a 345 | ) 346 | 347 | xyz_offset = get_offset(*shape_sc, lat) 348 | 349 | templ_interface = apply_peierls_to_template(templ_interface) 350 | lead.fill(templ_sc, *shape_sc_lead) 351 | 352 | # Adding a tunnel barrier between SM and SC 353 | lead = change_hopping_at_interface( 354 | lead, templ_interface, shape_normal_lead, shape_sc_lead 355 | ) 356 | 357 | if wraparound: 358 | lead = kwant.wraparound.wraparound(lead) 359 | return lead 360 | 361 | 362 | def apply_peierls_to_template(template, xyz_offset=(0, 0, 0)): 363 | """Adds p.orbital argument to the hopping functions.""" 364 | template = deepcopy(template) # Needed because kwant.Builder is mutable 365 | x0, y0, z0 = xyz_offset 366 | lat = template.lattice 367 | a = np.max(lat.prim_vecs) # lattice contant 368 | 369 | def phase(site1, site2, B_x, B_y, B_z, orbital, e, hbar): 370 | if orbital: 371 | x, y, z = site1.tag 372 | direction = site1.tag - site2.tag 373 | A = [B_y * (z - z0) - B_z * (y - y0), 0, B_x * (y - y0)] 374 | A = np.dot(A, direction) * a ** 2 * 1e-18 * e / hbar 375 | phase = np.exp(-1j * A) 376 | if lat.norbs == 2: # No PH degrees of freedom 377 | return phase 378 | elif lat.norbs == 4: 379 | return np.array( 380 | [phase, phase.conj(), phase, phase.conj()], dtype="complex128" 381 | ) 382 | else: # No orbital phase 383 | return 1 384 | 385 | for (site1, site2), hop in template.hopping_value_pairs(): 386 | template[site1, site2] = combine(hop, phase, operator.mul, 2) 387 | return template 388 | 389 | 390 | def get_offset(shape, start, lat): 391 | coords = [site.pos for site in lat.shape(shape, start)()] 392 | xyz_offset = np.mean(coords, axis=0) 393 | return xyz_offset 394 | 395 | 396 | def translation_ev(h, t, tol=1e6): 397 | """Compute the eigenvalues of the translation operator of a lead. 398 | 399 | Adapted from kwant.physics.leads.modes. 400 | 401 | Parameters 402 | ---------- 403 | h : numpy array, real or complex, shape (N, N) The unit cell 404 | Hamiltonian of the lead unit cell. 405 | t : numpy array, real or complex, shape (N, M) 406 | The hopping matrix from a lead cell to the one on which self-energy 407 | has to be calculated (and any other hopping in the same direction). 408 | tol : float 409 | Numbers and differences are considered zero when they are smaller 410 | than `tol` times the machine precision. 411 | 412 | Returns 413 | ------- 414 | ev : numpy array 415 | Eigenvalues of the translation operator in the form lambda=r*exp(i*k), 416 | for |r|=1 they are propagating modes. 417 | """ 418 | a, b = kwant.physics.leads.setup_linsys(h, t, tol, None).eigenproblem 419 | ev = kwant.physics.leads.unified_eigenproblem(a, b, tol=tol)[0] 420 | return ev 421 | 422 | 423 | def gap_minimizer(lead, params, energy): 424 | """Function that minimizes a function to find the band gap. 425 | This objective function checks if there are progagating modes at a 426 | certain energy. Returns zero if there is a propagating mode. 427 | 428 | Parameters 429 | ---------- 430 | lead : kwant.builder.InfiniteSystem object 431 | The finalized infinite system. 432 | params : dict 433 | A dict that is used to store Hamiltonian parameters. 434 | energy : float 435 | Energy at which this function checks for propagating modes. 436 | 437 | Returns 438 | ------- 439 | minimized_scalar : float 440 | Value that is zero when there is a propagating mode. 441 | """ 442 | h, t = cell_mats(lead, params, bias=energy) 443 | ev = translation_ev(h, t) 444 | norm = (ev * ev.conj()).real 445 | return np.min(np.abs(norm - 1)) 446 | 447 | 448 | def gap_from_modes(lead, params, tol=1e-6): 449 | """Finds the gapsize by peforming a binary search of the modes with a 450 | tolarance of tol. 451 | 452 | Parameters 453 | ---------- 454 | lead : kwant.builder.InfiniteSystem object 455 | The finalized infinite system. 456 | params : dict 457 | A dict that is used to store Hamiltonian parameters. 458 | tol : float 459 | The precision of the binary search. 460 | 461 | Returns 462 | ------- 463 | gap : float 464 | Size of the gap. 465 | 466 | Notes 467 | ----- 468 | For use with `lead = funcs.make_lead()`. 469 | """ 470 | Es = kwant.physics.Bands(lead, params=params)(k=0) 471 | lim = [0, np.abs(Es).min()] 472 | if gap_minimizer(lead, params, energy=0) < 1e-15: 473 | # No band gap 474 | gap = 0 475 | else: 476 | while lim[1] - lim[0] > tol: 477 | energy = sum(lim) / 2 478 | par = gap_minimizer(lead, params, energy) 479 | if par < 1e-10: 480 | lim[1] = energy 481 | else: 482 | lim[0] = energy 483 | gap = sum(lim) / 2 484 | return gap 485 | 486 | 487 | def phase_bounds_operator(lead, params, k_x=0, mu_param='mu'): 488 | params = dict(params, k_x=k_x) 489 | params[mu_param] = 0 490 | h_k = lead.hamiltonian_submatrix(params=params, sparse=True) 491 | sigma_z = scipy.sparse.csc_matrix(np.array([[1, 0], [0, -1]])) 492 | _operator = scipy.sparse.kron(scipy.sparse.eye(h_k.shape[0] // 2), sigma_z) @ h_k 493 | return _operator 494 | 495 | 496 | def find_phase_bounds(lead, params, k_x=0, num_bands=20, sigma=0, mu_param='mu'): 497 | """Find the phase boundaries. 498 | Solve an eigenproblem that finds values of chemical potential at which the 499 | gap closes at momentum k=0. We are looking for all real solutions of the 500 | form H*psi=0 so we solve sigma_0 * tau_z H * psi = mu * psi. 501 | 502 | Parameters 503 | ----------- 504 | lead : kwant.builder.InfiniteSystem object 505 | The finalized infinite system. 506 | params : dict 507 | A dictionary that is used to store Hamiltonian parameters. 508 | k_x : float 509 | Momentum value, by default set to 0. 510 | 511 | Returns 512 | -------- 513 | chemical_potential : numpy array 514 | Twenty values of chemical potential at which a bandgap closes at k=0. 515 | """ 516 | chemical_potentials = phase_bounds_operator(lead, params, k_x, mu_param) 517 | 518 | if num_bands is None: 519 | mus = np.linalg.eigvals(chemical_potentials.todense()) 520 | else: 521 | mus = sla.eigs(chemical_potentials, k=num_bands, sigma=sigma, which="LM")[0] 522 | 523 | real_solutions = abs(np.angle(mus)) < 1e-10 524 | 525 | mus[~real_solutions] = np.nan # To ensure it returns the same shape vector 526 | return np.sort(mus.real) 527 | -------------------------------------------------------------------------------- /figures.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Adaptive paper figures" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## imports and function definitions" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import functools\n", 24 | "import itertools\n", 25 | "import os\n", 26 | "import pickle\n", 27 | "\n", 28 | "import holoviews.plotting.mpl\n", 29 | "import matplotlib\n", 30 | "\n", 31 | "matplotlib.use(\"agg\")\n", 32 | "import matplotlib.pyplot as plt\n", 33 | "import matplotlib.tri as mtri\n", 34 | "import numpy as np\n", 35 | "from scipy import interpolate\n", 36 | "\n", 37 | "import adaptive\n", 38 | "\n", 39 | "\n", 40 | "%matplotlib inline\n", 41 | "%config InlineBackend.figure_format = 'svg'\n", 42 | "\n", 43 | "golden_mean = (np.sqrt(5) - 1) / 2 # Aesthetic ratio\n", 44 | "fig_width_pt = 246.0 # Columnwidth\n", 45 | "inches_per_pt = 1 / 72.27 # Convert pt to inches\n", 46 | "fig_width = fig_width_pt * inches_per_pt\n", 47 | "fig_height = fig_width * golden_mean # height in inches\n", 48 | "fig_size = [fig_width, fig_height]\n", 49 | "\n", 50 | "params = {\n", 51 | " \"backend\": \"ps\",\n", 52 | " \"axes.labelsize\": 13,\n", 53 | " \"font.size\": 13,\n", 54 | " \"legend.fontsize\": 10,\n", 55 | " \"xtick.labelsize\": 10,\n", 56 | " \"ytick.labelsize\": 10,\n", 57 | " \"text.usetex\": True,\n", 58 | " \"figure.figsize\": fig_size,\n", 59 | " \"font.family\": \"serif\",\n", 60 | " \"font.serif\": \"Computer Modern Roman\",\n", 61 | " \"legend.frameon\": True,\n", 62 | " \"savefig.dpi\": 300,\n", 63 | "}\n", 64 | "\n", 65 | "plt.rcParams.update(params)\n", 66 | "plt.rc(\"text.latex\", preamble=[r\"\\usepackage{xfrac}\", r\"\\usepackage{siunitx}\"])\n", 67 | "\n", 68 | "\n", 69 | "class HistogramNormalize(matplotlib.colors.Normalize):\n", 70 | " def __init__(self, data, vmin=None, vmax=None, mixing_degree=1):\n", 71 | " self.mixing_degree = mixing_degree\n", 72 | " if vmin is not None:\n", 73 | " data = data[data >= vmin]\n", 74 | " if vmax is not None:\n", 75 | " data = data[data <= vmax]\n", 76 | "\n", 77 | " self.sorted_data = np.sort(data.flatten())\n", 78 | " matplotlib.colors.Normalize.__init__(self, vmin, vmax)\n", 79 | "\n", 80 | " def __call__(self, value, clip=None):\n", 81 | " hist_norm = np.ma.masked_array(\n", 82 | " np.searchsorted(self.sorted_data, value) / len(self.sorted_data)\n", 83 | " )\n", 84 | " linear_norm = super().__call__(value, clip)\n", 85 | " return self.mixing_degree * hist_norm + (1 - self.mixing_degree) * linear_norm" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# 1D funcs\n", 95 | "def f(x, offset=0.123):\n", 96 | " a = 0.02\n", 97 | " return x + a ** 2 / (a ** 2 + (x - offset) ** 2)\n", 98 | "\n", 99 | "\n", 100 | "def g(x):\n", 101 | " return np.tanh(x * 40)\n", 102 | "\n", 103 | "\n", 104 | "def h(x):\n", 105 | " return np.sin(100 * x) * np.exp(-x ** 2 / 0.1 ** 2)\n", 106 | "\n", 107 | "\n", 108 | "funcs_1D = [\n", 109 | " dict(function=f, bounds=(-1, 1), title=\"peak\"),\n", 110 | " dict(function=g, bounds=(-1, 1), title=\"tanh\"),\n", 111 | " dict(function=h, bounds=(-0.3, 0.3), title=\"wave packet\"),\n", 112 | "]\n", 113 | "\n", 114 | "# 2D funcs\n", 115 | "def ring(xy, offset=0.123):\n", 116 | " a = 0.2\n", 117 | " x, y = xy\n", 118 | " return x + np.exp(-(x ** 2 + y ** 2 - 0.75 ** 2) ** 2 / a ** 4)\n", 119 | "\n", 120 | "\n", 121 | "@functools.lru_cache()\n", 122 | "def phase_diagram_setup(fname):\n", 123 | " data = adaptive.utils.load(fname)\n", 124 | " points = np.array(list(data.keys()))\n", 125 | " values = np.array(list(data.values()), dtype=float)\n", 126 | " bounds = [\n", 127 | " (points[:, 0].min(), points[:, 0].max()),\n", 128 | " (points[:, 1].min(), points[:, 1].max()),\n", 129 | " ]\n", 130 | " ll, ur = np.reshape(bounds, (2, 2)).T\n", 131 | " inds = np.all(np.logical_and(ll <= points, points <= ur), axis=1)\n", 132 | " points, values = points[inds], values[inds].reshape(-1, 1)\n", 133 | " return interpolate.LinearNDInterpolator(points, values), bounds\n", 134 | "\n", 135 | "\n", 136 | "def phase_diagram(xy, fname):\n", 137 | " ip, _ = phase_diagram_setup(fname)\n", 138 | " zs = ip(xy)\n", 139 | " return np.round(zs)\n", 140 | "\n", 141 | "\n", 142 | "def density(x, eps=0):\n", 143 | " e = [0.8, 0.2]\n", 144 | " delta = [0.5, 0.5, 0.5]\n", 145 | " c = 3\n", 146 | " omega = [0.02, 0.05]\n", 147 | "\n", 148 | " H = np.array(\n", 149 | " [\n", 150 | " [e[0] + 1j * omega[0], delta[0], delta[1]],\n", 151 | " [delta[0], e[1] + c * x + 1j * omega[1], delta[1]],\n", 152 | " [delta[1], delta[2], e[1] - c * x + 1j * omega[1]],\n", 153 | " ]\n", 154 | " )\n", 155 | " H += np.eye(3) * eps\n", 156 | " return np.trace(np.linalg.inv(H)).imag\n", 157 | "\n", 158 | "\n", 159 | "def level_crossing(xy):\n", 160 | " x, y = xy\n", 161 | " return density(x, y) + y\n", 162 | "\n", 163 | "\n", 164 | "funcs_2D = [\n", 165 | " dict(function=ring, bounds=[(-1, 1), (-1, 1)], npoints=33, title=\"ring\"),\n", 166 | " dict(\n", 167 | " function=functools.partial(phase_diagram, fname=\"phase_diagram.pickle\"),\n", 168 | " bounds=phase_diagram_setup(\"phase_diagram.pickle\")[1],\n", 169 | " npoints=100,\n", 170 | " fname=\"phase_diagram.pickle\",\n", 171 | " title=\"phase diagram\",\n", 172 | " ),\n", 173 | " dict(function=level_crossing, bounds=[(-1, 1), (-3, 3)], npoints=50, title=\"level crossing\"),\n", 174 | "]" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "## Interval and loss" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "np.random.seed(1)\n", 191 | "xs = np.array([0.1, 0.3, 0.35, 0.45])\n", 192 | "f = lambda x: x ** 3\n", 193 | "ys = f(xs)\n", 194 | "means = lambda x: np.convolve(x, np.ones(2) / 2, mode=\"valid\")\n", 195 | "xs_means = means(xs)\n", 196 | "ys_means = means(ys)\n", 197 | "\n", 198 | "fig, ax = plt.subplots(figsize=fig_size)\n", 199 | "ax.scatter(xs, ys, c=\"k\")\n", 200 | "ax.plot(xs, ys, c=\"k\")\n", 201 | "\n", 202 | "ax.annotate(\n", 203 | " s=r\"$L_{1,2} = \\sqrt{\\Delta x^2 + \\Delta y^2}$\",\n", 204 | " xy=(np.mean([xs[0], xs[1]]), np.mean([ys[0], ys[1]])),\n", 205 | " xytext=(xs[0] + 0.05, ys[0] - 0.05),\n", 206 | " arrowprops=dict(arrowstyle=\"->\"),\n", 207 | " ha=\"center\",\n", 208 | " zorder=10,\n", 209 | ")\n", 210 | "\n", 211 | "for i, (x, y) in enumerate(zip(xs, ys)):\n", 212 | " sign = [1, -1][i % 2]\n", 213 | " ax.annotate(\n", 214 | " s=fr\"$x_{i+1}, y_{i+1}$\",\n", 215 | " xy=(x, y),\n", 216 | " xytext=(x + 0.01, y + sign * 0.04),\n", 217 | " arrowprops=dict(arrowstyle=\"->\"),\n", 218 | " ha=\"center\",\n", 219 | " )\n", 220 | "\n", 221 | "ax.scatter(xs, ys, c=\"green\", s=5, zorder=5, label=\"existing data\")\n", 222 | "losses = np.hypot(xs[1:] - xs[:-1], ys[1:] - ys[:-1])\n", 223 | "ax.scatter(\n", 224 | " xs_means, ys_means, c=\"red\", s=300 * losses, zorder=8, label=\"candidate points\"\n", 225 | ")\n", 226 | "xs_dense = np.linspace(xs[0], xs[-1], 400)\n", 227 | "ax.plot(xs_dense, f(xs_dense), alpha=0.3, zorder=7, label=\"function\")\n", 228 | "\n", 229 | "ax.legend()\n", 230 | "ax.axis(\"off\")\n", 231 | "plt.savefig(\"figures/loss_1D.pdf\", bbox_inches=\"tight\", transparent=True)\n", 232 | "plt.show()" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "## Learner1D vs grid" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "fig, axs = plt.subplots(2, len(funcs_1D), figsize=(fig_width, 1.5 * fig_height))\n", 249 | "n_points = 50\n", 250 | "for i, ax in enumerate(axs.T.flatten()):\n", 251 | " ax.xaxis.set_ticks([])\n", 252 | " ax.yaxis.set_ticks([])\n", 253 | " kind = \"homogeneous\" if i % 2 == 0 else \"adaptive\"\n", 254 | " index = i // 2 if kind == \"homogeneous\" else (i - 1) // 2\n", 255 | " d = funcs_1D[index]\n", 256 | " bounds = d[\"bounds\"]\n", 257 | " f = d[\"function\"]\n", 258 | "\n", 259 | " if kind == \"homogeneous\":\n", 260 | " xs = np.linspace(*bounds, n_points)\n", 261 | " ys = f(xs)\n", 262 | " ax.set_title(rf\"\\textrm{{{d['title']}}}\")\n", 263 | " elif kind == \"adaptive\":\n", 264 | " loss = adaptive.learner.learner1D.curvature_loss_function()\n", 265 | " learner = adaptive.Learner1D(f, bounds=bounds, loss_per_interval=loss)\n", 266 | " adaptive.runner.simple(learner, goal=lambda l: l.npoints >= n_points)\n", 267 | " xs, ys = zip(*sorted(learner.data.items()))\n", 268 | " xs_dense = np.linspace(*bounds, 1000)\n", 269 | " ax.plot(xs_dense, f(xs_dense), c=\"red\", alpha=0.3, lw=0.5)\n", 270 | " ax.scatter(xs, ys, s=0.5, c=\"k\")\n", 271 | "\n", 272 | "axs[0][0].set_ylabel(r\"$\\textrm{homogeneous}$\")\n", 273 | "axs[1][0].set_ylabel(r\"$\\textrm{adaptive}$\")\n", 274 | "plt.savefig(\"figures/Learner1D.pdf\", bbox_inches=\"tight\", transparent=True)" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "## Learner2D vs grid" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "fig, axs = plt.subplots(2, len(funcs_2D), figsize=(fig_width, 1.5 * fig_height))\n", 291 | "\n", 292 | "plt.subplots_adjust(hspace=-0.1, wspace=0.1)\n", 293 | "\n", 294 | "with_tri = False\n", 295 | "\n", 296 | "for i, ax in enumerate(axs.T.flatten()):\n", 297 | " label = \"abcdef\"[i]\n", 298 | " ax.text(\n", 299 | " 0.5,\n", 300 | " 1.05,\n", 301 | " f\"$\\mathrm{{({label})}}$\",\n", 302 | " transform=ax.transAxes,\n", 303 | " horizontalalignment=\"center\",\n", 304 | " verticalalignment=\"bottom\",\n", 305 | " )\n", 306 | " ax.xaxis.set_ticks([])\n", 307 | " ax.yaxis.set_ticks([])\n", 308 | " kind = \"homogeneous\" if i % 2 == 0 else \"adaptive\"\n", 309 | " d = funcs_2D[i // 2] if kind == \"homogeneous\" else funcs_2D[(i - 1) // 2]\n", 310 | " bounds = d[\"bounds\"]\n", 311 | " npoints = d[\"npoints\"]\n", 312 | " f = d[\"function\"]\n", 313 | " fname = d.get(\"fname\")\n", 314 | " if fname is not None:\n", 315 | " f = functools.partial(f, fname=fname)\n", 316 | "\n", 317 | " if kind == \"homogeneous\":\n", 318 | " xs, ys = [np.linspace(*bound, npoints) for bound in bounds]\n", 319 | " data = {xy: f(xy) for xy in itertools.product(xs, ys)}\n", 320 | " learner = adaptive.Learner2D(f, bounds=bounds)\n", 321 | " learner.data = data\n", 322 | " d[\"learner_hom\"] = learner\n", 323 | " elif kind == \"adaptive\":\n", 324 | " learner = adaptive.Learner2D(f, bounds=bounds)\n", 325 | " if fname is not None:\n", 326 | " learner.load(fname)\n", 327 | " learner.data = {\n", 328 | " k: v for i, (k, v) in enumerate(learner.data.items()) if i <= npoints ** 2\n", 329 | " }\n", 330 | " adaptive.runner.simple(learner, goal=lambda l: l.npoints >= npoints ** 2)\n", 331 | " d[\"learner\"] = learner\n", 332 | "\n", 333 | " if with_tri:\n", 334 | " tri = learner.ip().tri\n", 335 | " triang = mtri.Triangulation(*tri.points.T, triangles=tri.vertices)\n", 336 | " ax.triplot(triang, c=\"w\", lw=0.2, alpha=0.8)\n", 337 | "\n", 338 | " values = np.array(list(learner.data.values()))\n", 339 | " plot_data = learner.plot(npoints if kind == \"homogeneous\" else None).Image.I.data\n", 340 | " im = ax.imshow(\n", 341 | " plot_data,\n", 342 | " extent=(-0.5, 0.5, -0.5, 0.5),\n", 343 | " interpolation=\"none\",\n", 344 | " )\n", 345 | " ax.set_xticks([])\n", 346 | " ax.set_yticks([])\n", 347 | "\n", 348 | " if i in [2, 3]:\n", 349 | " norm = HistogramNormalize(plot_data, mixing_degree=0.6)\n", 350 | " im.set_norm(norm)\n", 351 | "\n", 352 | "axs[0][0].set_ylabel(r\"$\\textrm{homogeneous}$\")\n", 353 | "axs[1][0].set_ylabel(r\"$\\textrm{adaptive}$\")\n", 354 | "\n", 355 | "plt.savefig(\"figures/Learner2D.pdf\", bbox_inches=\"tight\", transparent=True)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": null, 361 | "metadata": {}, 362 | "outputs": [], 363 | "source": [ 364 | "import time\n", 365 | "\n", 366 | "learner = adaptive.Learner1D(funcs_1D[0][\"function\"], bounds=funcs_1D[0][\"bounds\"])\n", 367 | "times = []\n", 368 | "for i in range(10000):\n", 369 | " t_start = time.time()\n", 370 | " points, _ = learner.ask(1)\n", 371 | " t_end = time.time()\n", 372 | " times.append(t_end - t_start)\n", 373 | " learner.tell(points[0], learner.function(points[0]))\n", 374 | "plt.plot(np.cumsum(times))" 375 | ] 376 | }, 377 | { 378 | "cell_type": "markdown", 379 | "metadata": {}, 380 | "source": [ 381 | "## Algorithm explaination: Learner1D step-by-step" 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": null, 387 | "metadata": {}, 388 | "outputs": [], 389 | "source": [ 390 | "fig, axs = plt.subplots(1, 1, figsize=(fig_width, 3 * fig_height))\n", 391 | "\n", 392 | "\n", 393 | "def f(x, offset=0.123):\n", 394 | " a = 0.2\n", 395 | " return a ** 2 / (a ** 2 + (x - offset) ** 2)\n", 396 | "\n", 397 | "\n", 398 | "learner = adaptive.Learner1D(\n", 399 | " f, (0, 2), loss_per_interval=adaptive.learner.learner1D.curvature_loss_function()\n", 400 | ")\n", 401 | "learner._recompute_losses_factor = 0.1\n", 402 | "xs_dense = np.linspace(*learner.bounds, 400)\n", 403 | "ys_dense = f(xs_dense)\n", 404 | "step = 0.4\n", 405 | "\n", 406 | "for i in range(11):\n", 407 | " offset = -i * step\n", 408 | "\n", 409 | " x = learner.ask(1)[0][0]\n", 410 | " y = f(x)\n", 411 | " learner.tell(x, y)\n", 412 | " xs, ys = map(np.array, zip(*sorted(learner.data.items())))\n", 413 | " ys = ys + offset\n", 414 | " if i >= 1:\n", 415 | " axs.plot(xs_dense, ys_dense + offset, c=\"k\", alpha=0.3, zorder=0)\n", 416 | " axs.plot(xs, ys, zorder=1, c=\"k\")\n", 417 | " axs.scatter(xs, ys, alpha=1, zorder=2, c=\"k\")\n", 418 | " (x_left, x_right), loss = list(learner.losses.items())[\n", 419 | " 0\n", 420 | " ] # it's an ItemSortedDict\n", 421 | " (y_left, y_right) = [\n", 422 | " learner.data[x_left] + offset,\n", 423 | " learner.data[x_right] + offset,\n", 424 | " ]\n", 425 | " axs.scatter([x_left, x_right], [y_left, y_right], c=\"r\", s=10, zorder=3)\n", 426 | " x_mid = np.mean((x_left, x_right))\n", 427 | " y_mid = np.interp(x_mid, (x_left, x_right), (y_left, y_right))\n", 428 | " axs.scatter(x_mid, y_mid, zorder=4, marker=\"x\", c=\"green\")\n", 429 | "\n", 430 | "axs.text(\n", 431 | " -0.1,\n", 432 | " 0.5,\n", 433 | " (r\"$\\mathrm{iterations}$\" + \"\\n\" + \"$\\longleftarrow$\"),\n", 434 | " transform=axs.transAxes,\n", 435 | " horizontalalignment=\"center\",\n", 436 | " verticalalignment=\"center\",\n", 437 | " rotation=90,\n", 438 | " fontsize=18,\n", 439 | ")\n", 440 | "\n", 441 | "\n", 442 | "# legend\n", 443 | "\n", 444 | "import matplotlib.patches as mpatches\n", 445 | "import matplotlib.lines as mlines\n", 446 | "\n", 447 | "\n", 448 | "class LargestInterval:\n", 449 | " pass\n", 450 | "\n", 451 | "\n", 452 | "class Interval:\n", 453 | " pass\n", 454 | "\n", 455 | "\n", 456 | "class Function:\n", 457 | " pass\n", 458 | "\n", 459 | "\n", 460 | "class IntervalHandler:\n", 461 | " def __init__(self, with_inner=True, length=20, *args, **kwargs):\n", 462 | " super().__init__(*args, **kwargs)\n", 463 | " self.with_inner = with_inner\n", 464 | " self.length = length\n", 465 | "\n", 466 | " def legend_artist(self, legend, orig_handle, fontsize, handlebox):\n", 467 | " x0, y0 = handlebox.xdescent, handlebox.ydescent\n", 468 | " offsets = [0, self.length]\n", 469 | " line = mlines.Line2D((0, offsets[-1]), (0, 0), zorder=0, c=\"k\")\n", 470 | " handlebox.add_artist(line)\n", 471 | "\n", 472 | " for offset in offsets:\n", 473 | " circle1 = mpatches.Circle(\n", 474 | " [x0 + offset, y0], 4, facecolor=\"k\", lw=3, zorder=1\n", 475 | " )\n", 476 | " handlebox.add_artist(circle1)\n", 477 | " if self.with_inner:\n", 478 | " circle2 = mpatches.Circle(\n", 479 | " [x0 + offset, y0], 3, facecolor=\"red\", lw=3, zorder=1\n", 480 | " )\n", 481 | " handlebox.add_artist(circle2)\n", 482 | "\n", 483 | "\n", 484 | "class FunctionHandler:\n", 485 | " def __init__(self, xs, ys, *args, **kwargs):\n", 486 | " super().__init__(*args, **kwargs)\n", 487 | " self.xs = xs / xs.ptp() * 20\n", 488 | " self.ys = ys - ys.mean()\n", 489 | "\n", 490 | " def legend_artist(self, legend, orig_handle, fontsize, handlebox):\n", 491 | " x0, y0 = handlebox.xdescent, handlebox.ydescent\n", 492 | "\n", 493 | " line = mlines.Line2D(self.xs, self.ys * 10, zorder=0, c=\"k\", alpha=0.3)\n", 494 | "\n", 495 | " handlebox.add_artist(line)\n", 496 | "\n", 497 | "\n", 498 | "plt.legend(\n", 499 | " [\n", 500 | " Function(),\n", 501 | " mlines.Line2D([], [], marker=\"o\", lw=0, c=\"k\"),\n", 502 | " LargestInterval(),\n", 503 | " Interval(),\n", 504 | " mlines.Line2D([], [], marker=\"x\", lw=0, c=\"green\"),\n", 505 | " ],\n", 506 | " [\n", 507 | " \"original function\",\n", 508 | " \"known point\",\n", 509 | " \"interval\",\n", 510 | " \"largest loss interval\",\n", 511 | " \"next candidate point\",\n", 512 | " ],\n", 513 | " handler_map={\n", 514 | " LargestInterval: IntervalHandler(False),\n", 515 | " Interval: IntervalHandler(True),\n", 516 | " Function: FunctionHandler(xs, ys),\n", 517 | " },\n", 518 | " bbox_to_anchor=(0.25, 0.9, 1.0, 0.0),\n", 519 | " ncol=1,\n", 520 | ")\n", 521 | "\n", 522 | "# On grid, uncomment for homogeneous plot\n", 523 | "# axs.plot(learner.bounds, [-(i + 0.5) * step, -(i + 0.5) * step], c='k', ls='--')\n", 524 | "# xs_hom = np.linspace(*learner.bounds, i)\n", 525 | "# ys_hom = f(xs_hom) - (i + 3) * step\n", 526 | "# axs.plot(xs_hom, ys_hom, zorder=1, c=\"k\")\n", 527 | "# axs.scatter(xs_hom, ys_hom, alpha=1, zorder=2, c=\"k\")\n", 528 | "\n", 529 | "axs.axis(\"off\")\n", 530 | "plt.savefig(\"figures/algo.pdf\", bbox_inches=\"tight\", transparent=True)\n", 531 | "plt.show()" 532 | ] 533 | }, 534 | { 535 | "cell_type": "markdown", 536 | "metadata": {}, 537 | "source": [ 538 | "## Line loss visualization" 539 | ] 540 | }, 541 | { 542 | "cell_type": "code", 543 | "execution_count": null, 544 | "metadata": {}, 545 | "outputs": [], 546 | "source": [ 547 | "from matplotlib.patches import Polygon\n", 548 | "\n", 549 | "fig, axs = plt.subplots(5, 1, figsize=(fig_width, 1.5 * fig_height))\n", 550 | "f = lambda x: np.sin(np.array(x)) ** 2\n", 551 | "xs = np.array([0, 1.3, 3, 5, 7, 8])\n", 552 | "ys = f(xs)\n", 553 | "\n", 554 | "\n", 555 | "def plot(xs, ax):\n", 556 | " ys = f(xs)\n", 557 | " xs_dense = np.linspace(xs[0], xs[-1], 300)\n", 558 | " ys_dense = f(xs_dense)\n", 559 | " ax.plot(xs_dense, ys_dense, alpha=0.3, c=\"k\")\n", 560 | " ax.plot(xs, ys, c=\"k\")\n", 561 | " ax.scatter(xs, ys, zorder=10, s=14, c=\"k\")\n", 562 | "\n", 563 | "\n", 564 | "plot(xs, axs[0])\n", 565 | "plot(xs, axs[1])\n", 566 | "\n", 567 | "for i, ax in enumerate(axs):\n", 568 | " ax.axis(\"off\")\n", 569 | " ax.set_ylim(-1.5, 1.5)\n", 570 | " label = \"abcdefgh\"[i]\n", 571 | " ax.text(\n", 572 | " 0.5,\n", 573 | " 0.9,\n", 574 | " f\"$\\mathrm{{({label})}}$\",\n", 575 | " transform=ax.transAxes,\n", 576 | " horizontalalignment=\"center\",\n", 577 | " verticalalignment=\"bottom\",\n", 578 | " )\n", 579 | "\n", 580 | "\n", 581 | "def plot_tri(xs, ax, colors):\n", 582 | " ys = f(xs)\n", 583 | " for i in range(len(xs)):\n", 584 | " if i == 0 or i == len(xs) - 1:\n", 585 | " continue\n", 586 | " verts = [(xs[i - 1], ys[i - 1]), (xs[i], ys[i]), (xs[i + 1], ys[i + 1])]\n", 587 | " poly = Polygon(verts, facecolor=colors[xs[i]], alpha=0.4)\n", 588 | " ax.add_patch(poly)\n", 589 | " ax.scatter([xs[i]], [ys[i]], c=colors[xs[i]], s=6, zorder=11)\n", 590 | " ax.plot(\n", 591 | " [xs[i - 1], xs[i + 1]], [ys[i - 1], ys[i + 1]], c=\"k\", ls=\"--\", alpha=0.3\n", 592 | " )\n", 593 | "\n", 594 | "\n", 595 | "learner = adaptive.Learner1D(\n", 596 | " f,\n", 597 | " (xs[0], xs[-1]),\n", 598 | " loss_per_interval=adaptive.learner.learner1D.curvature_loss_function(),\n", 599 | ")\n", 600 | "learner.tell_many(xs, ys)\n", 601 | "\n", 602 | "for i, ax in enumerate(axs[1:]):\n", 603 | " if i != 0:\n", 604 | " x_new = learner.ask(1)[0][0]\n", 605 | " learner.tell(x_new, f(x_new))\n", 606 | " xs, ys = zip(*sorted(learner.data.items()))\n", 607 | " plot(xs, ax)\n", 608 | " colors = {x: f\"C{i}\" for i, x in enumerate(learner.data.keys())}\n", 609 | " plot_tri(xs, ax, colors=colors)\n", 610 | "\n", 611 | "plt.savefig(\"figures/line_loss.pdf\", bbox_inches=\"tight\", transparent=True)\n", 612 | "plt.show()" 613 | ] 614 | }, 615 | { 616 | "cell_type": "markdown", 617 | "metadata": {}, 618 | "source": [ 619 | "## Line loss L1-error(N)" 620 | ] 621 | }, 622 | { 623 | "cell_type": "code", 624 | "execution_count": null, 625 | "metadata": {}, 626 | "outputs": [], 627 | "source": [ 628 | "import collections\n", 629 | "from adaptive import Learner1D, LearnerND\n", 630 | "from scipy import interpolate\n", 631 | "\n", 632 | "\n", 633 | "def err_1D(xs, ys, xs_rand, f):\n", 634 | " ip = interpolate.interp1d(xs, ys)\n", 635 | " abserr = np.abs(ip(xs_rand) - f(xs_rand))\n", 636 | " return np.average(abserr ** 2) ** 0.5\n", 637 | "\n", 638 | "\n", 639 | "def get_err_1D(learner, N):\n", 640 | " xs_rand = np.random.uniform(*learner.bounds, size=100_000)\n", 641 | "\n", 642 | " xs = np.linspace(*learner.bounds, N)\n", 643 | " ys = learner.function(xs)\n", 644 | " err_lin = err_1D(xs, ys, xs_rand, learner.function)\n", 645 | "\n", 646 | " xs, ys = zip(*learner.data.items())\n", 647 | " xs, ys = xs[:N], ys[:N]\n", 648 | " ip = interpolate.interp1d(xs, ys)\n", 649 | " err_learner = err_1D(xs, ys, xs_rand, learner.function)\n", 650 | " return err_lin, err_learner\n", 651 | "\n", 652 | "\n", 653 | "def err_2D(zs, zs_rand):\n", 654 | " abserr = np.abs(zs - zs_rand)\n", 655 | " return np.average(abserr ** 2) ** 0.5\n", 656 | "\n", 657 | "\n", 658 | "def get_err_2D(learner, N):\n", 659 | " xys_rand = np.vstack(\n", 660 | " [\n", 661 | " np.random.uniform(*learner.bounds[0], size=int(100_000)),\n", 662 | " np.random.uniform(*learner.bounds[1], size=int(100_000)),\n", 663 | " ]\n", 664 | " )\n", 665 | "\n", 666 | " xys_hom = np.array(\n", 667 | " [\n", 668 | " (x, y)\n", 669 | " for x in np.linspace(*learner.bounds[0], int(N ** 0.5))\n", 670 | " for y in np.linspace(*learner.bounds[1], int(N ** 0.5))\n", 671 | " ]\n", 672 | " )\n", 673 | " N = len(xys_hom)\n", 674 | "\n", 675 | " try:\n", 676 | " # Vectorized\n", 677 | " zs_hom = learner.function(xys_hom.T)\n", 678 | " zs_rand = learner.function(xys_rand)\n", 679 | " except:\n", 680 | " # Non-vectorized\n", 681 | " zs_hom = np.array([learner.function(xy) for xy in xys_hom])\n", 682 | " zs_rand = np.array([learner.function(xy) for xy in xys_rand.T])\n", 683 | "\n", 684 | " ip = interpolate.LinearNDInterpolator(xys_hom, zs_hom)\n", 685 | " zs = ip(xys_rand.T)\n", 686 | " err_lin = err_2D(zs, zs_rand)\n", 687 | "\n", 688 | " ip = interpolate.LinearNDInterpolator(learner.points[:N], learner.values[:N])\n", 689 | " zs = ip(xys_rand.T)\n", 690 | " err_learner = err_2D(zs, zs_rand)\n", 691 | "\n", 692 | " return err_lin, err_learner\n", 693 | "\n", 694 | "\n", 695 | "recalculate = False\n", 696 | "N_max = 10_000\n", 697 | "Ns = np.geomspace(4, N_max, 50).astype(int)\n", 698 | "fname = \"error_line_loss.pickle\"\n", 699 | "\n", 700 | "if not os.path.exists(fname) and not recalculate:\n", 701 | " loss_1D = adaptive.learner.learner1D.curvature_loss_function()\n", 702 | " loss_2D = adaptive.learner.learnerND.curvature_loss_function()\n", 703 | "\n", 704 | " err = collections.defaultdict(dict)\n", 705 | " for i, (funcs, loss, Learner, get_err) in enumerate(\n", 706 | " zip(\n", 707 | " [funcs_1D, funcs_2D],\n", 708 | " [loss_1D, loss_2D],\n", 709 | " [Learner1D, LearnerND],\n", 710 | " [get_err_1D, get_err_2D],\n", 711 | " )\n", 712 | " ):\n", 713 | " for d in funcs:\n", 714 | " learner = Learner(d[\"function\"], d[\"bounds\"], loss)\n", 715 | " adaptive.runner.simple(learner, goal=lambda l: l.npoints >= N_max)\n", 716 | " errs = [get_err(learner, N) for N in Ns]\n", 717 | " err_hom, err_adaptive = zip(*errs)\n", 718 | " err[i][d[\"title\"]] = (err_hom, err_adaptive)\n", 719 | "\n", 720 | " with open(fname, \"wb\") as f:\n", 721 | " pickle.dump(err, f)\n", 722 | "else:\n", 723 | " with open(fname, \"rb\") as f:\n", 724 | " err = pickle.load(f)" 725 | ] 726 | }, 727 | { 728 | "cell_type": "code", 729 | "execution_count": null, 730 | "metadata": {}, 731 | "outputs": [], 732 | "source": [ 733 | "fig, axs = plt.subplots(2, 1, figsize=(fig_width, 1.6 * fig_height))\n", 734 | "plt.subplots_adjust(hspace=0.3)\n", 735 | "\n", 736 | "axs[1].set_xlabel(\"$N$\")\n", 737 | "\n", 738 | "for i, ax in enumerate(axs):\n", 739 | " ax.set_ylabel(r\"$\\text{Err}_{1}(\\tilde{f})$\")\n", 740 | "\n", 741 | " for j, (title, (err_hom, err_adaptive)) in enumerate(err[i].items()):\n", 742 | " color = f\"C{j}\"\n", 743 | " label = \"abc\"[j]\n", 744 | " label = f\"$\\mathrm{{({label})}}$ {title}\"\n", 745 | " ax.loglog(Ns, err_hom, ls=\"--\", c=color)\n", 746 | " ax.loglog(Ns, err_adaptive, label=label, c=color)\n", 747 | "# error = np.array(err_hom) / np.array(err_adaptive)\n", 748 | "# if i == 0:\n", 749 | "# ax.loglog(Ns[:36], error[:36], c=color, label=label)\n", 750 | "# else:\n", 751 | "# ax.loglog(Ns, error, c=color, label=label)\n", 752 | " ax.legend()\n", 753 | "\n", 754 | "plt.savefig(\"figures/line_loss_error.pdf\", bbox_inches=\"tight\", transparent=True)\n", 755 | "plt.show()" 756 | ] 757 | }, 758 | { 759 | "cell_type": "code", 760 | "execution_count": null, 761 | "metadata": {}, 762 | "outputs": [], 763 | "source": [ 764 | "# Error reduction\n", 765 | "print(\"1D\")\n", 766 | "for title, (err_hom, err_adaptive) in err[0].items():\n", 767 | " print(title, err_hom[-1] / err_adaptive[-1])\n", 768 | "\n", 769 | "print(\"\\n2D\")\n", 770 | "for title, (err_hom, err_adaptive) in err[1].items():\n", 771 | " print(title, err_hom[-1] / err_adaptive[-1])" 772 | ] 773 | }, 774 | { 775 | "cell_type": "markdown", 776 | "metadata": {}, 777 | "source": [ 778 | "## iso-line plots" 779 | ] 780 | }, 781 | { 782 | "cell_type": "code", 783 | "execution_count": null, 784 | "metadata": {}, 785 | "outputs": [], 786 | "source": [ 787 | "from functools import lru_cache\n", 788 | "import numpy as np\n", 789 | "import scipy.linalg\n", 790 | "import scipy.spatial\n", 791 | "import kwant\n", 792 | "\n", 793 | "\n", 794 | "@lru_cache()\n", 795 | "def create_syst(unit_cell):\n", 796 | " lat = kwant.lattice.Polyatomic(unit_cell, [(0, 0, 0)])\n", 797 | " syst = kwant.Builder(kwant.TranslationalSymmetry(*lat.prim_vecs))\n", 798 | " syst[lat.shape(lambda _: True, (0, 0, 0))] = 6\n", 799 | " syst[lat.neighbors()] = -1\n", 800 | " return kwant.wraparound.wraparound(syst).finalized()\n", 801 | "\n", 802 | "\n", 803 | "def get_brillouin_zone(unit_cell):\n", 804 | " syst = create_syst(unit_cell)\n", 805 | " A = get_A(syst)\n", 806 | " neighbours = kwant.linalg.lll.voronoi(A)\n", 807 | " lattice_points = np.concatenate(([[0, 0, 0]], neighbours))\n", 808 | " lattice_points = 2 * np.pi * (lattice_points @ A.T)\n", 809 | " vor = scipy.spatial.Voronoi(lattice_points)\n", 810 | " brillouin_zone = vor.vertices[vor.regions[vor.point_region[0]]]\n", 811 | " return scipy.spatial.ConvexHull(brillouin_zone)\n", 812 | "\n", 813 | "\n", 814 | "def momentum_to_lattice(k, syst):\n", 815 | " A = get_A(syst)\n", 816 | " k, residuals = scipy.linalg.lstsq(A, k)[:2]\n", 817 | " if np.any(abs(residuals) > 1e-7):\n", 818 | " raise RuntimeError(\n", 819 | " \"Requested momentum doesn't correspond to any lattice momentum.\"\n", 820 | " )\n", 821 | " return k\n", 822 | "\n", 823 | "\n", 824 | "def get_A(syst):\n", 825 | " B = np.asarray(syst._wrapped_symmetry.periods).T\n", 826 | " return np.linalg.pinv(B).T\n", 827 | "\n", 828 | "\n", 829 | "def energies(k, unit_cell):\n", 830 | " syst = create_syst(unit_cell)\n", 831 | " k_x, k_y, k_z = momentum_to_lattice(k, syst)\n", 832 | " params = {\"k_x\": k_x, \"k_y\": k_y, \"k_z\": k_z}\n", 833 | " H = syst.hamiltonian_submatrix(params=params)\n", 834 | " energies = np.linalg.eigvalsh(H)\n", 835 | " return min(energies)\n", 836 | "\n", 837 | "\n", 838 | "from functools import partial\n", 839 | "\n", 840 | "from ipywidgets import interact_manual\n", 841 | "\n", 842 | "\n", 843 | "# Define the lattice vectors of some common unit cells\n", 844 | "lattices = dict(\n", 845 | " hexegonal=((0, 1, 0), (np.cos(-np.pi / 6), np.sin(-np.pi / 6), 0), (0, 0, 1)),\n", 846 | " simple_cubic=((1, 0, 0), (0, 1, 0), (0, 0, 1)),\n", 847 | " fcc=((0, 0.5, 0.5), (0.5, 0.5, 0), (0.5, 0, 0.5)),\n", 848 | " bcc=((-0.5, 0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, -0.5)),\n", 849 | ")\n", 850 | "\n", 851 | "\n", 852 | "from adaptive.learner.learnerND import volume\n", 853 | "\n", 854 | "\n", 855 | "def isoline_loss_function(level, priority):\n", 856 | " def loss(simplex, values, value_scale):\n", 857 | " values = np.array(values)\n", 858 | " which_side = np.sign(level * value_scale - values)\n", 859 | " crosses_isoline = np.any(np.diff(which_side))\n", 860 | " return volume(simplex)* (1 + priority * crosses_isoline)\n", 861 | " return loss\n", 862 | "\n", 863 | "level = 0.1\n", 864 | "loss_per_simplex = isoline_loss_function(level, 0.5)\n", 865 | "\n", 866 | "learners = []\n", 867 | "fnames = []\n", 868 | "for name, unit_cell in lattices.items():\n", 869 | " hull = get_brillouin_zone(unit_cell)\n", 870 | " learner = adaptive.LearnerND(\n", 871 | " partial(energies, unit_cell=unit_cell), hull, loss_per_simplex=loss_per_simplex\n", 872 | " )\n", 873 | " fnames.append(name)\n", 874 | " learners.append(learner)\n", 875 | "\n", 876 | "mapping = dict(zip(fnames, learners))\n", 877 | "\n", 878 | "learner = adaptive.BalancingLearner(learners, strategy=\"npoints\")\n", 879 | "\n", 880 | "\n", 881 | "def select(name, learners, fnames):\n", 882 | " return learners[fnames.index(name)]\n", 883 | "\n", 884 | "\n", 885 | "def iso(unit_cell, level=8.5):\n", 886 | " l = select(unit_cell, learners, fnames)\n", 887 | " adaptive.runner.simple(l, goal=lambda l: l.npoints > 1000)\n", 888 | " return l.plot_isosurface(level=level)\n", 889 | "\n", 890 | "\n", 891 | "interact_manual(iso, level=(-6, 9, 0.1), unit_cell=lattices.keys())" 892 | ] 893 | }, 894 | { 895 | "cell_type": "code", 896 | "execution_count": null, 897 | "metadata": {}, 898 | "outputs": [], 899 | "source": [ 900 | "def f(xy):\n", 901 | " x, y = xy\n", 902 | " return x ** 2 + y ** 3\n", 903 | "\n", 904 | "\n", 905 | "bounds = [(-1, 1), (-1, 1)]\n", 906 | "npoints = 12 ** 2\n", 907 | "\n", 908 | "\n", 909 | "level = 0.1\n", 910 | "loss = isoline_loss_function(level, priority=50)\n", 911 | "\n", 912 | "with_tri = True\n", 913 | "fig, axs = plt.subplots(1, 2, figsize=(1.5*fig_width, 1.5*fig_height))\n", 914 | "plt.subplots_adjust(wspace=0.3)\n", 915 | "\n", 916 | "for i, ax in enumerate(axs.flatten()):\n", 917 | " learner = adaptive.LearnerND(f, bounds, loss_per_simplex=loss)\n", 918 | " label = \"ab\"[i]\n", 919 | " ax.text(\n", 920 | " 0.5,\n", 921 | " 1.05,\n", 922 | " f\"$\\mathrm{{({label})}}$\",\n", 923 | " transform=ax.transAxes,\n", 924 | " horizontalalignment=\"center\",\n", 925 | " verticalalignment=\"bottom\",\n", 926 | " )\n", 927 | " ax.xaxis.set_ticks([])\n", 928 | " ax.yaxis.set_ticks([])\n", 929 | " kind = \"homogeneous\" if i == 0 else \"adaptive\"\n", 930 | "\n", 931 | " if kind == \"homogeneous\":\n", 932 | " xs, ys = [np.linspace(*bound, int(npoints ** 0.5)) for bound in bounds]\n", 933 | " data = {xy: f(xy) for xy in itertools.product(xs, ys)}\n", 934 | " learner.data = data\n", 935 | " elif kind == \"adaptive\":\n", 936 | " learner.data = {\n", 937 | " k: v for i, (k, v) in enumerate(learner.data.items()) if i <= npoints\n", 938 | " }\n", 939 | " adaptive.runner.simple(learner, goal=lambda l: l.npoints >= npoints)\n", 940 | "\n", 941 | " if with_tri:\n", 942 | " xy = np.array([learner.tri.get_vertices(s) for s in learner.tri.simplices])\n", 943 | " print(f\"{len(xy)} triangles\")\n", 944 | " triang = mtri.Triangulation(*xy.reshape(-1, 2).T)\n", 945 | " ax.triplot(triang, c=\"w\", lw=0.2, alpha=0.8)\n", 946 | "\n", 947 | " # Isolines\n", 948 | " vertices, lines = learner._get_iso(level, which=\"line\")\n", 949 | " paths = np.array([[vertices[i], vertices[j]] for i, j in lines]).T\n", 950 | " print(\"{} line segments\".format(len(paths.T)))\n", 951 | " ax.plot(*paths, c=\"k\", lw=0.7)\n", 952 | "\n", 953 | " values = np.array(list(learner.data.values()))\n", 954 | " ax.imshow(\n", 955 | " learner.plot(npoints if kind == \"homogeneous\" else None).Image.I.data,\n", 956 | " extent=(-1, 1, -1, 1),\n", 957 | " interpolation=\"none\",\n", 958 | " )\n", 959 | " ax.set_xticks([])\n", 960 | " ax.set_yticks([])\n", 961 | "\n", 962 | "axs[0].set_ylabel(r\"$\\textrm{homogeneous}$\")\n", 963 | "axs[1].set_ylabel(r\"$\\textrm{adaptive}$\")\n", 964 | "plt.savefig(\"figures/isoline.pdf\", bbox_inches=\"tight\", transparent=True)" 965 | ] 966 | } 967 | ], 968 | "metadata": { 969 | "language_info": { 970 | "name": "python", 971 | "pygments_lexer": "ipython3" 972 | } 973 | }, 974 | "nbformat": 4, 975 | "nbformat_minor": 2 976 | } 977 | -------------------------------------------------------------------------------- /paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '*Adaptive*: parallel active learning of mathematical functions' 3 | journal: 'PeerJ' 4 | author: 5 | - name: Tinkerer 6 | affiliation: 7 | - Kavli Institute of Nanoscience, Delft University of Technology, P.O. Box 4056, 2600 GA Delft, The Netherlands 8 | email: bas@nijho.lt 9 | abstract: | 10 | Large scale computer simulations are time-consuming to run and often require sweeps over input parameters to obtain a qualitative understanding of the simulation output. 11 | These sweeps of parameters can potentially make the simulations prohibitively expensive. 12 | Therefore, when evaluating a function numerically, it is advantageous to sample it more densely in the interesting regions (called adaptive sampling) instead of evaluating it on a manually-defined homogeneous grid. 13 | Such adaptive algorithms exist within the machine learning field. 14 | These methods can suggest a new point to calculate based on *all* existing data at that time; however, this is an expensive operation. 15 | An alternative is to use local algorithms---in contrast to the previously mentioned global algorithms---which can suggest a new point, based only on the data in the immediate vicinity of a new point. 16 | This approach works well, even when using hundreds of computers simultaneously because the point suggestion algorithm is cheap (fast) to evaluate. 17 | We provide a reference implementation in Python and show its performance. 18 | acknowledgements: | 19 | We'd like to thank ... 20 | contribution: | 21 | Bla 22 | ... 23 | 24 | # Introduction 25 | 26 | #### Simulations are costly and often require sampling a region in parameter space. 27 | In the computational sciences, one often does costly simulations---represented by a function $f$---where a certain region in parameter space $X$ is sampled, mapping to a codomain $Y$: $f \colon X \to Y$. 28 | Frequently, the different points in $X$ can be independently calculated. 29 | Even though it is suboptimal, one usually resorts to sampling $X$ on a homogeneous grid because of its simple implementation. 30 | 31 | #### Choosing new points based on existing data improves the simulation efficiency. 32 | 33 | An alternative, which improves the simulation efficiency, is to choose new potentially interesting points in $X$, based on existing data [@Gramacy2004; @Figueiredo1995; @Castro2008; @Chen2017]. 34 | Bayesian optimization works well for high-cost simulations where one needs to find a minimum (or maximum) [@Takhtaganov2018]. 35 | However, if the goal of the simulation is to approximate a continuous function using the fewest points, an alternative strategy is to use a greedy algorithm that samples mid-points of intervals with the largest length or curvature [@Wolfram2011]. 36 | Such a sampling strategy (i.e., in Fig. @fig:algo) would trivially speedup many simulations. 37 | Another advantage of such an algorithm is that it may be parallelized cheaply (i.e. more than one point may be sampled at a time), as we do not need to perform a global computation over all the data (as we would with Bayesian sampling) when determining which points to sample next. 38 | 39 | ![Visualization of a 1-D sampling strategy for a black-box function (grey). 40 | We start by calculating the two boundary points. 41 | Two adjacent existing data points (black) $\{x_i, y_i\}$ define an interval. 42 | Each interval has a loss $L_{i,i+1}$ associated with it that can be calculated from the points inside the interval $L_{i,i+1}(x_i, x_{i+1}, y_i, y_{i+1})$ and optionally of $N$ next nearest neighboring intervals. 43 | At each iteration the interval with the largest loss is indicated (red), with its corresponding candidate point (green) picked in the middle of the interval. 44 | The loss function in this example is an approximation to the curvature, calculated using the data from an interval and its nearest neighbors. 45 | ](figures/algo.pdf){#fig:algo} 46 | 47 | #### We describe a class of algorithms relying on local criteria for sampling, which allow for easy parallelization and have a low overhead. 48 | 49 | The algorithm visualized in @fig:algo consists of the following steps: 50 | (1) evaluate the function at the boundaries $a$ and $b$, of the interval of interest, 51 | (2) calculate the loss for the interval $L_{a, b} = \sqrt{(b - a)^2 + (f(b) - f(a))^2}$, 52 | (3) pick a new point $x_\textrm{new}$ in the centre of the interval with the largest loss, $(x_i, x_j)$, 53 | (4) calculate $f(x_\textrm{new})$, 54 | (5) discard the interval $(x_i, x_j)$ and create two new intervals $(x_i, x_\textrm{new})$ and $(x_\textrm{new}, x_j)$, calculating their losses $L_{x_i, x_\textrm{new}}$ and $L_{x_\textrm{new}, x_j}$ 55 | (6) repeat from step 3. 56 | 57 | In this paper we present a class of algorithms that generalizes the above example. 58 | This general class of algorithms is based on using a *priority queue* of subdomains (intervals in 1-D), ordered by a *loss* obtained from a *local loss function* (which depends only on the data local to the subdomain), and greedily selecting points from subdomains at the top of the priority queue. 59 | The advantage of these *local* algorithms is that they have a lower computational overhead than algorithms requiring *global* data and updates (e.g. Bayesian sampling), and are therefore more amenable to parallel evaluation of the function of interest. 60 | 61 | ![Comparison of homogeneous sampling (top) with adaptive sampling (bottom) for different one-dimensional functions (red) where the number of points in each column is identical. 62 | We see that when the function has a distinct feature---such as with the peak and tanh---adaptive sampling performs much better. 63 | When the features are homogeneously spaced, such as with the wave packet, adaptive sampling is not as effective as in the other cases.](figures/Learner1D.pdf){#fig:Learner1D} 64 | 65 | ![Comparison of homogeneous sampling (top) with adaptive sampling (bottom) for different two-dimensional functions where the number of points in each column is identical. 66 | On the left is the function $f(x) = x + a ^ 2 / (a ^ 2 + (x - x_\textrm{offset}) ^ 2)$. 67 | In the middle a topological phase diagram from \onlinecite{Nijholt2016}, where the function can take the values -1 or 1. 68 | On the right, we plot level crossings for a two-level quantum system. 69 | In all cases using Adaptive results in a higher fidelity plot. 70 | ](figures/Learner2D.pdf){#fig:Learner2D} 71 | 72 | 73 | #### We provide a reference implementation, the Adaptive package, and demonstrate its performance. 74 | We provide a reference implementation, the open-source Python package called Adaptive [@Nijholt2019a], which has previously been used in several scientific publications [@Vuik2018; @Laeven2019; @Bommer2019; @Melo2019]. 75 | It has algorithms for $f \colon \mathbb{R}^N \to \mathbb{R}^M$, where $N, M \in \mathbb{Z}^+$ but which work best when $N$ is small; integration in $\mathbb{R}$; and the averaging of stochastic functions. 76 | Most of our algorithms allow for a customizable loss function with which one can adapt the sampling algorithm to work optimally for different classes of functions. 77 | It integrates with the Jupyter notebook environment as well as popular parallel computation frameworks such as `ipyparallel`, `mpi4py`, and `dask.distributed`. 78 | It provides auxiliary functionality such as live-plotting, inspecting the data as the calculation is in progress, and automatically saving and loading of the data. 79 | 80 | The raw data and source code that produces all plots in this paper is available at \onlinecite{papercode}. 81 | 82 | # Review of adaptive sampling{#sec:review} 83 | 84 | Optimal sampling and planning based on data is a mature field with different communities providing their own context, restrictions, and algorithms to solve their problems. 85 | To explain the relation of our approach with prior work, we discuss several existing contexts. 86 | This is not a systematic review of all these fields, but rather, we aim to identify the important traits and design considerations. 87 | 88 | #### Experiment design uses Bayesian sampling because the computational costs are not a limitation. 89 | Optimal experiment design (OED) is a field of statistics that minimizes the number of experimental runs needed to estimate specific parameters and, thereby, reduce the cost of experimentation [@Emery1998]. 90 | It works with many degrees of freedom and can consider constraints, for example, when the sample space contains regions that are infeasible for practical reasons. 91 | One form of OED is response-adaptive design [@Hu2006], which concerns the adaptive sampling of designs for statistical experiments. 92 | Here, the acquired data (i.e., the observations) are used to estimate the uncertainties of a certain desired parameter. 93 | It then suggests further experiments that will optimally reduce these uncertainties. 94 | In this step of the calculation Bayesian statistics is frequently used. 95 | Bayesian statistics naturally provides tools for answering such questions; however, because it provides closed-form solutions, Markov chain Monte Carlo (MCMC) sampling is the standard tool for determining the most promising samples. 96 | In a typical non-adaptive experiment, decisions on which experiments to perform are made in advance. 97 | 98 | #### Plotting and low dimensional integration uses local sampling. 99 | Plotting a low dimensional function in between bounds requires one to evaluate the function on sufficiently many points such that when we interpolate values in between data points, we get an accurate description of the function values that were not explicitly calculated. 100 | In order to minimize the number of function evaluations, one can use adaptive sampling routines. 101 | For example, for one-dimensional functions, Mathematica [@WolframResearch] implements a `FunctionInterpolation` class that takes the function, $x_\textrm{min}$, and $x_\textrm{max}$, and returns an object that samples the function more densely in regions with high curvature; however, details on the algorithm are not published. 102 | Subsequently, we can query this object for points in between $x_\textrm{min}$ and $x_\textrm{max}$, and get the interpolated value, or we can use it to plot the function without specifying a grid. 103 | Another application for adaptive sampling is numerical integration. 104 | It works by estimating the integration error of each interval and then minimizing the sum of these errors greedily. 105 | For example, the `CQUAD` algorithm [@Gonnet2010] in the GNU Scientific Library [@Galassi1996] implements a more sophisticated strategy and is a doubly-adaptive general-purpose integration routine which can handle most types of singularities. 106 | In general, it requires more function evaluations than the integration routines in `QUADPACK` [@Galassi1996]; however, it works more often for difficult integrands. 107 | It is doubly-adaptive because it can decide to either subdivide intervals into more intervals or refine an interval by using a polynomial approximation of higher degree, requiring more points. 108 | 109 | #### PDE solvers and computer graphics use adaptive meshing. 110 | Hydrodynamics [@Berger1989; @Berger1984] and astrophysics [@Klein1999] use an adaptive refinement of the triangulation mesh on which a partial differential equation is discretized. 111 | By providing smaller mesh elements in regions with a higher variation of the solution, they reduce the amount of data and calculation needed at each step of time propagation. 112 | The remeshing at each time step happens globally, and this is an expensive operation. 113 | Therefore, mesh optimization does not fit our workflow because expensive global updates should be avoided. 114 | Computer graphics uses similar adaptive methods where a smooth surface can represent a surface via a coarser piecewise linear polygon mesh, called a subdivision surface [@DeRose1998]. 115 | An example of such a polygonal remeshing method is one where the polygons align with the curvature of the space or field; this is called anisotropic meshing [@Alliez2003]. 116 | 117 | # Design constraints and the general algorithm{#sec:design} 118 | 119 | #### We aim to sample low to intermediate cost functions in parallel. 120 | The general algorithm that we describe in this paper works best for low to intermediate cost functions. 121 | Determining the next candidate points happens in a single sequential process while the function executions can be in parallel. 122 | This means that to benefit from an adaptive sampling algorithm, that the time it takes to suggest a new point $t_\textrm{suggest}$ must be much smaller than the average function execution time $t_f$ over the number of parallel workers $N$: $t_f / N \gg t_\textrm{suggest}$. 123 | Functions that are fast to evaluate can be calculated on a dense grid, and functions that are slow to evaluate might benefit from full-scale Bayesian optimization where $t_\textrm{suggest}$ is large. 124 | We are interested in the intermediate case, when one wishes to sample adaptively, but cannot afford the luxury of fitting of all available data at each step. 125 | While this may seem restrictive, we assert that a large class of functions is inside the right regime for local adaptive sampling to be beneficial. 126 | 127 | #### We propose to use a local loss function as a criterion for choosing the next point. 128 | Because we aim to keep the suggestion time $t_\textrm{suggest}$ small, we propose to use the following approach, which operates on a constant-size subset of the data to determine which point to suggest next. 129 | We keep track of the subdomains in a priority queue, where each subdomain is assigned a priority called the "loss". 130 | To suggest a new point we remove the subdomain with the largest loss from the priority queue and select a new point $x_\textrm{new}$ from within it (typically in the centre) 131 | This splits the subdomain into several smaller subdomains $\{S_i\}$ that each contain $x_\textrm{new}$ on their boundaries. 132 | After evaluating the function at $x_\textrm{new}$ we must then recompute the losses using the new data. 133 | We choose to consider loss functions that are "local", i.e. the loss for a subdomain depends only on the points contained in that subdomain and possibly a (small) finite number of neighboring subdomains. 134 | This means that we need only recalculate the losses for subdomains that are "close" to $x_\textrm{new}$. 135 | Having computed the new losses we must then insert the $\{S_i\}$ into the priority queue, and also update the priorities of the neighboring subdomains, if their loss was recalculated. 136 | After these insertions and updates we are ready to suggest the next point to evaluate. 137 | Due to the local nature of this algorithm and the sparsity of space in higher dimensions, we will suffer from the curse of dimensionality. 138 | The algorithm, therefore, works best in low dimensional space; typically calculations that can reasonably be plotted, so with 1, 2, or 3 degrees of freedom. 139 | 140 | #### We summarize the algorithm with pseudocode 141 | 142 | The algorithm described above can be made more precise by the following Python code: 143 | 144 | ```python 145 | # First evaluate the bounds of the domain 146 | first_subdomain, = domain.subdomains() 147 | for x in domain.points(first_subdomain): 148 | data[x] = f(x) 149 | 150 | queue.insert(first_subdomain, priority=loss(domain, first_subdomain, data)) 151 | 152 | while queue.max_priority() < target_loss: 153 | loss, subdomain = queue.pop() 154 | 155 | new_points, new_subdomains = domain.split(subdomain) 156 | for x in new_points: 157 | data[x] = f(x) 158 | 159 | for subdomain in new_subdomains: 160 | queue.insert(subdomain, priority=loss(domain, subdomain, data)) 161 | 162 | if loss.n_neighbors > 0: 163 | subdomains_to_update = set() 164 | for d in new_subdomains: 165 | neighbors = domain.neighbors(d, loss.n_neighbors) 166 | subdomains_to_update.update(neighbors) 167 | subdomains_to_update -= set(new_subdomains) 168 | for subdomain in subdomains_to_update: 169 | queue.update(subdomain, priority=loss(domain, subdomain, data)) 170 | ``` 171 | 172 | where we have used the following definitions: 173 | 174 | `f` 175 | 176 | : The function we wish to learn 177 | 178 | `queue` 179 | 180 | : A priority queue of unique elements, supporting the following methods: `max_priority()`, to get the priority of the top element; `pop()`, remove and return the top element and its priority; `insert(element, priority)`, insert the given element with the given priority into the queue; `update(element, priority)`, update the priority of the given element, which is already in the queue. 181 | 182 | `domain` 183 | 184 | : An object representing the domain of `f` split into subdomains. Supports the following methods: `subdomains()`, returns all the subdomains; `points(subdomain)`, returns all the points contained in the provided subdomain; `split(subdomain)`, splits a subdomain into smaller subdomains, returning the new points and new subdomains produced as a result; `neighbors(subdomain, n_neighbors)`, returns the subdomains neighboring the provided subdomain. 185 | 186 | `data` 187 | 188 | : A hashmap storing the points `x` and their values `f(x)`. 189 | 190 | `loss(domain, subdomain, data)` 191 | 192 | : The loss function, with `loss.n_neighbors` being the degree of neighboring subdomains that the loss function uses. 193 | 194 | #### As an example, the interpoint distance is a good loss function in one dimension. 195 | An example of such a local loss function for a one-dimensional function is the interpoint distance, i.e. given a subdomain (interval) $(x_\textrm{a}, x_\textrm{b})$ with values $(y_\textrm{a}, y_\textrm{b})$ the loss is $\sqrt{(x_\textrm{a} - x_\textrm{b})^2 + (y_\textrm{a} - y_\textrm{b})^2}$. 196 | A more complex loss function that also takes the first neighboring intervals into account is one that approximates the second derivative using a Taylor expansion. 197 | Figure @fig:Learner1D shows a comparison between a result using this loss and a function that is sampled on a grid. 198 | 199 | #### This algorithm has a logarithmic overhead when combined with an appropriate data structure 200 | The key data structures in the above algorithm are `queue` and `domain`. 201 | The priority queue must support efficiently finding and removing the maximum priority element, as well as updating the priority of arbitrary elements whose priority is unknown (when updating the loss of neighboring subdomains). 202 | Such a datastructure can be achieved with a combination of a hashmap (mapping elements to their priority) and a red--black tree or a skip list [@Cormen2009] that stores `(priority, element)`. 203 | This has average complexity of $\mathcal{O}(\log{n})$ for all the required operations. 204 | In the reference implementation, we use the SortedContainers Python package [@Jenks2014], which provides an efficient implementation of such a data structure optimized for realistic sizes, rather than asymptotic complexity. 205 | The `domain` object requires efficiently splitting a subdomain and querying the neighbors of a subdomain. 206 | For the one-dimensional case this can be achieved by using a red--black tree to keep the points $x$ in ascending order. 207 | In this case both operations have an average complexity of $\mathcal{O}(\log{n})$. 208 | In the reference implementation we again use SortedContainers. 209 | We thus see that by using the appropriate data structures the time required to suggest a new point is $t_\textrm{suggest} \propto \mathcal{O}(\log{n})$. 210 | The total time spent on suggesting points when sampling $N$ points in total is thus $\mathcal{O}(N \log{N})$. 211 | 212 | #### With many points, due to the loss being local, parallel sampling incurs no additional cost. 213 | So far, the description of the general algorithm did not include parallelism. 214 | In order to include parallelism we need to allow for points that are "pending", i.e. whose value has been requested but is not yet known. 215 | In the sequential algorithm subdomains only contain points on their boundaries. 216 | In the parallel algorithm *pending* points are placed in the interior of subdomains, and the priority of the subdomains in the queue is reduced to take these pending points into account. 217 | Later, when a pending point $x$ is finally evaluated, we *split* the subdomain that contains $x$ such that it is on the boundary of new, smaller, subdomains. 218 | We then calculate the priority of these new subdomains, and insert them into the priority queue, and update the priority of neighboring subdomains if required. 219 | 220 | #### We summarize the algorithm with pseudocode 221 | The parallel version of the algorithm can be described by the following Python code: 222 | 223 | ```python 224 | def priority(domain, subdomain, data): 225 | subvolumes = domain.subvolumes(subdomain) 226 | max_relative_subvolume = max(subvolumes) / sum(subvolumes) 227 | L_0 = loss(domain, subdomain, data) 228 | return max_relative_subvolume * L_0 229 | 230 | # First evaluate the bounds of the domain 231 | first_subdomain, = domain.subdomains() 232 | for x in domain.points(first_subdomain): 233 | data[x] = f(x) 234 | 235 | new_points = domain.insert_points(first_subdomain, executor.ncores) 236 | for x in new_points: 237 | data[x] = None 238 | executor.submit(f, x) 239 | 240 | queue.insert(first_subdomain, priority=priority(domain, subdomain, data)) 241 | 242 | while executor.n_outstanding_points > 0: 243 | x, y = executor.get_one_result() 244 | data[x] = y 245 | 246 | # Split into smaller subdomains with `x` at a subdomain boundary 247 | # And calculate the losses for these new subdomains 248 | old_subdomains, new_subdomains = domain.split_at(x) 249 | for subdomain in old_subdomains: 250 | queue.remove(old_subdomain) 251 | for subdomain in new_subdomains: 252 | queue.insert(subdomain, priority(domain, subdomain, data)) 253 | 254 | if loss.n_neighbors > 0: 255 | subdomains_to_update = set() 256 | for d in new_subdomains: 257 | neighbors = domain.neighbors(d, loss.n_neighbors) 258 | subdomains_to_update.update(neighbors) 259 | subdomains_to_update -= set(new_subdomains) 260 | for subdomain in subdomains_to_update: 261 | queue.update(subdomain, priority(domain, subdomain, data)) 262 | 263 | # If it looks like we're done, don't send more work 264 | if queue.max_priority() < target_loss: 265 | continue 266 | 267 | # Send as many points for evaluation as we have compute cores 268 | for _ in range(executor.ncores - executor.n_outstanding_points) 269 | loss, subdomain = queue.pop() 270 | new_point, = domain.insert_points(subdomain, 1) 271 | data[new_point] = None 272 | executor.submit(f, new_point) 273 | queue.insert(subdomain, priority(domain, subdomain, data)) 274 | ``` 275 | 276 | Where we have used identical definitions to the serial case for `f`, `data`, `loss` and the following additional definitions: 277 | 278 | `queue` 279 | 280 | : As for the sequential case, but must additionally support: `remove(element)`, remove the provided element from the queue. 281 | 282 | `domain` 283 | 284 | : As for the sequential case, but must additionally support: `insert_points(subdomain, n)`, insert `n` (pending) points into the given subdomain without splitting the subdomain; `subvolumes(subdomain)`, return the volumes of all the sub-subdomains contained within the given subdomain; `split_at(x)`, split the domain at a new (evaluated) point `x`, returning the old subdomains that were removed, and the new subdomains that were added as a result. 285 | 286 | `executor` 287 | 288 | : An object that can submit function evaluations to computing resources and retrieve results. 289 | Supports the following methods: `submit(f, x)`, schedule the execution of `f(x)` and do not block ; `get_one_result()`, block waiting for a single result, returning the pair `(x, y)` as soon as it becomes available; `ncores`, the total number of parallel processing units; `n_outstanding_points`, the number of function evaluations that have been requested and not yet retrieved, incremented by `submit` and decremented by `get_one_result`. 290 | 291 | # Loss function design{#sec:loss} 292 | 293 | #### Sampling in different problems pursues different goals 294 | Not all goals are achieved by using an identical sampling strategy; the specific problem determines the goal. 295 | For example, quadrature rules requires a denser sampling of the subdomains where the interpolation error is highest, plotting (or function approximation) requires continuity of the approximation, maximization only cares about finding an optimum, and isoline or isosurface sampling aims to sample regions near a given function value more densely. 296 | These different sampling goals each require a loss function tailored to the specific case. 297 | 298 | #### Different loss functions tailor sampling performance for different classes of functions 299 | Additionally, it is important to take the class of functions being learned when selecting a loss function into account, even if the specific goal (e.g. continuity of the approximation) remains unchanged. 300 | For example, if we wanted a smooth approximation to a function with a singularity, then the interpoint distance loss function would be a poor choice, even if it is generally a good choice for that specified goal. 301 | This is because the aforementioned loss function will "lock on" to the singularity, and will fail to sample the function elsewhere once it starts. 302 | This is an illustration of the following principle: for optimal sampling performance, loss functions should be tailored to the particular domain of interest. 303 | 304 | #### Loss function regularization avoids singularities 305 | One strategy for designing loss functions is to take existing loss functions and apply a regularization. 306 | For example, to limit the over-sampling of singularities inherent in the distance loss we can set the loss of subdomains that are smaller than a given threshold to zero, which will prevent them from being sampled further. 307 | 308 | #### Adding loss functions allows for balancing between multiple priorities. 309 | Another general strategy for designing loss functions is to combine existing loss functions that optimize for particular features, and then combine them together. 310 | Typically one weights the different constituent losses to prioritize the different features. 311 | For example, combining a loss function that calculates the curvature with a distance loss function will sample regions with high curvature more densely, while ensuring continuity. 312 | Another important example is combining a loss function with the volume of the subdomain, which will ensure that the sampling is asymptotically dense everywhere (because large subdomains will have a correspondingly large loss). 313 | This is important if there are many distinct and narrow features that all need to be found, and densely sampled in the region around the feature. 314 | 315 | # Examples 316 | 317 | ## Line simplification loss 318 | 319 | #### The line simplification loss is based on an inverse Visvalingam’s algorithm. 320 | Inspired by a method commonly employed in digital cartography for coastline simplification, Visvalingam's algorithm, we construct a loss function that does its reverse [@Visvalingam1990]. 321 | Here, at each point (ignoring the boundary points), we compute the effective area associated with its triangle, see Fig. @fig:line_loss(b). 322 | The loss then becomes the average area of two adjacent triangles. 323 | By Taylor expanding $f$ around $x$ it can be shown that the area of the triangles relates to the contributions of the second derivative. 324 | We can generalize this loss to $N$ dimensions, where the triangle is replaced by a $(N+1)$ dimensional simplex. 325 | 326 | ![Line loss visualization. 327 | In this example, we start with 6 points (a) on the function (grey). 328 | Ignoring the endpoints, the effective area of each point is determined by its associated triangle (b). 329 | The loss of each interval can be computed by taking the average area of the adjacent triangles. 330 | Subplots (c), (d), and (e) show the subsequent iterations following (b).](figures/line_loss.pdf){#fig:line_loss} 331 | 332 | In order to compare sampling strategies, we need to define some error. 333 | We construct a linear interpolation function $\tilde{f}$, which is an approximation of $f$. 334 | We calculate the error in the $L^{1}$-norm, defined as, 335 | $$ 336 | \text{Err}_{1}(\tilde{f})=\left\Vert \tilde{f}-f\right\Vert _{L^{1}}=\int_{a}^{b}\left|\tilde{f}(x)-f(x)\right|\text{d}x. 337 | $$ 338 | This error approaches zero as the approximation becomes better. 339 | 340 | ![The $L^{1}$-norm error as a function of number of points $N$ for the functions in Fig. @fig:Learner1D (a,b,c). 341 | The interrupted lines correspond to homogeneous sampling and the solid line to the sampling with the line loss. 342 | In all cases adaptive sampling performs better, where the error is a factor 1.6-20 lower for $N=10000$. 343 | ](figures/line_loss_error.pdf){#fig:line_loss_error} 344 | 345 | Figure @fig:line_loss_error shows this error as a function of the number of points $N$. 346 | Here, we see that for homogeneous sampling to get the same error as sampling with a line loss, a factor $\approx 1.6-20$ times more points are needed, depending on the function. 347 | 348 | ## A parallelizable adaptive integration algorithm based on cquad 349 | 350 | #### The `cquad` algorithm belongs to a class that is parallelizable. 351 | In @sec:review we mentioned the doubly-adaptive integration algorithm `CQUAD` [@Gonnet2010]. 352 | This algorithm uses a Clenshaw-Curtis quadrature rules of increasing degree $d$ in each interval [@Clenshaw1960]. 353 | The error estimate is $\sqrt{\int{\left(f_0(x) - f_1(x)\right)^2}}$, where $f_0$ and $f_1$ are two successive interpolations of the integrand. 354 | To reach the desired total error, intervals with the maximum absolute error are improved. 355 | Either (1) the degree of the rule is increased or (2) the interval is split if either the function does not appear to be smooth or a rule of maximum degree ($d=4$) has been reached. 356 | All points inside the intervals can be trivially calculated in parallel; however, when there are more resources available than points, Adaptive needs to guess whether an (1) interval's should degree of the rule should be increased or (2) or the interval is split. 357 | Here, we choose to always increase until $d=4$, after which the interval is split. 358 | 359 | ## isoline and isosurface sampling 360 | A judicious choice of loss function allows to sample the function close to an isoline (isosurface in 2D). Specifically, we prioritize subdomains that are bisected by the isoline or isosurface: 361 | 362 | ```python 363 | def isoline_loss_function(level, priority): 364 | def loss(simplex, values, value_scale): 365 | values = np.array(values) 366 | which_side = np.sign(level * value_scale - values) 367 | crosses_isoline = np.any(np.diff(which_side)) 368 | return volume(simplex)* (1 + priority * crosses_isoline) 369 | return loss 370 | ``` 371 | 372 | See Fig. @fig:isoline for a comparison with uniform sampling. 373 | 374 | ![Comparison of isoline sampling of $f(x,y)=x^2 + y^3$ at $f(x,y)=0.1$ using homogeneous sampling (left) and adaptive sampling (right) with the same amount of points $n=12^2=144$. 375 | We plot the function interpolated on a grid (color) with the triangulation on top (white) where the function is sampled on the vertices. 376 | The solid line (black) indicates the isoline at $f(x,y)=0.1$. 377 | The isoline in the homogeneous case consists of 43 line segments and the adaptive case consists of 94 line segments. 378 | ](figures/isoline.pdf){#fig:isoline} 379 | 380 | # Implementation and benchmarks 381 | 382 | #### The learner abstracts a loss based priority queue. 383 | We will now introduce Adaptive's API. 384 | The object that can suggest points based on existing data is called a *learner*. 385 | The learner abstracts the sampling strategy based on a priority queue and local loss functions that we described in @sec:design. 386 | We define a learner as follows: 387 | ```python 388 | from adaptive import Learner1D 389 | 390 | def f(x): 391 | a = 0.01 392 | return x + a**2 / (a**2 + x**2) 393 | 394 | learner = Learner1D(f, bounds=(-1, 1)) 395 | ``` 396 | We provide the function to learn, the domain boundaries, and use a default loss function. 397 | We can then *ask* the learner for points: 398 | ```python 399 | points, priorities = learner.ask(4) 400 | ``` 401 | The learner gives us back the points that we should sample next, as well as the priorities of these points (the loss of the parent subdomains). 402 | We can then evaluate some of these points and *tell* the learner about the results: 403 | ```python 404 | data = [learner.function(x) for x in points] 405 | learner.tell_many(points, data) 406 | ``` 407 | To change the loss function we pass a function that takes points and values, like so: 408 | ```python 409 | def distance_loss(xs, ys): # used by default 410 | dx = xs[1] - xs[0] 411 | dy = ys[1] - ys[0] 412 | return np.hypot(dx, dy) 413 | 414 | learner = Learner1D(peak, bounds=(-1, 1), loss_per_interval=distance_loss) 415 | ``` 416 | If we wanted to create the "volume loss" discussed in @sec:loss we could simply write: 417 | ```python 418 | def uniform_loss(xs, ys): 419 | dx = xs[1] - xs[0] 420 | return dx 421 | 422 | learner = Learner1D(peak, bounds=(-1, 1), loss_per_interval=uniform_loss) 423 | ``` 424 | 425 | #### The runner orchestrates the function evaluation. 426 | The previous example shows how we can drive the learner manually. 427 | For example, to run the learner until the loss is below `0.01` we could do the following: 428 | ```python 429 | def goal(learner): 430 | return learner.loss() < 0.01 431 | 432 | while not goal(learner): 433 | (x,), _ = learner.ask(1) 434 | y = f(x) 435 | learner.tell(x, y) 436 | ``` 437 | This approach allows for the best *adaptive* performance (i.e. fewest number of points to reach the goal) because the learner has maximal information about `f` every time we ask it for the next point. 438 | However this does not allow to take advantage of multiple cores, which may enable better *walltime* performance (i.e. time to reach the goal). 439 | Adaptive abstracts the task of driving the learner and executing `f` in parallel to a *Runner*: 440 | ```python 441 | from adaptive import Runner 442 | runner = Runner(learner, goal) 443 | ``` 444 | The above code uses the default parallel execution context, which occupies all the cores on the machine. 445 | It is simple to use *ipyparallel* to enable calculations on a cluster: 446 | ```python 447 | import ipyparallel 448 | 449 | runner = Runner(learner, goal, executor=ipyparallel.Client()) 450 | ``` 451 | If the above code is run in a Jupyter notebook it will not block. 452 | Adaptive takes advantage of the capabilities of the IPython to execute concurrently with the Python kernel. 453 | This means that as the calculation is in progress the data is accessible without race conditions via `learner.data`, and can be plotted with `learner.plot()`. 454 | Additionally, in a Jupyter notebook environment, we can call `runner.live_info()` to display useful information about the ongoing calculation. 455 | 456 | We have also implemented a `LearnerND` with a similar API 457 | ```python 458 | from adaptive import LearnerND 459 | 460 | def ring(xy): # pretend this is a slow function 461 | x, y = xy 462 | a = 0.2 463 | return x + np.exp(-(x**2 + y**2 - 0.75**2)**2/a**4) 464 | 465 | learner = adaptive.LearnerND(ring, bounds=[(-1, 1), (-1, 1)]) 466 | runner = Runner(learner, goal) 467 | ``` 468 | 469 | Again, it is possible to specify a custom loss function using the `loss_per_simplex` argument. 470 | 471 | #### The BalancingLearner can run many learners simultaneously. 472 | Frequently, more than one function (learner) needs to run at once, to do this we have implemented the `BalancingLearner`, which does not take a function, but a list of learners. 473 | This learner internally asks all child learners for points and will choose the point of the learner that maximizes the loss improvement; it balances the resources over the different learners. 474 | We can use it like 475 | ```python 476 | from functools import partial 477 | from adaptive import BalancingLearner 478 | 479 | def f(x, pow): 480 | return x**pow 481 | 482 | learners = [Learner1D(partial(f, pow=i)), bounds=(-10, 10) for i in range(2, 10)] 483 | bal_learner = BalancingLearner(learners) 484 | runner = Runner(bal_learner, goal) 485 | ``` 486 | For more details on how to use Adaptive, we recommend reading the tutorial inside the documentation [@Nijholt2018]. 487 | 488 | # Possible extensions 489 | 490 | #### Anisotropic triangulation would improve the algorithm. 491 | One of the fundamental operations in the adaptive algorithm is selecting a point from within a subdomain. 492 | The current implementation uses simplices for subdomains (triangles in 2D, tetrahedrons in 3D), and picks a point either (1) in the center of the simplex or (2) on the longest edge of the simplex. 493 | The choice depends on the shape of the simplex; the center is only used if using the longest edge would produce unacceptably thin simplices. 494 | A better strategy may be to choose points on the edge of a simplex such that the simplex aligns with the gradient of the function, creating an anisotropic triangulation [@Dyn1990]. 495 | This is a similar approach to the anisotropic meshing techniques mentioned in the literature review. 496 | 497 | #### Learning stochastic functions is a promising direction. 498 | Stochastic processes frequently appear in numerical sciences. 499 | Currently, Adaptive has an `AverageLearner` that samples a random variable (modelled as a function that takes no parameters and returns a different value each time it is called) until the mean is known to within a certain standard error. 500 | This is advantageous because no predetermined number of samples has to be set before starting the simulation. 501 | Extending this learner to be able to deal with stochastic functions in arbitrary dimensions would be a useful addition. 502 | 503 | #### Experimental control needs to deal with noise, hysteresis, and the cost for changing parameters. 504 | Finally, there is the potential to use Adaptive for experimental control. 505 | There are a number of challenges associated with this use case. 506 | Firstly, experimental results are typically stochastic (due to noise), and would require sampling the same point in parameter space several times. 507 | This aspect is closely associated with sampling stochastic functions discussed in the preceding paragraph. 508 | Secondly, in an experiment one typically cannot jump around arbitrary quickly in parameter space. 509 | It may be faster to sweep one parameter compared to another; for example, in condensed matter physics experiments, sweeping magnetic field is much slower than sweeping voltage source frequency. 510 | Lastly, some experiments exhibit hysteresis. 511 | This means that results may not be reproducible if a different path is taken through parameter space. 512 | In such a case one would need to restrict the sampling to only occur along a certain path in parameter space. 513 | Incorporating such extensions into Adaptive would require adding a significant amount of extra logic, as learners would need to take into account not only the data available, but the order in which the data was obtained, and the timing statistics at different points in parameter space. 514 | Despite these challenges, however, Adaptive can already be used in experiments that are not restricted in these ways. 515 | --------------------------------------------------------------------------------